程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 線程安全,線程安全是什麼意思

線程安全,線程安全是什麼意思

編輯:JAVA綜合教程

線程安全,線程安全是什麼意思


最近學習並發編程,並發編程肯定和多線程、線程安全有關系,那麼下面是我總結了自己學習線程安全的筆記!!!

1.線程安全的概念

  多個線程訪問某一個類(對象或方法)時,這個類始終都能保持正確的行為,那麼這個類(對象或方法是線程安全的)。

2.synchronized

  可以在任意對象和方法上加鎖,而加鎖的這段代碼稱為“互斥區”或“臨界區”。

  a.示例

package com.wp.test;
public class MyThread extends Thread {
    private int count = 5;
    public synchronized void run(){
        count--;
        System.out.println(this.currentThread().getName()+" count="+count);
    }
    public static void main(String args[]){
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread,"t1");
        Thread t2 = new Thread(myThread,"t2");
        Thread t3 = new Thread(myThread,"t3");
        Thread t4 = new Thread(myThread,"t4");
        Thread t5 = new Thread(myThread,"t5");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

  b.示例總結

  如果run方法前沒有加關鍵字“synchronized”,那麼對於多個線程操作count變量,是不安全的。加上“synchronized”時,那麼線程是安全的;當多個線程訪問run方法時,以排隊的方式進行處理(此處的排隊是按照CPU分配的先後順序而定),一個線程想要執行這個synchronized修飾的run方法,首先要獲得鎖,如果獲取不到,便會一直等到獲取這把鎖為止,此時,如果有多個線程,那麼多個線程會競爭這把鎖,這時會有鎖競爭問題。鎖競爭問題很導致應用程序非常慢。

3.多個線程多個鎖

  多個線程,每個線程都可以拿到自己指定的鎖,分別獲得鎖之後執行鎖定的內容。此處有兩個概念:對象鎖和類鎖,示例總結將做解釋。

  a.示例

package com.wp.test;
 /**
  * 關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼(方法)當作鎖
  * 所以代碼中哪個線程先執行synchronized對應的方法,該線程就持有該方法所屬對象的鎖(LOCK)
  * 
  * 在靜態方法前加關鍵字synchronized,表示該方法的鎖是類級別的鎖。
 * 
 */
public class MultiThread {
    private  static int num = 0;
    /* static **/
    public static synchronized void printNum(String tag){
        try{
            if("a".equals(tag)){
                num = 100;
                System.out.println("tag a!set number over!");
                Thread.sleep(1000);
            }else{
                num = 200;
                System.out.println("tag b! set number over!");
            }
            System.out.println("num="+num);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    //注意觀察run方法輸出順序
    public static void main(String args[]){
        //兩個不同的對象
        final MultiThread m1 = new MultiThread();
        final MultiThread m2 = new MultiThread();
        Thread t1 = new Thread(new Runnable() {
            
            @Override
            public void run() {
                m1.printNum("a");
            }
        });
        Thread t2 = new Thread(new Runnable() {
            
            @Override
            public void run() {
                m2.printNum("b");
            }
        });
        t1.start();
        t2.start();
    }
}
View Code

 

  執行結果:

1. 無static:
tag a!set number over!
tag b! set number over!
num=200
num=100
2.有static
tag a!set number over!
num=100
tag b! set number over!

  b.示例總結

  代碼中線程取得的鎖都是對象鎖,而不是把一段代碼(或方法)當作鎖,對象鎖的特點是不同的對象對應著不同的鎖,互不影響。在靜態方法上加上關鍵字synchronized,表示鎖定的是.class類,屬於類級別的鎖。

4.對象鎖的同步和異步

  a.同步synchronized

  同步的概念是共享,如果多個線程共享資源,那麼就沒有必要進行同步了。

  b.異步asynchronized

  異步的概念是獨立,多個線程相互之間不受任何制約。

  c.同步的目的就是為了線程安全,對於線程安全,需要滿足兩個特性:1).原子性(同步)    2).可見性

  d.示例

 

package com.wp.test;
 /**
 * 對象鎖的同步和異步問題
 */
public class MyObject {
    public synchronized void method1(){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    public void method2(){
        System.out.println(Thread.currentThread().getName());
    }
    public static void main(String args[]){
        final MyObject mo = new MyObject();
        Thread t1 = new Thread(new Runnable() {
            
            @Override
            public void run() {
                mo.method1();
            }
        },"t1");
        Thread t2 = new Thread(new Runnable() {
            
            @Override
            public void run() {
                mo.method2();
            }
        },"t2");
        t1.start();
        t2.start();
    }
}
View Code

 

 

 

  示例總結:若t1線程先持有Object對象鎖,t2線程如果這個時候要調用對象中的同步(synchronized)方法則需要等待t1釋放鎖,才可以調用,也就說同步了;若t1線程先持有Object對象鎖,t2線程這個時候調用異步(非synchronized修飾)的方法,則會立即調用,t1和t2之間沒有影響,也就說是異步了。

 4.髒讀

  對於對象的同步和異步方法,我們在設計的時候一定要考慮問題的整體性,不然就會出現數據不一致的錯誤,很經典的一個錯誤就是髒讀。

  a.示例

package com.wp.test;
public class DirtyRead {
    private String uname = "sxt";
    private String pwd = "123";
    public synchronized void setValue(String uname,String pwd){
        try {
            this.uname = uname;
            Thread.sleep(2000);
            this.pwd = pwd;
            System.out.println("setValue設置的值:uname="+uname+" pwd="+pwd);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    public void getValue(){
        System.out.println("getValue的最終值:uname="+uname+" pwd="+pwd);
    }
    public static void main(String args[]) throws Exception{
        final DirtyRead dr = new DirtyRead();
        Thread t = new Thread(new Runnable() {
            
            @Override
            public void run() {
                dr.setValue("wangping", "456");
            }
        });
        t.start();
        Thread.sleep(1000);
        dr.getValue();
    }
}
/**getValue的最終值:uname=wangping pwd=123

  setValue設置的值:uname=wangping pwd=456*/
View Code

 

  b.示例分析

  pwd不一致,解決方法:getValue方法加關鍵字synchronized。加上對象鎖之後,等到鎖被釋放才可以獲得鎖。保持業務數據一致性。

  c.示例總結

  當我在給對象的方法加鎖的時候,一定要考慮業務的整體性,即示例中setValue和getValue方法都加synchronized鎖住,就保證了業務的原子性,保證業務不會出錯。

  d.oracle關系型數據庫,一致性讀實現原理

  案例描述:oracle,用戶A,9點查詢某條數據(100),9:10才可以查到所需數據。而用戶B在9:05執行了DML操作,那麼A所查的數據在數據庫已經變化(200)。那麼A在9:10查到的結果是100還是200?答案是:100。

      原因:oracle數據庫有一致性讀的特性。B在update時,會將之前的數據保存到undo中做記錄。當A在9:00這一時刻查時,將這一動作保存到ITL中,當A在9:10查100數據時,會判斷ITL中對該數據的動作是否更新(是否在9:00這一刻之後發生變化),如果更新了,那麼會從100對應的undo中將之前的數據返回。所以,結果為100。

  Undo的作用:提供一致性讀(Consistent Read)、回滾事務(Rollback Transaction)以及實例恢復(Instance Recovery)。

  詳細解釋:http://www.educity.cn/shujuku/1121393.html

5.synchronized鎖重入

  關鍵字synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個對象得到對象鎖之後,不釋放鎖,還可以再次獲得該對象的鎖,稱為鎖重入。

  a.示例1:方法鎖重入

package com.wp.test;
public class SyncDubbo1 {
    public synchronized void method1(){
        System.out.println("method1------------------");
        method2();
    }
    public synchronized void method2(){
        System.out.println("method2------------------");
        method3();
    }
    public synchronized void method3(){
        System.out.println("method3------------------");
    }
    public static void main(String args[]){
        final SyncDubbo1 s = new SyncDubbo1();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                s.method1();
            }
        });
        t.start();
    }
}
/**結果:

 * method1-----------------

 * method2------------------

* method3------------------
*/
View Code

 

  示例2:子類方法鎖重入

package com.wp.test;
public class SyncDubbo2 {
    static class Main{
        public int i = 10;
        public synchronized void operationSup(){
            try{
                i--;
                System.out.println("Main print i="+i);
                Thread.sleep(100);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    static class Sub extends Main{
        public synchronized void operationSub(){
            try{
                while(i>0){
                    i--;
                    System.out.println("Sub print i="+i);
                    Thread.sleep(100);
                    this.operationSup();
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    public static void main(String args[]){
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                Sub s = new Sub();
                s.operationSub();
            }
        });
        t.start();
    }
}
View Code

 

  結果:

Sub print i=9
Main print i=8
Sub print i=7
Main print i=6
Sub print i=5
Main print i=4
Sub print i=3
Main print i=2
Sub print i=1
Main print i=0

  示例3:鎖中的內容出現異常

  對於web應用程序,異常釋放鎖的情況,如果不特殊處理,那麼業務邏輯會出現很嚴重的錯,比如執行一個隊列任務,很對任務對象都在等待第一個對象正確執行完之後釋放鎖,但是執行第一個對象時出現了異常,導致剩余任務沒有執行,那麼業務邏輯就會出現嚴重錯誤,所以在設計代碼的時候一定要慎重考慮。

  解決方式:出現異常時,捕獲異常後通過記錄異常信息到日志文件,然後剩余任務對象繼續執行,那麼整個任務執行完之後,再對出現異常的那個任務對象進行處理,從而不會影響其他任務對象的執行。

package com.wp.test;
public class SyncException {
    private int i = 0;
    public synchronized void operation(){
        while(true){
            try{
                i++;
                Thread.sleep(200);
                System.out.println(Thread.currentThread().getName()+",i="+i);
                if(i==3){
                    Integer.parseInt("a");//throw RuntimeException
                }
            }catch(Exception e){
                e.printStackTrace();
                System.out.println("log info i="+i);
            }
        }
    }
    public static void main(String args[]){
        final SyncException se = new SyncException();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
               se.operation();
            }
        });
        t.start();
    }
}
View Code

 

  結果:執行方法的時候,出現異常,不會釋放鎖,業務繼續執行。執行完之後,對發生的異常進行處理。比如說存儲過程:當更新某張表的數據時,出現異常,那麼將異常信息保存到日志表中,稍後進行處理,而使得該表的數據繼續更新。

Thread-0,i=1
Thread-0,i=2
Thread-0,i=3
java.lang.NumberFormatException: For input string: "a"
log info i=3
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Integer.parseInt(Integer.java:492)
    at java.lang.Integer.parseInt(Integer.java:527)
    at com.wp.test.SyncException.operation(SyncException.java:11)
    at com.wp.test.SyncException$1.run(SyncException.java:24)
    at java.lang.Thread.run(Thread.java:745)
Thread-0,i=4
Thread-0,i=5
Thread-0,i=6

6.synchronized關鍵字

  使用synchronized聲明的方法在某些情況下是有弊端的,比如A線程調用同步的方法執行一個很長時間的任務,那麼B線程就必須等待同樣長的時間才能執行,這樣的情況下可以使用synchronized代碼塊去優化代碼執行時間,通常說,減小了鎖的粒度。

  Synchronized可以使用任意的Object進行加鎖,用法比較靈活。

  特別注意,就是不要使用String的常量加鎖,會出現死循環問題。

  鎖對象改變問題:當使用一個對象進行加鎖的時候,要注意對象本身是否發生變化(地址),如果發生變化,那麼就會釋放鎖。例如,如果是字符串的鎖,當字符串改變後就會釋放鎖。但是,對象的屬性發生變化,不會有影響的。

  a.示例1:synchronized代碼塊

package com.wp.test;
public class ObjectLock {
    public void method1(){
        synchronized(this){
            try {
                System.out.println("do method1...");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void method2(){
        synchronized(ObjectLock.class){
            try {
                System.out.println("do method2...");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    private Object lock = new Object();
    public void method3(){
        synchronized(lock){
            try {
                System.out.println("do method3...");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String args[]){
        final ObjectLock ol = new ObjectLock();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                ol.method1();
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                ol.method2();
            }
        });
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                ol.method3();
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}
View Code

 

  結果:

do method1...
do method3...
do method2...

  b.示例2:字符串鎖改變

package com.wp.test;
public class ChangeLock {
    private String lock = "lock";
    public void method(){
        synchronized(lock){
            try {

                System.out.println("當前線程:"+Thread.currentThread().getName()+"開始");
                lock = "lock1";
                Thread.sleep(2000);
                System.out.println("當前線程: "+Thread.currentThread().getName()+"結束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String args[]){
        final ChangeLock cl = new ChangeLock();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                cl.method();
            }
        },"t1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
               cl.method();
            }
        },"t2");
        t1.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
View Code

 

  結果:字符串改變之後,會釋放鎖。

  示例3:

package com.wp.test;
public class ModifyLock {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public synchronized void changeAttribute(String name,int age){
        try {
            System.out.println("當前線程:"+Thread.currentThread().getName()+"開始");
            this.setName(name);
            this.setAge(age);
            System.out.println("當前線程:"+Thread.currentThread().getName()+"修改的內容為:"
                    +this.getName()+","+this.getAge());
            Thread.sleep(2000);
            System.out.println("當前線程:"+Thread.currentThread().getName()+"結束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String args[]){
        final ModifyLock ml = new ModifyLock();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                ml.changeAttribute("張三", 20);
            }
        },"t1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
               ml.changeAttribute("李四", 21);
            }
        },"t2");
        t1.start();
        t2.start();
    }
}
View Code

 

  結果分析:如果對象本身不發生變化,那麼依然同步,屬性變化,沒有影響,依然同步。

7.volatile關鍵字

  當多個線程使用同一變量時,為了線程安全,通常我們會在訪問變量的地方加一把鎖,但是這樣效率比較低。但是Volatile的效率相對高點。

  Volatile關鍵字的主要作用是使用變量在多個線程間可見。原理:強制線程到主內存裡去讀取變量,而不去線程工作內存區去讀,那麼當前線程使用的變量是最新的變量(不管其他線程有無修改),從而實現了多個線程間變量可見,也就是滿足了線程安全的可見性。如果不明白,下面的示例,運行一下,就明白了。

package com.wp.test;
public class RunThread extends Thread{
    /**volatile*/
    private volatile boolean isRunning = true;
    public void setRunning(boolean isRunning){
        this.isRunning = isRunning;
    }
    public void run(){
        System.out.println("進入run方法。。");
        while(isRunning == true){
            //...
        }
        System.out.println("線程終止。。");
    }
    public static void main(String args[]) throws InterruptedException{
        RunThread rt = new RunThread();
        rt.start();
        Thread.sleep(3000);
        rt.setRunning(false);
        System.out.println("isRunning的值已經設置成了false");
        Thread.sleep(1000);
        System.out.println(rt.isRunning);
    }
}
View Code

 

  結果分析:isRunning不用volatile修飾時,當主線程修改isRunning的值時,線程rt的內存中的isRunning副本不會變化;isRunning用volatile修飾時,當主線程修改isRunning的值時,會強制線程rt從內存中讀取isRunning的值,那麼rt內存裡的isRunning也就發生了修改。

 

  Volatile關鍵字雖然擁有多個線程之間的可見性,但是卻不具備同步性(也就是原子性),可以算得上一個輕量級的synchronized,性能要比synchronized強很多,不會造成阻塞(在很多開源的框架裡,比如netty的底層代碼就是大量使用volatile,可見netty性能非常不錯。)這裡需要注意,一般volatile用於只針對於多個線程可見的變量操作,並不能代替synchronized的同步功能。具體來說,volatile關鍵字只有可見性,沒有原子性。要實現原子性,建議使用atomic類的系列對象,支持原子性操作(注意atomic類只保證本身方法原子性,並不保證多次操作的原子性)。用一個示例說明:

  示例1:volatile關鍵字只有可見性,沒有原子性,建議使用atomic類的系列對象

package com.wp.test;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileNoAtomic extends Thread {
    //private static volatile int count;
    private static AtomicInteger count = new AtomicInteger();
    private static void addCount(){
        for(int i=0;i<1000;i++){
            //count++;
            count.incrementAndGet();
        }
        System.out.println(count);
    }
    public void run(){
        addCount();
    }
    public static void main(String args[]){
        VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
        for(int i=0;i<10;i++){
            arr[i] = new VolatileNoAtomic();
        }
        for(int i=0;i<10;i++){
            arr[i].start();
        }
    }
}
View Code

  結果分析:當使用volatile修飾時,由於沒有原子性,因此,(線程不安全)結果達不到10000;那麼使用AtomicInteger時,具有原子性,結果正確為10000。

  示例2:atomic類只保證本身方法原子性,並不保證多次操作的原子性

package com.wp.test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicUse {
    private static AtomicInteger count = new AtomicInteger(0);
    //多個addAndGet在一個方法內是原子性的,需要加synchronized進行修飾,保證4個addAndGet整體原子性
    /**synchronized*/
    public synchronized int multiAdd(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count.addAndGet(1);
        count.addAndGet(2);
        count.addAndGet(3);
        count.addAndGet(4);
        return count.get();
    }
    public static void main(String args[]){
        final AtomicUse au = new AtomicUse();
        List<Thread> ts = new ArrayList<Thread>();
        for(int i=0;i<100;i++){
            ts.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(au.multiAdd());
                }
            }));
        }
        for(Thread t : ts){
            t.start();
        }
    }
}
View Code

  結果分析:多個addAndGet在一個方法內是原子性的,需要加synchronized進行修飾,保證4個addAndGet整體原子性。不加,每次加的值不是整10,加的話是整10。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved