java多線程系列(二)---對象變量併發訪問

對象變量的併發訪問

前言:本系列將從零開始講解java多線程相關的技術,內容參考於《java多線程核心技術》與《java併發編程實戰》等相關資料,但願站在巨人的肩膀上,再經過個人理解能讓知識更加簡單易懂。html

目錄

線程安全

  • 線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致或者數據污染。 線程不安全就是不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據是髒數據

局部變量並不會數據共享

public class T1 {
    public static void main(String[] args) {
        PrivateNum p=new PrivateNum();
        MyThread threadA=new MyThread('A',p);
        MyThread threadB=new MyThread('B',p);
        threadA.start();
        threadB.start();
    }}
    class MyThread extends Thread
    {
        char i;
        PrivateNum p;
        public MyThread(char i,PrivateNum p)
        {
            this.i=i;
            this.p=p;
        }
        public void run()
        {
            p.test(i);
        }
    }
    class PrivateNum
    {
        
        public void test( char i) 
        {
            try {
                int num=0;
                if(i=='A')
                {
                    num=100;
                    
                    System.out.println("線程A已經設置完畢");
                    Thread.sleep(1000);
                }
                else
                {
                    num=200;
                    System.out.println("線程B已經設置完畢");
                }
                System.out.println("線程"+i+"的值:"+num);
                
            }
         catch (InterruptedException e) {
            // TODO 自動生成的 catch 塊
            e.printStackTrace();
        }}
    }
    
    線程A已經設置完畢
    線程B已經設置完畢
    線程B的值:200
    線程A的值:100
  • 在這段代碼中,線程A和B前後對num進行賦值,當兩個線程都賦值後再分別打印,可是輸出的結果並不相同,後賦值的線程並無對num的值進行覆蓋,由於這裏的num是在方法裏面的,也就是局部變量,不一樣線程的num並不共享,因此並不會發生覆蓋。

實例成員變量數據共享

public void test( char i) 
        {
            int num=0;
            try {
            
                if(i=='A')
                {
                    num=100;
                    
                    System.out.println("線程A已經設置完畢");
                    Thread.sleep(1000);
                }
                else
                {
                    num=200;
                    System.out.println("線程B已經設置完畢");
                }
                System.out.println("線程"+i+"的值:"+num);
                
            }
         catch (InterruptedException e) {
            // TODO 自動生成的 catch 塊
            e.printStackTrace();
        }}
        線程B已經設置完畢
    線程A已經設置完畢
    線程B的值:200
    線程A的值:200
  • 這裏的代碼只是將int num=0放到了方法的外面,可是輸出的結果卻不一樣,由於這時候線程AB訪問的是同一個變量(指向同一個地址),因此這個時候會發生覆蓋,同時這裏出現了線程安全問題。

synchronized關鍵字能夠避免線程安全問題

public synchronized void test( char i) 
        {
            int num=0;
            try {
            
                if(i=='A')
                {
                    num=100;
                    
                    System.out.println("線程A已經設置完畢");
                    Thread.sleep(1000);
                }
                else
                {
                    num=200;
                    System.out.println("線程B已經設置完畢");
                }
                System.out.println("線程"+i+"的值:"+num);
                
            }
         catch (InterruptedException e) {
            // TODO 自動生成的 catch 塊
            e.printStackTrace();
        }}
    線程A已經設置完畢
    線程A的值:100
    線程B已經設置完畢
    線程B的值:200
  • 這裏只是上面代碼的基礎上增長了一個synchronized ,避免了線程安全問題

總結

  • 若是多個線程訪問的是同一個對象方法中的局部變量,那麼這個變量並不共享,線程AB對此變量的操做將互不影響
  • 若是多個線程訪問的是同一個對象方法中的成員變量,那麼這個變量共享,若是不處理好線程問題,可能會出現線程安全問題
  • 經過synchronized關鍵字可使方法同步

多個線程訪問的是兩個不一樣實例的同一個同步方法

public class T1 {
    public static void main(String[] args) {
        PrivateNum p1=new PrivateNum();
        PrivateNum p2=new PrivateNum();
        MyThread threadA=new MyThread('A',p1);
        MyThread threadB=new MyThread('B',p2);
        threadA.start();
        threadB.start();
    }}
    線程A已經設置完畢
    線程B已經設置完畢
    線程B的值:200  
    線程A的值:100
  • 這裏的代碼又是在上面的代碼進行修改,這裏咱們添加了synchronized關鍵字,對方法上鎖,可是倒是異步執行的(同步的話,應該是這樣輸出 線程A已經設置完畢 線程A的值:100 ),這是由於這裏是兩個鎖,建立了p1和p2對象,建立的是兩個鎖,鎖對象不一樣不形成互斥做用。

多線程調用同一個實例的兩個不一樣(一個同步,一個非同步)方法

public class T1 {
    public static void main(String[] args) {
        PrivateNum p1=new PrivateNum();
    
        MyThread threadA=new MyThread('A',p1);
        MyThread2 threadB=new MyThread2('B',p1);
        threadA.start();
        threadB.start();
    }}
    class MyThread extends Thread
    {
        char i;
        PrivateNum p;
        public MyThread(char i,PrivateNum p)
        {
            this.i=i;
            this.p=p;
        }
        public void run()
        {
            p.test(i);
        }
    }
    class MyThread2 extends Thread
    {
        char i;
        PrivateNum p;
        public MyThread2(char i,PrivateNum p)
        {
            this.i=i;
            this.p=p;
        }
        public void run()
        {
            p.test2(i);
        }
    }
    class PrivateNum
    {
        int num=0;
        public void test2(char i)
        {
            System.out.println("線程"+i+"執行,線程A並無同步執行");
        }
        public synchronized void  test( char i) 
        {
            
            try {
                
                if(i=='A')
                {
                    num=100;
                    
                    System.out.println("線程A已經設置完畢");
                    Thread.sleep(100);
                }
                else
                {
                    num=200;
                    System.out.println("線程B已經設置完畢");
                }
                System.out.println("線程"+i+"的值:"+num);
                
            }
         catch (InterruptedException e) {
            // TODO 自動生成的 catch 塊
            e.printStackTrace();
        }}
    }
    線程A已經設置完畢
    線程B執行,線程A並無同步執行
    線程A的值:100
    線程B的值:200
  • 這裏的代碼咱們給PrivateNum添加了一個非同步的test2方法,MyThreadrun中調用的同步的test方法,MyThread2中調用的是非同步的test2方法,實驗代表線程B能夠異步調用非同步的方法。

多線程調用同一個實例的兩個不一樣的同步方法

public synchronized void test2(char i)
        {
            System.out.println("線程"+i+"執行,線程A同步執行");
        }
    線程A已經設置完畢
    線程A的值:100
    線程B執行,線程A同步執行
    線程B的值:200
  • 這裏的代碼咱們只是給test2方法添加一個synchronized關鍵字,這個時候兩個線程調用的方法同步執行。

總結

  • 多個線程調用的不一樣實例的同步方法,線程不互斥。
  • 若是兩個線程的鎖對象同樣(都是p1),兩個線程分別調用同步方法和非同步方法,線程不會同步執行。
  • 可是若是調用兩個不一樣的同步方法,由於鎖對象一致,兩個線程同步執行。
  • 設想一個狀況,有一個實例有兩個方法,一個修改值(synchronized),一個讀值(非synchronized),此時兩個線程一個修改值,一個讀取值,這個時候由於這兩個線程並不會掙搶鎖,兩個線程互不影響,那麼此時可能就會出現一種狀況,線程A還沒修改完,線程B就讀取到沒有修改的值。這就是所謂的髒讀。

重入鎖

public class T1 {
    public static void main(String[] args) {

        MyThread3 thread=new MyThread3();
        thread.start();
    }}
    class MyThread3 extends Thread
    {
        Service s=new Service();
        public void run()
        {
            s.service1();
        }
    }
    class Service
    {
        public synchronized void service1()
        {
            System.out.println("服務1並無被鎖住");
            service2();
        }
        public synchronized void service2()
        {
            System.out.println("服務2並無被鎖住");
            service3();
        }
        public synchronized void service3()
        {
            System.out.println("服務3並無被鎖住");
        }
    }
    服務1並無被鎖住
服務2並無被鎖住
服務3並無被鎖住
  • 咱們可能會這麼認爲,thread線程執行了同步的service1方法,這個時候把鎖佔住,若是這個時候要執行另外一個同步方法service2方法,必須先執行完service1方法,而後把鎖讓出去才行,可是實驗證實鎖是能夠重入的,一個線程得到鎖後,還沒釋放後能夠再次獲取鎖。

出現異常會釋放鎖

  • 若是同步方法裏面出現異常,會自動將鎖釋放

同步方法不會繼承

public class T1 {
    public static void main(String[] args) {

        Service3 s=new Service3();
        MyThread4 t1=new MyThread4(s,'1');
        MyThread4 t2=new MyThread4(s,'2');
        t1.start();
        t2.start();
        
    }}
    class MyThread4 extends Thread
    {
        Service3 s;
        char name;
        public MyThread4(Service3 s,char name)
        {
            this.s=s;
            this.name=name;
        }
        public void run()
        {
            s.service(name);
        }
        
    }
    class Service2 
    {
        public synchronized void service(char name)
        {
            for (int i = 3; i >0; i--) {
                System.out.println(i);
            }
        }
    }
    class Service3 extends Service2 
    {
        public void service(char name)
        {
            for (int i = 5; i >0; i--) {
                System.out.println("線程"+name+":"+i);
            }
        }
    }
    線程1:5 線程2:5
  • 若是父類的方法是同步的,若是子類重載同步方法,可是沒有synchronized關鍵字,那麼是沒有同步做用的。

總結

  • 重入鎖,一個得到的鎖的線程沒執行完能夠繼續得到鎖。
  • 線程佔用鎖的時候,若是執行的同步出現異常,會將鎖讓出。
  • 父類方法同步,子類重寫該方法(沒有synchronized關鍵字修飾),是沒有同步做用的。

同步代碼塊

public class T1 {
    public static void main(String[] args) {
    Service2 s=new Service2();
        MyThread t1=new MyThread(s,'A');
        MyThread t2=new MyThread(s,'B');
        t1.start();
        t2.start();
        
    }
        
    }
    class Service2 
    {
        public  void service(char name)
        {
            synchronized(this)
                {
                for (int i = 3; i >0; i--) {
                    System.out.println(name+":"+i);
                }
            }
        }
    }
    class MyThread extends Thread
    {
        Service2 s=new Service2();
        char name;
        public  MyThread(Service2 s,char name)
        {
            this.s=s;
            this.name=name;
        }
        public void run()
        {
            s.service(name);
        }
    }
    A:3
    A:2
    A:1
    B:3
    B:2
    B:1
  • 當多個線程訪問同一個對象的synchronized(this)代碼塊時,一段時間內只有一個線程能執行

同步代碼塊的鎖對象

class Service2 
    {
        String s=new String("鎖");
        public  void service(char name)
        {
            
            synchronized(s)
                {
                for (int i = 3; i >0; i--) {
                    System.out.println(name+":"+i);
                }
            }
        }
        
    }
  • 將this換成本身建立的鎖(一個對象),一樣能夠實現同步功能

部分同步,部分異步

public  void service(char name)
        {
            for (int i = 6; i >3; i--) {
                System.out.println(name+":"+i);
            }
            synchronized(this)
                {
                for (int i = 3; i >0; i--) {
                    System.out.println(name+":"+i);
                }
            }
        }
        A:6
B:6
A:5
B:5
A:4
B:4
A:3
A:2
A:1
B:3
B:2
B:1
  • 不在同步代碼塊中的代碼能夠異步執行,在同步代碼塊中的代碼同步執行

不一樣方法裏面的synchronized代碼塊同步執行

public class T1 {
    public static void main(String[] args) {
    Service2 s=new Service2();
        MyThread t1=new MyThread(s,'A');
        MyThread2 t2=new MyThread2(s,'B');
        t1.start();
        t2.start();
        
    }
        
    }
    class Service2 
    {
        public  void service(char name)
        {
            
            synchronized(this)
                {
                for (int i = 3; i >0; i--) {
                    System.out.println(name+":"+i);
                }
            }
        }
        public void service2(char name)
        {
            synchronized(this)  {
                for (int i = 6; i >3; i--) {
                    System.out.println(name+":"+i);
                }
            }
        }
    }
    class MyThread extends Thread
    {
        Service2 s=new Service2();
        char name;
        public  MyThread(Service2 s,char name)
        {
            this.s=s;
            this.name=name;
        }
        public void run()
        {
            s.service(name);
        }
    }
    class MyThread2 extends Thread
    {
        Service2 s=new Service2();
        char name;
        public  MyThread2(Service2 s,char name)
        {
            this.s=s;
            this.name=name;
        }
        public void run()
        {
            s.service2(name);
        }
    }
A:3
A:2
A:1
B:6
B:5
B:4
  • 兩個線程訪問同一個對象的兩個同步代碼塊,這兩個代碼塊是同步執行的

鎖不一樣沒有互斥做用

class Service2 
    {
        Strign s=new String();
        public  void service(char name)
        {
            
            synchronized(s)
                {
                for (int i = 3; i >0; i--) {
                    System.out.println(name+":"+i);
                }
            }
        }
        public void service2(char name)
        {
            synchronized(this)  {
                for (int i = 6; i >3; i--) {
                    System.out.println(name+":"+i);
                }
            }
        }
    }
  • 將this改爲s,也就是改變鎖對象,發現兩個方法並非同步執行的

synchronized方法和synchronized(this)代碼塊是鎖定當前對象的

public  void service(char name)
        {
            
            synchronized(this)
                {
                for (int i = 3; i >0; i--) {
                    System.out.println(name+":"+i);
                }
            }
        }
public synchronized void service2(char name)
        {
                for (int i = 6; i >3; i--) {
                    System.out.println(name+":"+i);
                }
        }
  • 將service2的代碼塊改爲synchronized 方法,發現輸出結果是同步的的,說明鎖定的都是當前對象

總結

  • 同步代碼塊的鎖對象能夠是本對象,也能夠是其餘對象。同一個鎖對象能夠產生互斥做用,不一樣鎖對象不能產生互斥做用
  • 一個方法中有同步代碼塊和非同步代碼塊,同步代碼塊的代碼是同步執行的(塊裏的代碼一次執行完),而非同步代碼塊的代碼能夠異步執行
  • 一個對象中的不一樣同步代碼塊是互斥的,執行完一個代碼塊再執行另外一個代碼塊
  • 同步代碼塊(this)和synchronized方法的鎖定的都是當前對象 this

syncronized static 同步靜態方法

class Service2 
    {
        
        public  synchronized static void service()
        {
            
            
                for (int i = 3; i >0; i--) {
                    System.out.println(name+":"+i);
                }
            
        }
        
    }
  • 在這裏鎖對象就不是service對象,而是Class(Class(和String Integer同樣)是一個類)

Class鎖

class Service2 
    {
        
        public   static void service()
        {
            
            
                synchronized(Service.class)
                {
                for (int i = 3; i >0; i--) {
                    System.out.println(name+":"+i);
                }
            }
            
        }
        
    }
  • 這裏的效果和上面靜態的synchronized同樣

靜態類中非靜態同步方法

public class T1 {
    public static void main(String[] args) {

        Service.Service2 s=new Service.Service2();
        Thread t1=new Thread(new Runnable()
                {public void run(){s.service();}});
        Thread t2=new Thread(new Runnable()
        {public void run(){Service.Service2.service2();}});
        t1.start();
        
        t2.start();
    }
    
    
    
    }
    class Service{
    static class Service2 
    {
        
        public synchronized   void service()
        {
            

                for (int i = 20; i >10; i--) {
                    
                    System.out.println(i);
                }
            
        }
        public static synchronized void service2()
        {
            for (int i = 9; i >3; i--) {
                System.out.println(i);
            }
        }
        
    }}
    //不一樣步執行
  • 這裏service方法的鎖仍是service對象,

總結

  • Class類也能夠是鎖,Class鎖的實現能夠經過靜態的synchronizd方法,也能夠經過靜態方法裏面的同步代碼塊(鎖對象爲Class)
  • 靜態類的同步方法鎖對象仍是該類的一個實例

死鎖

public class DealThread implements Runnable {

    public String username;
    public Object lock1 = new Object();
    public Object lock2 = new Object();

    public void setFlag(String username) {
        this.username = username;
    }

    @Override
    public void run() {
        if (username.equals("a")) {
            synchronized (lock1) {
                try {
                    System.out.println("username = " + username);
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("按lock1->lock2代碼順序執行了");
                }
            }
        }
        if (username.equals("b")) {
            synchronized (lock2) {
                try {
                    System.out.println("username = " + username);
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("按lock2->lock1代碼順序執行了");
                }
            }
        }
    }

}
  • 線程AB開始執行時,由於鎖不一樣,因此不互斥,A當執行到另外一個同步代碼塊(鎖2)的時候,因爲這個時候鎖給線程B佔有了,因此只能等待,一樣B線程也是如此,AB互相搶對方的鎖,可是因此形成了死鎖。

鎖對象發生改變

  • 修改鎖對象的屬性不印象結果,好比此時鎖對象爲user對象,我把user的name設爲jiajun,此時不影響結果

volatile

public class Tee {
    public static void main(String[] args) {
    
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunning(false);
            System.out.println("已經賦值爲false");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        }
    }

    
    
    class RunThread extends Thread {

         private boolean isRunning = true;

        public boolean isRunning() {
            return isRunning;
        }

        public void setRunning(boolean isRunning) {
            this.isRunning = isRunning;
        }

        @Override
        public void run() {
            System.out.println("進入run了");
            while (isRunning == true) {
            }
            System.out.println("線程被中止了!");
        }

    }
  • 在這裏,把vm運行參數設置爲-server(右鍵運行配置,自變量那裏能夠設置,server參數能夠提升運行性能),結果發現雖然咱們將值設置爲false,可是卻仍然進入死循環。
  • isRunning變量存放在公共堆棧和線程的私有堆棧中,咱們對他賦值爲false時,只對公共堆棧進行更新,而但咱們設置爲-server後,讀取的是線程私有棧的內容,因此也就形成了死循環。咱們能夠在isRunning變量前加上volatite關鍵字,這個時候訪問的是公共堆棧,就不會形成死循環了。
  • 之前咱們使用單線程的時候,這種狀況狀況不會發生,可是當多個線程進行讀寫操做的時候就可能爆發出問題,這是由於咱們沒有用同步機制來保證他,這是咱們須要注意的一點。
public class Tee {
    public static void main(String[] args) {

        MyThread[] mythreadArray = new MyThread[100];
        for (int i = 0; i < 100; i++) {
            mythreadArray[i] = new MyThread();
        }

        for (int i = 0; i < 100; i++) {
            mythreadArray[i].start();
        }
    }

    
 class MyThread extends Thread {
        volatile public static int count;

         private static void addCount() {
            for (int i = 0; i < 100; i++) {
                count++;
            }
            System.out.println("count=" + count);
        }

        @Override
        public void run() {
            addCount();
        }

    }
  • 在這裏咱們只count前添加volatile,可是最終結果輸出的並非10000,說明並無同步的做用,volatile不處理數據原子性(i++不是原子操做)
  • 咱們將volatile去掉,將addcount方法用synchronized修飾,發現輸出了10000,說明了synchronized的同步做用不只保證了對同一個鎖線程的互斥,還保證了數據的同步。

總結

  • volitate增長了實例變量在對個線程之間的可見性,保證咱們得到的是變量的最新值。
  • volatile在讀上面保持了同步做用,可是在寫上面不保持同步
  • synchronized的同步做用不只保證了對同一個鎖線程的互斥,還保證了數據的同步

volatile對比synchronized

  • 二者修飾的不一樣,volatile修飾的是變量,synchronized修飾的是方法和代碼塊
  • 二者的功能不一樣。volatile保證數據的可見性,synchronized是線程同步執行(間接保證數據可見性,讓線程工做內存的變量和公共內存的同步)
  • volatile性能比synchronized性能高

用原子類實現i++同步

class MyThread extends Thread {
        static AtomicInteger count=new AtomicInteger(0);
         private  static void addCount() {
            for (int i = 0; i < 100; i++) {
                count.incrementAndGet();
            }
            System.out.println(count.get());
        }

        @Override
        public void run() {
            addCount();
        }

    }
  • 將上面的count++進行用原子類AtomicInteger改變,最後輸出了1000

我以爲分享是一種精神,分享是個人樂趣所在,不是說我以爲我講得必定是對的,我講得可能不少是不對的,可是我但願我講的東西是我人生的體驗和思考,是給不少人反思,也許給你一秒鐘、半秒鐘,哪怕說一句話有點道理,引起本身心裏的感觸,這就是我最大的價值。(這是我喜歡的一句話,也是我寫博客的初衷)

做者:jiajun 出處: http://www.cnblogs.com/-new/
本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。若是以爲還有幫助的話,能夠點一下右下角的【推薦】,但願可以持續的爲你們帶來好的技術文章!想跟我一塊兒進步麼?那就【關注】我吧。java

相關文章
相關標籤/搜索