目录
前言
小伙伴们大家好,上次介绍了创建线程的四种方式,可以点击下方链接熟悉下,这次来分析线程使用中要考虑的线程安全问题
一、线程不安全案例
1、经典中的经典,抢票案例,如下图,创建一个SynTest类,定义了ticketNum代表票的数量,一个抢票的方法getTicketmain方法中循环创建了十五个线程模拟十五名用户在抢票,右键运行,看下结果,好家伙,问题大了,前三个线程买完票数量只减少一张,合着你们仨做一张座,后面更甚者,票数为零了还能抢到一张,站票是吧(三哥直呼内行)。开个玩笑,这种情况是肯定不允许的,也就是多线程情况下要保证线程的安全
public class SynTest {
int ticketNum = 10;
public void getTicket(){
if(ticketNum<=0){
System.out.println("线程"+Thread.currentThread().getName()+"抢票失败,剩余:"+ticketNum);
return;
}
System.out.println("线程"+Thread.currentThread().getName()+"抢到1张,剩余:"+ticketNum);
ticketNum--;
}
public static void main(String[] args) {
SynTest synTest = new SynTest();
for(int i = 0;i<15;i++){
new Thread(()->{
synTest.getTicket();
}).start();
}
}
}
2、解决方案
使用synchronized关键字,在 抢票方法中引用了同步锁,这样每个线程在调用方法执行完成前别的贤臣更不能调用,这样就保证了安全有序,结果如下
public class SynTest {
final static Object lock = new Object();
int ticketNum = 10;
public void getTicket(){
synchronized (lock){
if(ticketNum<=0){
System.out.println("线程"+Thread.currentThread().getName()+"抢票失败,剩余:"+ticketNum);
return;
}
System.out.println("线程"+Thread.currentThread().getName()+"抢到1张,剩余:"+ticketNum);
ticketNum--;
}
}
public static void main(String[] args) {
SynTest synTest = new SynTest();
for(int i = 0;i<15;i++){
new Thread(()->{
synTest.getTicket();
}).start();
}
}
}
二、synchronized关键字底层原理
那么,到底该关键字底层做了什么,就可以让线程安全有序执行呢,这就要到.class文件中一探究竟了,打开项目包下的编译后的输出目录target文件夹,找到.class所在位置,右键终端打开,然后输入命令“javap -v SynTest.class” 查看字节码文件,如下找到抢票方法的内部:
注意“monitorenter"字符,是JVM提供的Monitor监视器提供
- monitorenter 是上锁开始的地方(拆开就是 monitor enter)
- monitorexit 解锁的地方(monitor exit)
- 这两个代码包住的指令就是上锁的代码
- 仔细往下看的话,会发现有两个monitorexit指令,防止锁住的代码抛异常后不能及时释放锁
三、Monitor结构
1.存储结构
- Owner: 存储当前获取锁的线程的标识,只有一个线程可以获取
- EntryList: 关联没有抢到锁的线程,也就是处于Blocked状态的线程
- WaitSet: 关联调用了wait方法的线程,也就是处于waiting状态的线程
2.具体流程
- 代码进入synchronized代码块,先对lock(同步锁)关联monitor,然后判断Owner是否有线程持有
- 若当前没有线程持有,则当前线程持有,标识该线程获取锁成功
- 如果当前已持有,则让线程进入entryList阻塞,如果持有锁的线程已经释放锁,则在entryList中的线程开始竞争锁的持有权(该过程属于非公平竞争)
- 如果代码块中调用了wait(),则线程进入waitSet中等待
四、原理总结
synchronized同步锁采用互斥的方式让同一时刻至多一个线程持有同步锁,底层是由JVM级别的monitor对象实现,线程获得锁需要关联monitor,其中,monitor内部有三个属性,owner:是关联获得锁的线程,entryList:是出于阻塞状态的线程,waitSet:关联的是处于waiting 状态的线程
拓展
monitor实现的锁属于重量级锁,因为涉及到了用户状态的切换,进程的上下文切换,性能较低
因此在JDK1.6之后引入了偏向锁,轻量级锁这两种新型锁机制,下次来分析分析这两个机制的原理以及使用场景
最后
对于synchronized的使用以及底层原理到这里就结束了,欢迎一起讨论