當時用多線程訪問同一個資源時,非常容易出現線程安全的問題,例如當多個線程同時對一個數據進行修改時,會導致某些線程對數據的修改丟失。因此需要采用同步機制來解決這種問題。
第一種 同步方法
第二種 同步代碼塊
第三種 使用特殊成員變量(volatile 成員變量)實現線程同步(前提是對成員變量的操作是原子操作)
第四種 使用Lock接口(java.util.concurrent.locks包)
第五種 使用線程局部變量(thread-local)解決多線程對同一變量的訪問沖突,而不能實現同步(ThreadLocal類)
第六種 使用阻塞隊列實現線程同步(java.util.concurrent包)
第七種 使用原子變量實現線程同步 (java.util.concurrent.atomic包)
第一種 同步方法
同步方法即使用 synchronized關鍵字修飾的方法。在Java語言中,每個對象都有一個內置的對象鎖與之相關聯,該鎖會保護整個方法,即對象在任何時候只允許被一個線程所擁有,當一個線程調用對象的一段synchronized代碼時,首先需要獲得這個鎖,然后去執行相應的代碼,執行結束,釋放鎖。synchronized關鍵字也可以以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類。
synchronized關鍵字主要有兩種用法:synchronized方法和synchronized塊。此外該關鍵字還可以作用於靜態方法、類或某個實例,但這都對程序的效率有很大的影響。
給一個方法增加synchronized關鍵字之后就可以使它成為同步方法,這個方法可以是靜態方法和非靜態方法,但是不能是抽象類的抽象方法,也不能是接口中的抽象方法。
synchronized方法,在方法的聲明前加入synchronized關鍵字。例如
1 package com.test.multiThread; 2 3 public class Bank { 4 private int account = 0; 5 6 public int getAccount(){ 7 return account; 8 } 9 // 同步方法 10 public synchronized void save(int money){ 11 this.account += money; 12 } 13 } 14 15 ================================= 16 17 package com.test.multiThread; 18 19 public class MyThread implements Runnable { 20 private Bank bank; 21 public MyThread(Bank bank){ 22 this.bank = bank; 23 } 24 @Override 25 public void run() { 26 bank.save(1); 27 //bank.save01(1); 28 //bank.save02(1); 29 } 30 } 31 32 ================================= 33 34 package com.test.multiThread; 35 36 import java.util.ArrayList; 37 38 public class MultiThreadDemo { 39 public static void main(String[] args) throws InterruptedException { 40 Bank bank = new Bank(); 41 System.out.println(bank.getAccount()); 42 ArrayList<Thread> list = new ArrayList<>(); 43 for (int i = 0; i < 100000; i++){ 44 list.add(new Thread(new MyThread(bank))); 45 } 46 for (Thread thread: list){ 47 thread.start(); 48 } 49 for (Thread thread: list){ 50 thread.join(); 51 } 52 System.out.println(bank.getAccount()); 53 } 54 }
只要把多線程訪問的資源的操作放到multiThreadAccess方法中,就能夠保證這個方法在同一時刻只能被一個線程訪問,從而保證了多線程訪問的安全性。然而當一個方法的方法體規模非常大時,把該方法聲明為synchronized會大大影響程序的執行效率。為了提高程序的執行效率,Java語言提供了synchronized塊。
第二種 同步代碼塊
即synchronized關鍵字修飾的語句塊。被synchronized修飾的語句塊會自動被加上內置鎖,從而實現同步。
同步是一種高開銷的操作,因此應該盡量減少同步的內容,通常沒有必要使用同步方法,使用同步代碼塊來同步關鍵代碼即可。
可以把任意的代碼塊聲明為synchronized,也可以制定上鎖的對象,有非常高的靈活性。用法如下
1 package com.test.multiThread; 2 3 public class Bank { 4 private int account = 0; 5 6 public int getAccount(){ 7 return account; 8 } 9 // 同步代碼塊 10 public void save(int money){ 11 synchronized (this){ 12 this.account += money; 13 } 14 } 15 } 16 17 =============================== 18 19 package com.test.multiThread; 20 21 public class MyThread implements Runnable { 22 private Bank bank; 23 public MyThread(Bank bank){ 24 this.bank = bank; 25 } 26 @Override 27 public void run() { 28 bank.save(1); 29 } 30 } 31 32 33 =============================== 34 35 package com.test.multiThread; 36 37 import java.util.ArrayList; 38 39 public class MultiThreadDemo { 40 public static void main(String[] args) throws InterruptedException { 41 Bank bank = new Bank(); 42 System.out.println(bank.getAccount()); 43 ArrayList<Thread> list = new ArrayList<>(); 44 for (int i = 0; i < 100000; i++){ 45 list.add(new Thread(new MyThread(bank))); 46 } 47 for (Thread thread: list){ 48 thread.start(); 49 } 50 for (Thread thread: list){ 51 thread.join(); 52 } 53 System.out.println(bank.getAccount()); 54 } 55 }
當使用synchronized來修飾某個共享資源的時候,如果線程Thread01在執行synchronized代碼,另外一個線程Thread02也要同時執行同一對象的統一synchronized代碼時,線程Thread02將要等到線程Thread01執行成后才能繼續執行。在這種情況下,可以使用wait()方法和notify()方法。
在synchronized代碼被執行期間,線程可以調用對象的wait()方法,釋放對象鎖,進入等待狀態,並且可以調用notify()方法或者notifyAll()方法通知正在等待的而其他線程,notify()喚醒一個線程(等待隊列中的第一個線程),並允許它去獲得鎖,而notifyAll()方法喚醒所有等待這個對象的線程,並允許它們去競爭獲得鎖。
第三種 使用特殊成員變量(volatile 成員變量)實現線程同步(前提是對成員變量的操作是原子操作)
volatile是一個類型修飾符,被設計用來修飾被不同線程訪問和修飾的變量。當變量沒有被volatile修飾時,線程讀取數據時可能會從緩存中去讀取,如果其他線程修改了該變量,則無法讀取到修改后的數據。當變量被volatile修飾時,線程每次使用時都會直接到內存中提取,而不會利用緩存,從而保證了數據的同步。
volatile關鍵字主要目的是放置編譯器對代碼的優化,使得每次使用數據的時候都從內存里提取,而不是緩存,保證獲得的數據是最新被修改的數據。但是volatile不能保證操作的原子性,一般不能替代synchronized代碼塊,除非對變量的操作是原子操作的情況下才可以使用volatile。
① volatile關鍵字為成員變量的訪問提供了一種免鎖機制,但要保證對成員變量的操作是原子操作的情況下才能使用
② volatile關鍵字相當於告訴虛擬機該成員變量可能會被其他線程修改
③ 每次使用被volatile修飾的成員變量都要從內存提取,重新計算,而不會使用寄存機器中的值
④ volatile不會提供任何原子操作,不能保證線程安全
⑤ volatile不能用來修飾final類型的變量
⑥ 使用volatile會降低程序的執行效率
Java中原子性保證:Java內存模型只保證了基本讀取和復制是原子性操作,如果要實現更大范圍操作的原子性,可以通過synchronized和Lock保證任一時刻只有一個線程執行該代碼,那么自然就不存在原子性問題了,從而保證了原子性。
Java中可見性保證:synchronized和Lock、volatile三種,推薦使用synchronized方式,volatile有局限,適合某個特定場合。
第四種 使用Lock接口(java.util.concurrent.locks包)
JDK5新增了一個java.util.concurrent.locks包來支持同步。該包中提供了Lock接口以及它的一個實現類ReentrantLock(重入鎖)
Lock接口也可以用來實現多線程的同步,其提供了如下方法來實現多線程的同步
1 public abstract void lock() // 以阻塞方式來獲得鎖,即如果獲得了鎖就立即返回,如果其他線程持有鎖,當前線程等待,直到獲取鎖后返回。當前線程會一直處於阻塞狀態,且會忽略interrupt()方法 2 public abstract boolean tryLock() // 以非阻塞的方式獲得鎖,即嘗試性的去獲取鎖,如果獲得鎖就返回true,否則返回false 3 public abstract boolean tryLock(long time, TimeUnit unit) // 如果在給定時間內獲得鎖,返回true,否則返回false 4 public abstract void lockInterruptibly // 如果獲得鎖,則立即返回,如果沒有獲得鎖,則當前線程會處於休眠狀態,直到獲得鎖,或者當前線程被其他線程中斷(會收到InterruptedException異常)。 5 public abstract void unlock // 釋放鎖
ReentrantLock類的構造方法
1 public ReentrantLock() // 創建一個ReentrantLock實例 2 public ReentrantLock(boolean fair) // 創建公平鎖的構造方法,但由於能大幅度降低程序運行效率,不推薦使用
使用Lock接口實現多線程同步的例子
1 package com.test.multiThread; 2 3 import java.util.concurrent.locks.Lock; 4 import java.util.concurrent.locks.ReentrantLock; 5 6 public class Bank { 7 private int account = 0; 8 private Lock lock = new ReentrantLock(); // 聲明這個重入鎖 9 10 public int getAccount(){ 11 return account; 12 } 13 public void save(int money){ 14 lock.lock(); // 以阻塞方式獲得鎖 15 try { 16 account += money; 17 } finally { 18 lock.unlock(); // 釋放鎖 19 } 20 } 21 } 22 23 ============================= 24 25 package com.test.multiThread; 26 27 public class MyThread implements Runnable { 28 private Bank bank; 29 public MyThread(Bank bank){ 30 this.bank = bank; 31 } 32 @Override 33 public void run() { 34 bank.save(1); 35 } 36 } 37 38 ============================= 39 40 package com.test.multiThread; 41 42 import java.util.ArrayList; 43 44 public class MultiThreadDemo { 45 public static void main(String[] args) throws InterruptedException { 46 Bank bank = new Bank(); 47 System.out.println(bank.getAccount()); 48 ArrayList<Thread> list = new ArrayList<>(); 49 for (int i = 0; i < 100000; i++){ 50 list.add(new Thread(new MyThread(bank))); 51 } 52 for (Thread thread: list){ 53 thread.start(); 54 } 55 for (Thread thread: list){ 56 thread.join(); 57 } 58 System.out.println(bank.getAccount()); 59 } 60 }
第五種 使用線程局部變量(thread-local)解決多線程對同一變量的訪問沖突,而不能實現同步 (ThreadLocal類)
1 public class ThreadLocal<T> 2 extends Object
如果使用ThreadLocal來管理變量,則每一個使用該變量的線程都會獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。所以對於同線程對共享變量的操作互不影響。
public class ThreadLocal<T> extends Object 常用方法 public ThreadLocal() // 構造方法 public T get() // 返回次線程局部變量的當前線程副本中的值 public void set(T value) // 將次線程局部變量的當前線程副本中的值設置為value protected T initialValue() // 返回次線程局部變量的當前線程的初始值 public void remove() //
Thread-local與同步機制的比較:
1)兩者都是為了解決多線程中相同變量的訪問沖突問題
2)Thread-local采用“空間換時間”方法,同步機制采用“時間換空間”的方式
使用Thread-local的例子
1 package com.test.multiThread; 2 3 public class Bank { 4 private static ThreadLocal<Integer> account = ThreadLocal.withInitial(() -> 0); 5 public void save(int money){ 6 account.set(account.get() + money); 7 } 8 public int getAccount(){ 9 return account.get(); 10 } 11 } 12 13 ============================ 14 15 package com.test.multiThread; 16 17 public class MyThread implements Runnable { 18 private Bank bank; 19 public MyThread(Bank bank){ 20 this.bank = bank; 21 } 22 @Override 23 public void run() { 24 for (int i = 1; i < 10; i++){ 25 bank.save(i); 26 } 27 System.out.println("Thread-local中的值: " + bank.getAccount()); 28 } 29 } 30 31 ============================ 32 33 package com.test.multiThread; 34 35 import java.util.ArrayList; 36 37 public class MultiThreadDemo { 38 public static void main(String[] args) throws InterruptedException { 39 Bank bank = new Bank(); 40 System.out.println("原始值:" + bank.getAccount()); 41 ArrayList<Thread> list = new ArrayList<>(); 42 for (int i = 0; i < 10; i++){ 43 list.add(new Thread(new MyThread(bank))); 44 } 45 for (Thread thread: list){ 46 thread.start(); 47 } 48 for (Thread thread: list){ 49 thread.join(); 50 } 51 System.out.println("原始值:" + bank.getAccount()); 52 } 53 }
結果:改變的只是線程中變量的值,線程結束后Thread-local變量就銷毀了
第六種 使用阻塞隊列實現線程同步(java.util.concurrent包)
在JDK5提供的java.util.concurrent包中的 Class LinkedBlockingQueue<E> 可以實現線程的同步。
LinkedBlockingQueue<E>是一個基於已連接節點的,范圍任意的blocking queue。其常用方法如下:
1 public LinkedBlockingQueue() //創建一個容量為Interger.MAX_VALUE的LinkedBlockingQueue 2 public int size() // 返回隊列中的元素個數 3 public void put(E e) throws InterruptedException // 在隊尾添加一個元素,如果隊列滿則阻塞 4 public E take() throws InterruptedException // 返回並移除對首元素,如果隊列空則阻塞
使用阻塞隊列實現生產者-消費者。總的來說生產者的速度和消費者的速度相同,但是因為阻塞隊列的緣故,不需要控制阻塞,當阻塞對列滿的時候,生產者線程就會被阻塞,直到不再滿;反之亦然,當消費者線程多於生產者線程時,消費者速度大於生產者速度,當隊列為空時,就會阻塞消費者線程,直到隊列非空。
1 package com.test.multiThread; 2 3 import java.util.concurrent.BlockingQueue; 4 import java.util.concurrent.LinkedBlockingQueue; 5 6 public class WorkDesk { 7 private BlockingQueue<String> desk = new LinkedBlockingQueue<>(10); 8 public void washDish() throws InterruptedException{ 9 desk.put("盤子"); 10 } 11 public String useDish() throws InterruptedException{ 12 return desk.take(); 13 } 14 } 15 16 ================================= 17 18 package com.test.multiThread; 19 20 public class Producer implements Runnable { 21 private String producerName; 22 private WorkDesk workDesk; 23 24 public Producer(String producerName, WorkDesk workDesk){ 25 this.producerName = producerName; 26 this.workDesk = workDesk; 27 } 28 @Override 29 public void run() { 30 try { 31 while (true) { 32 workDesk.washDish(); 33 System.out.println(producerName + "洗好一個盤子"); 34 Thread.sleep(1000); 35 } 36 } catch (Exception e){ 37 e.printStackTrace(); 38 } 39 } 40 } 41 42 ================================= 43 44 package com.test.multiThread; 45 46 public class Consumer implements Runnable { 47 private String consumerName; 48 private WorkDesk workDesk; 49 50 public Consumer(String consumerName, WorkDesk workDesk){ 51 this.consumerName = consumerName; 52 this.workDesk = workDesk; 53 } 54 55 @Override 56 public void run() { 57 try { 58 while (true) { 59 workDesk.useDish(); 60 System.out.println(consumerName + "使用一個盤子"); 61 Thread.sleep(1000); 62 } 63 } catch (Exception e){ 64 e.printStackTrace(); 65 } 66 } 67 } 68 69 ================================= 70 71 package com.test.multiThread; 72 73 import java.util.concurrent.ExecutorService; 74 import java.util.concurrent.Executors; 75 76 public class TestBlockingQueue { 77 public static void main(String[] args){ 78 WorkDesk workDesk = new WorkDesk(); 79 80 ExecutorService service = Executors.newCachedThreadPool(); 81 Producer producer01 = new Producer("生產者-1-", workDesk); 82 Producer producer02 = new Producer("生產者-2-", workDesk); 83 84 Consumer consumer01 = new Consumer("消費者-1-", workDesk); 85 Consumer consumer02 = new Consumer("消費者-2-", workDesk); 86 87 service.submit(producer01); 88 service.submit(producer02); 89 service.submit(consumer01); 90 service.submit(consumer02); 91 } 92 }
第七種 使用原子變量實現線程同步(java.util.concurrent.atomic包)
需要使用線程同步的根本原因在於對普通變量的操作不是原子的。
原子操作就是指將讀取變量值、修改變量值、保存變量值看成一個整體來操作,即這幾步要么同時完成,要么都不完成。
在JDK5中提供的java.util.concurrent.atomic包中提供了創建原子類型變量的工具類,使用這些工具類能夠簡化線程同步。
1 package com.test.multiThread; 2 3 import java.util.concurrent.atomic.AtomicInteger; 4 5 public class Bank { 6 private AtomicInteger account = new AtomicInteger(0); // 創建具有給定初始值的新的AtomicInteger 7 8 public int getAccount(){ 9 return account.get(); // 獲取當前值 10 } 11 12 public void save(int money){ 13 account.addAndGet(money); // 以原子方式將給定值與當前值相加 14 } 15 } 16 17 ================================ 18 19 package com.test.multiThread; 20 21 public class MyThread implements Runnable { 22 private Bank bank; 23 public MyThread(Bank bank){ 24 this.bank = bank; 25 } 26 @Override 27 public void run() { 28 bank.save(1); 29 } 30 } 31 32 ================================ 33 34 package com.test.multiThread; 35 36 import java.util.ArrayList; 37 38 public class MultiThreadDemo { 39 public static void main(String[] args) throws InterruptedException { 40 Bank bank = new Bank(); 41 System.out.println("原始值:" + bank.getAccount()); 42 ArrayList<Thread> list = new ArrayList<>(); 43 for (int i = 0; i < 100000; i++){ 44 list.add(new Thread(new MyThread(bank))); 45 } 46 for (Thread thread: list){ 47 thread.start(); 48 } 49 for (Thread thread: list){ 50 thread.join(); 51 } 52 System.out.println("線程執行完后:" + bank.getAccount()); 53 } 54 }