共计 6630 个字符,预计需要花费 17 分钟才能阅读完成。
上一篇文章,咱们相熟了 Java 锁的分类。明天,来学习下 Java 中罕用的乐观锁 synchronized 和 ReetrantLock 吧。学习使我高兴,哦耶!
synchronized
synchronized 是什么?
synchronized 关键字能够保障,一段时间内共享资源只能被一个线程所应用,或者说一段代码一段时间内只能被一个线程执行,并且共享资源对其余线程是可见的。
实际上,synchronized 就是,某个线程拿到一个锁,锁住共享资源,当应用完,放开锁,让其余线程申请锁并应用共享资源。
synchronized 锁的级别
synchronized 作用在一般办法或者代码片段上时,锁为对 象自身 。作用在 static 办法或者代码片段上时,锁为 类自身。
synchronized 的根本应用
咱们构想一个卖票场景,有 A、B 两个售票窗口卖票,票池 (共享资源) 只有一个。
试验 1
public class SellTicketRunnable implements Runnable {
// 残余票数
static int ticket = 1000;
@Override
public void run() {for (int i=0;i<550;i++){sell();
}
}
// 买票操作
private synchronized void sell() {System.out.println(Thread.currentThread().getName()+"开始卖票");
try {
// 模仿卖票
if (ticket <= 0){System.out.println(Thread.currentThread().getName()+"窗口告诉,票卖完了~");
}else {Thread.sleep(5);
ticket--;
System.out.println(Thread.currentThread().getName()+"出票胜利,当初还有"+ticket+"张票");
}
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"完结卖票");
}
}
public static void main(String[] args) throws InterruptedException {
// 代码示例 1 根本应用
SellTicketRunnable sellTicketRunnable = new SellTicketRunnable();
Thread thread_1 = new Thread(sellTicketRunnable,"A 窗口");
Thread thread_2 = new Thread(sellTicketRunnable,"B 窗口");
thread_1.start();
thread_2.start();
thread_1.join();
thread_2.join();
System.out.println("运行完结,剩下"+SellTicketRunnable.ticket+"张票");
}
首先创立售票类 SellTicketRunnable
,定义布告资源ticket
为 1000 张票,咱们每个窗口模仿卖票 550 张,如果发现票卖完了,就零碎提醒,否则票数减 1。main
办法开启两个线程,发现完满运行。发现票数最终为 0,并且每个线程访问共享资源的工夫内都是独享的。
A 窗口开始卖票
A 窗口窗口告诉,票卖完了~
A 窗口完结卖票
B 窗口开始卖票
B 窗口窗口告诉,票卖完了~
B 窗口完结卖票
运行完结,剩下 0 张票
synchronized 对象级别的锁
方才只生成了一个 SellTicketRunnable
,只有一把锁。那咱们生成两个SellTicketRunnable
对象,会不会有两把锁呢?
试验 2
SellTicketRunnable sellTicketRunnable = new SellTicketRunnable();
SellTicketRunnable sellTicketRunnable_backups = new SellTicketRunnable();
Thread thread_1 = new Thread(sellTicketRunnable,"A 窗口");
Thread thread_2 = new Thread(sellTicketRunnable_backups,"B 窗口");
thread_1.start();
thread_2.start();
thread_1.join();
thread_2.join();
System.out.println("运行完结,剩下"+SellTicketRunnable.ticket+"张票");
// 运行后果如下:A 窗口开始卖票
B 窗口开始卖票
A 窗口出票胜利,当初还有 999 张票
A 窗口完结卖票
A 窗口开始卖票
B 窗口出票胜利,当初还有 998 张票
B 窗口完结卖票
B 窗口开始卖票
A 窗口出票胜利,当初还有 997 张票
A 窗口完结卖票
...
运行完结,剩下 - 1 张票
将 main
办法改成上边所示。首先,访问共享资源的工夫不再独享。A 窗口还没拜访完数据库呢,B 窗口就去拜访了。这最终导致票可能超卖。(就剩 1 张票了,A、B 窗口同时卖出,同时更新共享资源)。当然这段代码你多运行几次才会呈现残余 -1
的状况,有时候可能为 0
,毕竟那么巧的事,不是每次都遇到哈。阐明, 此时锁是对象级别的。
实际上,如果 synchronized
作用在对象级别上。内存中,对象的对象头会记录以后获取锁的线程,利用的是 Monitor
机制。
synchronized 类级别的锁
试验 3
public class SellTicketRunnablePlus implements Runnable {
// 残余票数
static int ticket = 1000;
@Override
public void run() {for (int i=0;i<550;i++){sell();
}
}
public void sell() {synchronized(SellTicketRunnablePlus.class){System.out.println(Thread.currentThread().getName()+"开始卖票");
try {
// 模仿卖票
if (ticket <= 0){System.out.println(Thread.currentThread().getName()+"窗口告诉,票卖完了~");
}else {Thread.sleep(5);
ticket--;
System.out.println(Thread.currentThread().getName()+"出票胜利,当初还有"+ticket+"张票");
}
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"完结卖票");
}
}
}
// 运行后果
A 窗口开始卖票
A 窗口窗口告诉,票卖完了~
A 窗口完结卖票
B 窗口开始卖票
B 窗口窗口告诉,票卖完了~
B 窗口完结卖票
运行完结,剩下 0 张票
将 试验 2 主函数中的 SellTicketRunnable
类换成 SellTicketRunnablePlus
。SellTicketRunnablePlus
只是给 sell
办法外部,synchronized
锁的是 SellTicketRunnablePlus
类。此时又是一把锁了,所以两个窗口又能够某段时间内独享共享资源了。
synchronized 是重入锁
synchronized 能够保障 不同 线程同一时间只能有有一个独享共享资源,比如说线程 1 持有了锁,线程 2 去申请锁的时候,发现线程 1 持有锁呢,所以线程 2 须要等会 (线程阻塞)。那么线程 1 在持有锁的状况下,能够再申请一把同样的锁吗?
试验 4
public class ReentryTest {public synchronized void outMethod(){innerMethod();
System.out.println("这是内部办法,执行了");
}
private synchronized void innerMethod(){System.out.println("这是外部办法,执行了");
}
}
// main 办法
ReentryTest reentryTest = new ReentryTest();
reentryTest.outMethod();
// 运行后果
这是外部办法,执行了
这是内部办法,执行了
当线程 1 执行 outMethod
办法时,取得了锁。outMethod
调用 innerMethod
办法时,线程 1 又去申请了同一把锁,发现申请胜利了。可重入锁
是指同一个线程能够屡次加同一把锁。
自 JDK1.6 开始,当只有两个线程竞争锁时,synchronized
是轻量级锁,超过两个线程竞争的时候是重量级锁。对于锁的分类,请戳链接: Java 锁分类原来是这个样子。
ReetrantLock
synchronized
是关键字,很多操作都是隐式的,比如说 开释锁
、 自旋次数
等,都是虚拟机帮你搞定的。为了显示操作,并且领有更弱小的性能,ReetrantLock
来了。
ReetrantLock 根本应用
试验 5
ReentrantLock lock = new ReentrantLock();
lock.lock();
try{// 业务逻辑}catch (Exception e){ }finally {lock.unlock();
}
ReetrantLock 须要手动申请锁和开释锁,别离为办法 lock
和unlock
。
ReetrantLock 重入性
和 synchronized
一样,ReetrantLock
也具备重入性。
试验 6
ReentrantLock lock = new ReentrantLock();
int count = 0;
for (int i = 1; i <= 3; i++) {lock.lock();
System.out.println("阐明获取锁"+ ++count +"次");
}
for (int i = 1; i <= 3; i++) {lock.unlock();
}
//
阐明获取锁 1 次
阐明获取锁 2 次
阐明获取锁 3 次
偏心锁和非偏心锁
ReetrantLock
能够申请偏心锁或者非偏心锁(理解锁的分类:Java 锁分类原来是这个样子)。
首先咱们补充一个知识点,ReetrantLock
是实现 AQS
机制的,就是说所有申请锁的线程,会被按需放到一个队列中,而后顺次获取锁。偏心锁
保障了,获取锁的程序性。
试验 7
// 主函数
ReentrantLock lock = new ReentrantLock(true);
for (int i=1;i<=5;i++){new Thread(new FairLockThread(lock),"第"+i+"个").start();}
// FairLockThread 类
public class FairLockThread implements Runnable {
ReentrantLock lock;
public FairLockThread(ReentrantLock lock) {this.lock = lock;}
@Override
public void run() {for (int i = 1; i <= 2; i++) {lock.lock();
try {Thread.sleep(500);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "开始执行了");
System.out.println(Thread.currentThread().getName() + ":" + lock.getQueueLength());
lock.unlock();}
}
}
// 后果
第 1 个开始执行了
第 1 个:4
第 2 个开始执行了
第 2 个:4
第 3 个开始执行了
第 3 个:4
第 4 个开始执行了
第 4 个:4
第 5 个开始执行了
第 5 个:4
第 1 个开始执行了
第 1 个:4
第 2 个开始执行了
第 2 个:3
第 3 个开始执行了
第 3 个:2
第 4 个开始执行了
第 4 个:1
第 5 个开始执行了
第 5 个:0
ReentrantLock
new 的时候传入 true
,就是申请了一把偏心锁。FairLockThread
办法外面让一个线程执行两次申请锁、开释锁操作,并且模仿应用锁 0.5 秒。getQueueLength
办法就是查看,以后队列中阻塞的线程数。能够看出,锁的两遍申请是依照程序的,从 1~5。从线程是也能够看出,没有哪个线程能够偷偷的本人两边都执行完。
还是试验 7
ReentrantLock lock = new ReentrantLock(false);
// 后果
第 2 个开始执行了
第 2 个:4
第 2 个开始执行了
第 2 个:4
第 1 个开始执行了
第 1 个:3
第 1 个开始执行了
第 1 个:3
第 3 个开始执行了
第 3 个:2
第 4 个开始执行了
第 4 个:2
第 4 个开始执行了
第 4 个:2
第 5 个开始执行了
第 5 个:1
第 5 个开始执行了
第 5 个:1
第 3 个开始执行了
第 3 个:0
咱们只须要将主函数,newReentrantLock
的时候设置成 false,此时申请的就是非偏心锁了。再看运行后果,某个线程执行完第一遍,很大概率 上就会执行第二遍。没有依照程序执行,这是不偏心的。
执行完一遍,而后紧接着执行第二遍,不必切换上下文,某线程统一应用 CPU,这样效率更快的,所以 非偏心锁效率更高。
ReetrantLock 可中断,预防死锁问题
试想一下,如果线程 1 曾经持有锁 1,当初想拿锁 2,而后就能够开心的完结了。线程 2 曾经持有锁 2,当初想拿锁 1,而后就能够开心的完结了。这俩线程还欢快的碰面了,后果谁都不撒手,谁都不能欢快的完结,于是乎,死锁就产生了。
试验 8
public class InterruptThread implements Runnable{
ReentrantLock firstLock;
ReentrantLock secondLock;
public InterruptThread(ReentrantLock firstLock, ReentrantLock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {firstLock.lock();
Thread.sleep(1000);
secondLock.lock();} catch (InterruptedException e) {e.printStackTrace();
} finally {firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"失常完结!");
}
}
}
// 主函数
ReentrantLock lock = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
Thread a = new Thread(new InterruptThread(lock, lock2), "A");
Thread b = new Thread(new InterruptThread(lock2, lock), "B");
a.start();
b.start();
// 后果
没有后果...
以上,运行到电脑死机也不会完结了。如果咱们在主函数最初一行前面加上一行
a.interrupt();
运行后果,放个图吧。
能够看出,尽管 A 就义掉了,然而因为 A 的中断(放弃持有锁 1)。B 顺利完成了!为小 A 默哀一分钟。。。
雷同与不同
雷同
synchronized
和ReetrantLock
都是乐观锁、可重入锁。
不同
synchronized
是隐士申请、开释锁,虚拟机层面保护。ReetrantLock
是显示操作,代码保护。- 在 JDK1.6 之前,
synchronized
性能极差,1.6 之后,它俩性能差不多。 ReetrantLock
可中断,防止死锁产生。ReetrantLock
能够申请偏心锁
或者非偏心锁
,可依据需要定制。
呜呼,从摸索到验证,辣条君用了一天,小伙伴们点个赞再走吧。