1、多線程的同步:
在多線程中,可能有多個線程試圖訪問一個有限的資源,必須預防這種情況的發生。所以引入了同步機制:在線程使用一個資源時為其加鎖,這樣其他的線程便不能訪問那個資源了,直到解鎖后才可以訪問。
成員變量:
如果一個變量是成員變量,那么多個線程對同一個對象的成員變量進行操作,這多個線程是共享一個成員變量的。
局部變量:
如果一個變量是局部變量,那么多個線程對同一個對象進行操作,每個線程都會有一個該局部變量的拷貝。他們之間的局部變量互不影響。
實現了Runnable的線程類:
class MyThread3 implements Runnable{ //兩個線程操作同一個對象,共享成員變量 //int i; @Override public void run() { //兩個線程操作同一個對象,各自保存局部變量的拷貝 int i = 0; while(i<100){ System.out.println(i); i++; try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }在main方法中用兩個線程操作同一個對象: public static void main(String[] args) { MyThread3 myThread = new MyThread3(); //下面兩個線程對同一個對象(Runnable的實現類對象)進行操作 Thread thread = new Thread(myThread); Thread thread2 = new Thread(myThread); //各自保存局部變量的拷貝,互不影響,輸出200個數字 thread.start(); thread2.start(); }
這里如果把i變成成員變量,則輸出100個數字。
下面舉個例子,兩個線程共用一個Number對象,通過Number類的getNumber方法獲取數據,讀取數據並改寫時,發現了重復讀操作:
首先創建一個Number類:
class Number{ private int number = 10; public String getNumber(int i){ if(number > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } number -= i; return "取出"+i+"成功,剩余數量:"+number; } return "取出"+i+"失敗,剩余數量:"+number; } }線程類,在線程類中的私有屬性包含了Number類的引用: class MyThread4 extends Thread{ //兩個線程操作同一個對象,共享成員變量 Number number; public MyThread4(Number number){ this.number = number; } @Override public void run() { System.out.println(number.getNumber(8)); } }在main函數中創建兩個線程類,包含了同一個Number類實例的引用: public static void main(String[] args) { Number number = new Number(); //兩個線程操作同一個對象,共享對象number的成員變量number MyThread4 myThread = new MyThread4(number); MyThread4 myThread2 = new MyThread4(number); myThread.start(); myThread2.start(); }
這樣,當第一個線程讀取Number中的number變量時先保存下來再休眠0.1秒,然后第二個線程再讀取number變量並保存,此時兩個線程保存了同樣的數字,在修改時,也就導致修改了同一個數字兩次。
使用synchronized關鍵字,該關鍵字修飾的方法叫做同步方法。
Java中每個對象都有一個鎖或者稱為監視器,當訪問某個對象的synchronized方法時,表示將該對象上鎖,而不僅僅是為該方法上鎖。
這樣如果一個對象的synchronized方法被某個線程執行時,其他線程無法訪問該對象的任何synchronized方法(但是可以調用其他非synchronized的方法)。直至該synchronized方法執行完。
當調用一個對象的靜態synchronized方法時,它鎖定的並不是synchronized方法所在的對象,而是synchronized方法所在對象對應的Class對象。這樣,其他線程就不能調用該類的其他靜態synchronized方法了,但是可以調用非靜態的synchronized方法。
結論:執行靜態synchronized方法鎖方法所在對象,執行非靜態synchronized方法鎖方法所在對象對應的Class對象。
下面是多線程調用靜態的方法的例子,由於鎖定了方法所在對象對應的Class對象,其他線程無法調用該方法所在對象其他的靜態synchronized方法:
/** * 定義一個類,包含了線程類需要調用的方法 */ class Compute1{ //這時如果某個線程調用該方法, //將鎖定synchronized方法所在對象對應的class對象, //而不是鎖定synchronized方法所在對象 public synchronized static void execute(){ for(int i = 0; i<100; i++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("compute1:execute1 " + i++); } } public synchronized static void execute2(){ for(int i = 0; i<100; i++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("compute1:execute2 " + i++); } } }main方法中兩個線程分別調用同一個對象的兩個static synchronized方法: public static void main(String[] args) { Compute1 com = new Compute1(); Thread thread1 = new Thread1(com); Thread thread2 = new Thread2(com); thread1.start(); thread2.start(); }
一次只能調用一個靜態方法,直到執行完成。
通過使用synchronized同步代碼塊,鎖定一個對象,該對象作為可執行的標志從而達到同步的效果:
/** * 定義一個類,包含了線程類需要調用的方法 */ class Compute1{ //通過同步代碼塊鎖定object1對象進行鎖定了其他同樣的synchronized代碼塊 private Object object1 = new Object(); public void execute(){ synchronized(object1){ for(int i = 0; i<100; i++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("compute1:execute1 " + i++); } } } public synchronized void execute2(){ synchronized(object1){ for(int i = 0; i<100; i++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("compute1:execute2 " + i++); } } } }
如果想要使用synchronized同步代碼塊達到和使用synchronized方法同樣的效果,可以鎖定this引用:
synchronized(this){ … }
synchronized同步代碼塊只是鎖定了該代碼塊,代碼塊外面的代碼還是可以被訪問的。
synchronized方法是粗粒度的並發控制,某一個時刻只能有一個線程執行該synchronized方法。
synchronized同步代碼塊是細粒度的並發控制,只會將塊中的代碼同步,代碼塊之外的代碼可以被其他線程同時訪問。
