互斥同步(synchronized、Lock、ReentrantLock、ReadWriteLock、ReentrantReadWriteLock)
互斥同步
Java 提供了兩種鎖機制來控制多個線程對共享資源的互斥訪問:
1. synchronized
synchronized關鍵字保證在同一時刻,只有一個線程可以執(zhí)行某個對象內某一個方法或某一段代碼塊。
重量級鎖。包含兩個特征:互斥性和可見性。
synchronized可以解決一個線程看到對象處于不一致的狀態(tài),可以保證進入同步方法或者同步代碼塊的每個線程都可以看到由同一個鎖保護之前所有的修改效果。
實現(xiàn)同步的基礎:Java中每個對象都可作為鎖。
1.1 同步一個代碼塊
public void func() {synchronized (this) {// ...} }它只作用于同一個對象,如果調用兩個對象上的同步代碼塊,就不會進行同步。
對于以下代碼,使用 ExecutorService 執(zhí)行了兩個線程,由于調用的是同一個對象的同步代碼塊,因此這兩個線程會進行同步。
當一個線程進入同步語句塊時,另一個線程就必須等待。
public class SynchronizedExample {public void func1() {synchronized (this) {for (int i = 0; i < 10; i++) {System.out.print(i + " ");}}} }public static void main(String[] args) {SynchronizedExample e1 = new SynchronizedExample();ExecutorService executorService = Executors.newCachedThreadPool();executorService.execute(() -> e1.func1()); executorService.execute(() -> e1.func1()); }運行結果:
對于以下代碼,兩個線程調用了不同對象的同步代碼塊,因此這兩個線程就不需要同步。
從輸出結果可以看出,兩個線程交叉執(zhí)行。
public static void main(String[] args) {SynchronizedExample e1 = new SynchronizedExample();SynchronizedExample e2 = new SynchronizedExample();ExecutorService executorService = Executors.newCachedThreadPool();executorService.execute(() -> e1.func1());executorService.execute(() -> e2.func1()); }運行結果:
1.2 同步一個方法
public synchronized void func () {// ... }它和同步代碼塊一樣,作用于同一個對象。
1.3?同步一個類
public void func() {synchronized (SynchronizedExample.class) {// ...} }作用于整個類,也就是說兩個線程調用同一個類的不同對象上的這種同步語句,也會進行同步。
public class SynchronizedExample {public void func2() {synchronized (SynchronizedExample.class) {for (int i = 0; i < 10; i++) {System.out.print(i + " ");}}} }public static void main(String[] args) {SynchronizedExample e1 = new SynchronizedExample();SynchronizedExample e2 = new SynchronizedExample();ExecutorService executorService = Executors.newCachedThreadPool();executorService.execute(() -> e1.func2());executorService.execute(() -> e2.func2()); }運行結果:
1.4?同步一個靜態(tài)方法
public synchronized static void fun() {// ... }作用于整個類。
1.5 synchronized缺陷:
- 不可中斷:A執(zhí)行,等待。當A阻塞時,B得一直等待
- 如果多個線程都只是進行讀操作,當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。(一般上希望讀鎖是共享鎖,而寫鎖是排它鎖)
- 無法知道線程有沒有成功獲取到鎖
2. java.util.concurrent.locks包中常用的類和接口
2.1 Lock
Lock是一個接口:
public interface Lock {void lock();void lockInterruptibly() throws InterruptedException;boolean tryLock();boolean tryLock(long time, TimeUnit unit) throws InterruptedException;void unlock();Condition newCondition(); }采用Lock,必須主動去釋放鎖,并且在發(fā)生異常時,不會自動釋放鎖。
因此一般來說,使用Lock必須在try{}catch{}塊中進行,并且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發(fā)生。
2.1.1 獲取鎖
lock()方法
是平常使用得最多的用來獲取鎖的一個方法。如果鎖已被其他線程獲取,則進行等待。
Lock lock = ...; lock.lock(); try{... //處理任務 }catch(Exception ex){... }finally{lock.unlock(); //釋放鎖 }?tryLock()方法
有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。
Lock lock = ...; if(lock.tryLock()) {try{...//處理任務}catch(Exception ex){...}finally{lock.unlock(); //釋放鎖} }else {...//如果不能獲取鎖,則直接做其他事情 }lockInterruptibly()方法
比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態(tài)。
也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
由于lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。
public void method() throws InterruptedException {lock.lockInterruptibly();try { //.....}finally {lock.unlock();} }??注意,當一個線程獲取了鎖之后,是不會被interrupt()方法中斷的。
因為單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。
因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。
而用synchronized修飾的話,當一個線程處于等待某個鎖的狀態(tài),是無法被中斷的,只有一直等待下去。
2.2 ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C) 包中的鎖。
唯一實現(xiàn)了Lock接口的類,并且ReentrantLock提供了更多的方法。
2.2.1 lock()
public class Test {private ArrayList<Integer> arrayList = new ArrayList<Integer>();public static void main(String[] args) {final Test test = new Test(); new Thread(){public void run() {test.insert(Thread.currentThread());};}.start();new Thread(){public void run() {test.insert(Thread.currentThread());};}.start();} public void insert(Thread thread) {Lock lock = new ReentrantLock(); //注意這個地方lock.lock();try {System.out.println(thread.getName()+"得到了鎖");for(int i=0;i<5;i++) {arrayList.add(i);}} catch (Exception e) {// TODO: handle exception}finally {System.out.println(thread.getName()+"釋放了鎖");lock.unlock();}} }運行結果:
Thread-0得到了鎖
Thread-1得到了鎖
Thread-0釋放了鎖
Thread-1釋放了鎖
為什么第二個線程怎么會在第一個線程釋放鎖之前得到了鎖?
原因:在insert方法中的lock變量是局部變量,每個線程執(zhí)行該方法時都會保存一個副本,那么理所當然每個線程執(zhí)行到lock.lock()處獲取的是不同的鎖,所以就不會發(fā)生沖突。
知道了原因改起來就比較容易了,只需要將lock聲明為類的屬性即可。
import java.util.ArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;public class Test{Lock lock = new ReentrantLock(); //注意這個地方private ArrayList<Integer> arrayList = new ArrayList<Integer>();public static void main(String[] args) {final Testtest = new Test(); new Thread(){public void run() {test.insert(Thread.currentThread());};}.start();new Thread(){public void run() {test.insert(Thread.currentThread());};}.start();} public void insert(Thread thread) {lock.lock();try {System.out.println(thread.getName()+"得到了鎖");for(int i=0;i<5;i++) {arrayList.add(i);}} catch (Exception e) {// TODO: handle exception}finally {System.out.println(thread.getName()+"釋放了鎖");lock.unlock();}} }2.2.2 tryLock()
package Test1;import java.util.ArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;public class Code1 {Lock lock = new ReentrantLock(); //注意這個地方private ArrayList<Integer> arrayList = new ArrayList<Integer>();public static void main(String[] args) {final Code1 test = new Code1(); new Thread(){public void run() {test.insert(Thread.currentThread());};}.start();new Thread(){public void run() {test.insert(Thread.currentThread());};}.start();} public void insert(Thread thread) {if(lock.tryLock()) {try {System.out.println(thread.getName()+"得到了鎖");for(int i=0;i<5;i++) {arrayList.add(i);}} catch (Exception e) {// TODO: handle exception}finally {System.out.println(thread.getName()+"釋放了鎖");lock.unlock();}}else {System.out.println(thread.getName()+"未獲取到鎖");}} }運行結果:
Thread-0得到了鎖
Thread-0釋放了鎖
Thread-1未獲取到鎖
2.2.3?lockInterruptibly()
package Test1;import java.util.concurrent.locks.*;public class Code1 implements Runnable{Lock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {Code1 test = new Code1();//線程1Thread thread1 = new Thread(test);thread1.start();//線程2Thread thread2 = new Thread(test);thread2.start();//讓主線程等待兩秒,讓線程2嘗試獲取線程1中的鎖,兩秒后嘗試中斷線程2Thread.sleep(2000);thread2.interrupt();}public void run(){try {insert();} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName()+"被中斷");e.printStackTrace();}} public void insert() throws InterruptedException {lock.lockInterruptibly();/* 要想得到正確的中斷等待線程,* 獲取鎖必須放在trycatch語句塊外* 然后拋出InterruptedException*/System.out.println(Thread.currentThread().getName() + "獲取了鎖");try {for (;;) {}} finally {lock.unlock();System.out.println(Thread.currentThread().getName() + "釋放了鎖");}} }?
運行結果:
Thread-0獲取了鎖
Thread-1被中斷
java.lang.InterruptedException...
運行結果:
2.3 ReadWriteLock
ReadWriteLock也是一個接口,在它里面只定義了兩個方法:
public interface ReadWriteLock {/*** Returns the lock used for reading.** @return the lock used for reading.*/Lock readLock();/*** Returns the lock used for writing.** @return the lock used for writing.*/Lock writeLock(); }一個用來獲取讀鎖,一個用來獲取寫鎖。
也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。
下面的ReentrantReadWriteLock實現(xiàn)了ReadWriteLock接口。
2.4?ReentrantReadWriteLock
里面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。
2.4.1?假如有多個線程要同時進行讀操作的話,先看一下synchronized達到的效果:
package Test1;import java.util.concurrent.locks.ReentrantReadWriteLock;public class Code1 {private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();public static void main(String[] args) {final Code1 test = new Code1();new Thread() {public void run() {test.get(Thread.currentThread());};}.start();new Thread() {public void run() {test.get(Thread.currentThread());};}.start();}public synchronized void get(Thread thread) {long start = System.currentTimeMillis();while (System.currentTimeMillis() - start <= 1) {System.out.println(thread.getName() + "正在進行讀操作");}System.out.println(thread.getName() + "讀操作完畢");} }運行結果:
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0讀操作完畢
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1讀操作完畢
直到thread1執(zhí)行完讀操作之后,才會打印thread2執(zhí)行讀操作的信息。
2.4.2?改成用讀寫鎖:
package Test1;import java.util.concurrent.locks.ReentrantReadWriteLock;public class Code1 {private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();public static void main(String[] args) {final Code1 test = new Code1();new Thread() {public void run() {test.get(Thread.currentThread());};}.start();new Thread() {public void run() {test.get(Thread.currentThread());};}.start();}//更改此處public void get(Thread thread) {rwl.readLock().lock();try {long start = System.currentTimeMillis();while (System.currentTimeMillis() - start <= 1) {System.out.println(thread.getName() + "正在進行讀操作");}System.out.println(thread.getName() + "讀操作完畢");} finally {rwl.readLock().unlock();}} }運行結果:
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-0正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1正在進行讀操作
Thread-1讀操作完畢
Thread-0讀操作完畢
thread1和thread2在同時進行讀操作,可大大提升了讀操作的效率。
如果有一個線程已經占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。
如果有一個線程已經占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
3. 比較
| JVM 實現(xiàn)的 | JDK 實現(xiàn)的 |
| 自動釋放 | 手動finally塊中,unlock() |
| 無法判斷是否獲取鎖的狀態(tài), | ? |
| 新版本 Java 對 synchronized 進行了很多優(yōu)化 例如自旋鎖等; 與ReentrantLock 大致相同。 | ? |
| 不行 | 可中斷 |
| 非公平的 | 也是非公平的,但是 也可以是公平的。 |
| ? | 可以同時綁定多個 Condition 對象 |
| 適合代碼少量的同步問題 | 大量同步的代碼的同步問題 |
等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改為處理其他事情。
公平鎖:公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。
參考:https://blog.csdn.net/qq_19734597/article/details/80874972
總結
以上是生活随笔為你收集整理的互斥同步(synchronized、Lock、ReentrantLock、ReadWriteLock、ReentrantReadWriteLock)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中断(interrupted()、isI
- 下一篇: AQS(CountdownLatch、C