基本使用
如下抢票的代码,如果不加锁,就会出现超卖或者一张票卖给多个人
Synchronized
【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有
【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住
public class TicketDemo {
static Object lock = new Object();
int ticketNum = 10;
public synchronized void getTicket() {
synchronized (this) {
if (ticketNum <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() +
"抢到一张票,剩余:" + ticketNum);
// 非原子性操作
ticketNum--;
}
}
public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
ticketDemo.getTicket();
}).start();
}
}
}
Monitor
Monitor
被翻译为监视器,是由
jvm
提供,
c++
语言实现
在代码中想要体现
monitor
需要借助
javap
命令查看
clsss
的字节码,比如以下代
码
public class SyncTest {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
找到这个类的
class
文件,在
class
文件目录下执行
javap
-
v SyncTest.class
,反
编译效果如下
monitorenter
上锁开始的地方
monitorexit
解锁的地方
其中被
monitorenter
和
monitorexit
包围住的指令就是上锁的代码
有两个
monitorexit
的原因,第二个
monitorexit
是为了防止锁住的代码抛异常后不
能及时释放锁
在使用了
synchornized
代码块时需要指定一个对象,所以
synchornized
也被称为
对象锁
monitor
主要就是跟这个对象产生关联,如下图
Monitor
内部具体的存储结构:
Owner
:存储当前获取锁的线程的,只能有一个线程可以获取
EntryList
:关联没有抢到锁的线程,处于
Blocked
状态的线程
WaitSet
:关联调用了
wait
方法的线程,处于
Waiting
状态的线程
具体的流程:
代码进入
synchorized
代码块,先让
lock
(对象锁)关联的
monitor
,然后判断
Owner
是否有线程持有
如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
如果有线程持有,则让当前线程进入
entryList
进行阻塞,如果
Owner
持有的线
程已经释放了锁,在
EntryList
中的线程去竞争锁的持有权(非公平)
如果代码块中调用了
wait()
方法,则会进去
WaitSet
中进行等待
总结
synchronized底层使用的JVM级别中的Monitor来决定当前线程是否获取了锁,如果某一个线程获取到锁,在没有释放锁之前,其他线程是不能或得到锁的。synchronized属于悲观锁。
synchronized因为需要依赖于JVM级别的Monitor,相对性能也会比较低。
关于Monitor,Monitor对象存在于每个java对象头中,synchronized锁便是通过这种方式获取锁的,这也是为什么java中任意对象都可以作为锁的原因。
Monitor内部中维护了三个变量
WaitSet
:保存处于
Waiting
状态的线程
EntryList
:保存处于
Blocked
状态的线程
Owner
:持有锁的线程
只有一个线程获取到的标志就是在
monitor
中设置成功了
Owner
,一个
monitor
中只能有一个
Owner
在上锁的过程中,如果有其他线程也来抢锁,则进入
EntryList
进行阻塞,当
获得锁的线程执行完了,释放了锁,就会唤醒
EntryList
中等待的线程竞争
锁,竞争的时候是非公平的。