Lock和synchronized
- 鎖是一種工具,用于控制對共享資源的訪問
- Lock和synchronized,這兩個是最創(chuàng)建的鎖,他們都可以達(dá)到線程安全的目的,但是使用和功能上有較大不同
- Lock不是完全替代synchronized的,而是當(dāng)使用synchronized不合適或不足以滿足要求的時候,提供高級功能
- Lock 最常見的是ReentrantLock實現(xiàn)
為啥需要Lock
- syn效率低:鎖的釋放情況少,試圖獲得鎖時不能設(shè)定超時,不能中斷一個正在試圖獲得鎖的線程
- 不夠靈活,加鎖和釋放的時機單一,每個鎖僅有一個單一的條件(某個對象),可能是不夠的
- 無法知道是否成功獲取到鎖
主要方法
Lock();
最普通的獲取鎖,最佳實踐是finally中釋放鎖,保證發(fā)生異常的時候鎖一定被釋放
/**
* 描述:Lock不會像syn一樣,異常的時候自動釋放鎖
* 所以最佳實踐是finally中釋放鎖,保證發(fā)生異常的時候鎖一定被釋放
*/
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
//獲取本鎖保護的資源
System.out.println(Thread.currentThread().getName() + "開始執(zhí)行任務(wù)");
} finally {
lock.unlock();
}
}
tryLock(long time,TimeUnit unit);超時就放棄
用來獲取鎖,如果當(dāng)前鎖沒有被其它線程占用,則獲取成功,則返回true,否則返回false,代表獲取鎖失敗
/**
* 描述:用TryLock避免死鎖
*/
static class TryLockDeadlock implements Runnable {
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("線程1獲取到了鎖1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("線程1獲取到了鎖2");
System.out.println("線程1成功獲取到了2把鎖");
break;
}finally {
lock2.unlock();
}
}else{
System.out.println("線程1獲取鎖2失敗,已重試");
}
} finally {
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("線程1獲取鎖1失敗,已重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 0) {
try {
if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
try {
System.out.println("線程2獲取到了鎖2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("線程2獲取到了鎖1");
System.out.println("線程2成功獲取到了2把鎖");
break;
}finally {
lock1.unlock();
}
}else{
System.out.println("線程2獲取鎖1失敗,已重試");
}
} finally {
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("線程2獲取鎖2失敗,已經(jīng)重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
TryLockDeadlock r1 = new TryLockDeadlock();
TryLockDeadlock r2 = new TryLockDeadlock();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
}
執(zhí)行結(jié)果:
線程1獲取到了鎖1
線程2獲取到了鎖2
線程1獲取鎖2失敗,已重試
線程2獲取到了鎖1
線程2成功獲取到了2把鎖
線程1獲取到了鎖1
線程1獲取到了鎖2
線程1成功獲取到了2把鎖
lockInterruptibly(); 中斷
相當(dāng)于tryLock(long time,TimeUnit unit) 把超時時間設(shè)置為無限,在等待鎖的過程中,線程可以被中斷
/**
* 描述:獲取鎖的過程中,中斷了
*/
static class LockInterruptibly implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "嘗試獲取鎖");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + "獲取到了鎖");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "睡眠中被中斷了");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "釋放了鎖");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "等鎖期間被中斷了");
}
}
public static void main(String[] args) {
LockInterruptibly lockInterruptibly = new LockInterruptibly();
Thread thread0 = new Thread(lockInterruptibly);
Thread thread1 = new Thread(lockInterruptibly);
thread0.start();
thread1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread0.interrupt();
}
}
執(zhí)行結(jié)果:
Thread-0嘗試獲取鎖
Thread-1嘗試獲取鎖
Thread-0獲取到了鎖
Thread-0睡眠中被中斷了
Thread-0釋放了鎖
Thread-1獲取到了鎖
Thread-1釋放了鎖
Java鎖分類:
樂觀鎖和悲觀鎖:
樂觀鎖:
比較樂觀,認(rèn)為自己在處理操作的時候,不會有其它線程來干擾,所以并不會鎖住操作對象
- 在更新的時候,去對比我修改期間的數(shù)據(jù)有沒有被改變過,如沒有,就正常的修改數(shù)據(jù)
- 如果數(shù)據(jù)和我一開始拿到的不一樣了,說明其他人在這段時間內(nèi)改過,會選擇放棄,報錯,重試等策略
- 樂觀鎖的實現(xiàn)一般都是利用CAS算法來實現(xiàn)的
劣勢:
可能造成ABA問題,就是不知道是不是修改過
使用場景:
適合并發(fā)寫入少的情況,大部分是讀取的場景,不加鎖的能讓讀取的性能大幅提高
悲觀鎖:
比較悲觀,認(rèn)為如果我不鎖住這個資源,別人就會來爭搶,就會造成數(shù)據(jù)結(jié)果錯誤,所以它會鎖住操作對象,Java中悲觀鎖的實現(xiàn)就是syn和Lock相關(guān)類
劣勢:
- 阻塞和喚醒帶來的性能劣勢
- 如果持有鎖的線程被永久阻塞,比如遇到了無限循環(huán),死鎖等活躍性問題,那么等待該線程釋放鎖的那幾個線程,永遠(yuǎn)也得不到執(zhí)行
- 優(yōu)先級反轉(zhuǎn),優(yōu)先級低的線程拿到鎖不釋放或釋放的比較慢,就會造成這個問題
使用場景:
適合并發(fā)寫入多的情況,適用于臨界區(qū)持鎖時間比較長的情況:
- 臨界區(qū)有IO操作
- 臨界區(qū)代碼復(fù)雜或者循環(huán)量大
- 臨界區(qū)競爭非常激烈
可重入鎖:
可重入就是說某個線程已經(jīng)獲得某個鎖,可以再次獲取鎖而不會出現(xiàn)死鎖
ReentrantLock 和 synchronized 都是可重入鎖
// 遞歸調(diào)用演示可重入鎖
static class RecursionDemo{
public static ReentrantLock lock = new ReentrantLock();
private static void accessResource(){
lock.lock();
try {
System.out.println("已經(jīng)對資源處理了");
if (lock.getHoldCount() < 5){
System.out.println("已經(jīng)處理了"+lock.getHoldCount()+"次");
accessResource();
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
new RecursionDemo().accessResource();
}
}
執(zhí)行結(jié)果:
已經(jīng)對資源處理了
已經(jīng)處理了1次
已經(jīng)對資源處理了
已經(jīng)處理了2次
已經(jīng)對資源處理了
已經(jīng)處理了3次
已經(jīng)對資源處理了
已經(jīng)處理了4次
已經(jīng)對資源處理了
ReentrantLock的其它方法
- isHeldByCurrentThread 可以看出鎖是否被當(dāng)前線程持有
- getQueueLength()可以返回當(dāng)前正在等待這把鎖的隊列有多長,一般這兩個方法是開發(fā)和調(diào)試時候使用,上線后用到的不多
公平鎖和非公平鎖
- 公平指的是按照線程請求的順序,來分配鎖;
- 非公平指的是,不完全按照請求的順序,在一定情況下,可以插隊
- 非公平鎖可以避免喚醒帶來的空檔期
/**
* 描述:演示公平鎖和非公平鎖
*/
class FairLock{
public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread[] thread = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue));
}
for (int i = 0; i < 5; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Job implements Runnable{
PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"開始打印");
printQueue.printJob(new Object());
System.out.println(Thread.currentThread().getName()+"打印完成");
}
}
class PrintQueue{
// true 公平,false是非公平
private Lock queueLock = new ReentrantLock(true);
public void printJob(Object document){
queueLock.lock();
try {
int duration = new Random().nextInt(10)+1;
System.out.println(Thread.currentThread().getName()+"正在打印,需要"+duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
queueLock.lock();
try {
int duration = new Random().nextInt(10)+1;
System.out.println(Thread.currentThread().getName()+"正在打印,需要"+duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
執(zhí)行結(jié)果:
Thread-0開始打印
Thread-0正在打印,需要10秒
Thread-1開始打印
Thread-2開始打印
Thread-3開始打印
Thread-4開始打印
Thread-1正在打印,需要2秒
Thread-2正在打印,需要2秒
Thread-3正在打印,需要2秒
Thread-4正在打印,需要4秒
Thread-0正在打印,需要2秒
Thread-0打印完成
Thread-1正在打印,需要7秒
Thread-1打印完成
Thread-2正在打印,需要8秒
Thread-2打印完成
Thread-3正在打印,需要3秒
Thread-3打印完成
Thread-4正在打印,需要8秒
Thread-4打印完成
true改為false演示非公平鎖:
Lock queueLock = new ReentrantLock(false);
執(zhí)行結(jié)果:
Thread-0正在打印,需要7秒
Thread-1開始打印
Thread-2開始打印
Thread-3開始打印
Thread-4開始打印
Thread-0正在打印,需要9秒
Thread-0打印完成
Thread-1正在打印,需要3秒
Thread-1正在打印,需要2秒
Thread-1打印完成
Thread-2正在打印,需要4秒
Thread-2正在打印,需要7秒
Thread-2打印完成
Thread-3正在打印,需要10秒
Thread-3正在打印,需要2秒
Thread-3打印完成
Thread-4正在打印,需要7秒
Thread-4正在打印,需要8秒
Thread-4打印完成
共享鎖和排它鎖:
- 排它鎖,又稱為獨占鎖,獨享鎖
- 共享鎖,又稱為讀鎖,獲得共享鎖之后,可以查看但無法修改和刪除數(shù)據(jù),其他線程此時也可以獲取到共享鎖,也可以查看但無法修改和刪除數(shù)據(jù)
- 共享鎖和排它鎖的典型是讀寫鎖 ReentrantReadWriteLock,其中讀鎖是共享鎖,寫鎖是獨享鎖
讀寫鎖的作用:
- 在沒有讀寫鎖之前,我們假設(shè)使用ReentrantLock,那么雖然我們保證了線程安全,但是也浪費了一定的資源:
Thrad4釋放了寫鎖多個讀操作同時進(jìn)行,并沒有線程安全問題/** * 描述:演示可以多個一起讀,只能一個寫 */ class CinemaReadWrite{ private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); private static void read(){ readLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了讀鎖,正在讀取"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + "釋放了讀鎖"); readLock.unlock(); } } private static void write(){ writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了寫鎖,正在寫入"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + "釋放了寫鎖"); writeLock.unlock(); } } public static void main(String[] args) { new Thread(()-> read(),"Thrad1").start(); new Thread(()-> read(),"Thrad2").start(); new Thread(()-> write(),"Thrad3").start(); new Thread(()-> write(),"Thrad4").start(); } } 執(zhí)行結(jié)果: Thrad1得到了讀鎖,正在讀取 Thrad2得到了讀鎖,正在讀取 Thrad2釋放了讀鎖 Thrad1釋放了讀鎖 Thrad3得到了寫鎖,正在寫入 Thrad3釋放了寫鎖 Thrad4得到了寫鎖,正在寫入
- 在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是無阻塞的,提高了程序的執(zhí)行效率
讀寫鎖的規(guī)則:
- 多個線程值申請讀鎖,都可以申請到
- 要么一個或多個一起讀,要么一個寫,兩者不會同時申請到,只能存在一個寫鎖
讀鎖和寫鎖的交互方式:
讀鎖插隊策略:
- 公平鎖:不允許插隊
- 非公平鎖:寫鎖可以隨時插隊,讀鎖僅在等待隊列頭節(jié)點不是想獲取寫鎖線程的時候可以插隊
自旋鎖和阻塞鎖
- 讓當(dāng)前線程進(jìn)行自旋,如果自旋完成后前面鎖定同步資源的線程已經(jīng)釋放了鎖,那么當(dāng)前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷。這就是自旋鎖。
- 阻塞鎖和自旋鎖相反,阻塞鎖如果遇到?jīng)]拿到鎖的情況,會直接把線程阻塞,知道被喚醒
自旋缺點:
- 如果鎖被占用的時間很長,那么自旋的線程只會白浪費處理器資源
- 在自旋的過程中,一直消耗cpu,所以雖然自旋鎖的起始開銷低于悲觀鎖,但是隨著自旋的時間增長,開銷也是線性增長的
原理:
- 在Java1.5版本及以上的并發(fā)框架java.util.concurrent 的atmoic包下的類基本都是自旋鎖的實現(xiàn)
- AtomicInteger的實現(xiàn):自旋鎖的實現(xiàn)原理是CAS,AtomicInteger中調(diào)用unsafe 進(jìn)行自增操作的源碼中的do-while循環(huán)就是一個自旋操作,如果修改過程中遇到其他線程競爭導(dǎo)致沒修改成功,就在while里死循環(huán)直至修改成功
/**
* 描述:自旋鎖演示
*/
class SpinLock{
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock(){
Thread currentThread = Thread.currentThread();
while (!sign.compareAndSet(null,currentThread)){
System.out.println("自旋獲取失敗,再次嘗試");
}
}
public void unLock(){
Thread currentThread = Thread.currentThread();
sign.compareAndSet(currentThread,null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable(){
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"開始嘗試自旋鎖");
spinLock.lock();
System.out.println(Thread.currentThread().getName()+"獲取到了自旋鎖");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
spinLock.unLock();
System.out.println(Thread.currentThread().getName()+"釋放了自旋鎖");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
執(zhí)行結(jié)果:
Thread-0開始嘗試自旋鎖
Thread-0獲取到了自旋鎖
Thread-1開始嘗試自旋鎖
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
自旋獲取失敗,再次嘗試
Thread-0釋放了自旋鎖
Thread-1獲取到了自旋鎖
Thread-1釋放了自旋鎖
使用場景:
- 自旋鎖一般用于多核服務(wù)器,在并發(fā)度不是特別高的情況下,比阻塞鎖的效率要高
- 另外,自旋鎖適用于臨界區(qū)比較短小的情況,否則如果臨界區(qū)很大(線程一旦拿到鎖,很久之后才會釋放),那也是不合適的
總結(jié)
以上就是關(guān)于 Java 中鎖的分類以及具體使用的全部內(nèi)容,想要了解更多相關(guān) Java 中鎖的其他使用內(nèi)容請搜索W3Cschool以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持我們!