Java中多線程訪問沖突的解決方式


當時用多線程訪問同一個資源時,非常容易出現線程安全的問題,例如當多個線程同時對一個數據進行修改時,會導致某些線程對數據的修改丟失。因此需要采用同步機制來解決這種問題。

第一種 同步方法

第二種 同步代碼塊

第三種 使用特殊成員變量(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 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM