锁是并发编程里保证数据线程安全的一个重要的工具。今天就来了解下,java里的锁。
如何实现一个高并发的计数器
我们首先来考虑一个问题,如何实现一个高并发的计数器。
稍微了解过java的人应该立马能想到"synchronized"关键字。但是事实上,在Java里可以通过多种方式来实现线程安全的计数器。
我们将会解锁以下几种锁,并详细的解锁每一种锁的机制。这篇文字会介绍synchronized
- synchronized
- Lock接口
- 原子操作类
- 加法器
synchronized
代码例子
1.synchronized可以直接修饰方法
public class Example {
private Integer count = 0;
private synchronized int add(){
return count++;
}
public static void start(){
Example obj = new Example();
for (int i = 0;i<10;i++){
new Thread(()->System.out.println(obj.add())).start();
}
}
}
2.也可以修饰代码块
public class Example {
private static Integer count = 0;
private int add(){
synchronized(count){
return count++;
}
}
public static void start(){
Example obj = new Example();
for (int i = 0;i<10;i++){
new Thread(()->System.out.println(obj.add())).start();
}
}
}
当中需要注意的几个点
1.不能锁定int等基础类型,只能锁定包装类,如Integer
2.如果被锁定的对象没有使用static锁定,那么多个线程必须使用同一个对象才会使用同一个锁。
synchronized的实现机制
那么synchronized是如何实现的?
通过对象头实现
查看下每个对象的对象头,就会发现,这里有很多与锁相关的标志位。
天呐,锁的标志居然不止一个,还有偏向锁,轻量级锁,重量级锁,这些到底是什么呢?
事实上,synchronized不仅只有一种状态,而是三种不同的锁的状态,当遇到不同的情况时,会将锁按照偏向锁,轻量级锁,重量级锁的顺序进行升级。
- 偏向锁:
偏向锁是为了提升锁的性能。第一次获得锁时,线程会把当前的线程id写入到对象头,这样子,同一个线程在下次重新获得锁时,只有比较线程id就可以,避免了许多锁的繁琐操作。偏向锁仅当发生竞争时才会进行锁的升级。 - 轻量级锁:
轻量级锁会使用CAS(稍后会讲)机制尝试更新轻量级锁标志,如果在超时时间内尝试失败的话,就会升级到重量级锁。 - 重量级锁:
一旦膨胀到重量级,那么所有发生竞争又没有获得锁的线程就会全部进入阻塞状态。线程状态的切换时非常昂贵的,所以重量级锁也是性能最差的。
CAS(Compare and swap)
接下来我们聊聊刚刚提到的CAS机制,CAS全称是Compare and swap,比较与替换。为了说明CAS,还是回到最初提出的问题如何实现一个高并发的计数器?
假设没有使用锁的情况下,可能会出现以下问题。
线程1:取出10,+1,设置为11。
线程2:和线程1同时取出10,+1,设置为11,然而这个时候,内存中的值可能已经被线程1设置为11了。
线程1和线程2,同时操作内存里的值,同时取出值10,再加一,然后再把数值11更新到内存里。最终的结果是11.这显然是错误的,两个线程分别加一,最终的结果应该是12才对。
问题的关键在于,线程2把11设置回内存时,并没有考虑,内存中的值是否已经被改变。
而CAS机制就是多加了比较内存原始值是否回被改变这一步。
线程1:取出10,+1,判断内存值是否为10,如果是则设置为11
线程2:取出也是10,+1,判断内存值是否为10,如果不是则设置失败
通过判断内存值是否改变,失败后可以通过重试等方式重新计算,保证了值不会计算错误。
锁的升级过程
在了解完CAS机制后,就可以大概画出一个锁的升级过程。如图
1.在代码访问同步块时,会判断锁的标志位,如果是偏向锁,那么会判断对象头是否已经存储了线程ID,如果没有存储线程ID,则使用CAS设置对象头的线程ID,这里使用CAS设置线程ID的原因是在设置线程ID的时候也有可能发生竞争,所以必须对象头的线程ID为空时才能设置成功。
2.如果已经存在对象头或者使用CAS设置线程ID失败,说明发生了竞争,这个时候就必须进行锁的升级,在升级之前会把偏向锁撤销(由于在全局安全点(所有线程都暂停时)时才撤销,所以这个时候不会有竞争),撤销锁时,会把锁的标志位置为轻量级锁,清除线程ID,并设置轻量级锁指针为获得锁的线程的相关指针。
3.撤销锁之后线程会进行轻量级锁的竞争,使用CAS机制更新轻量级锁指针,和之前的CAS机制不同的时,这里会设置一个超时时间,在超时时间内会不停的使用CAS机制进行更新,如果在超时时间内,轻量级锁被释放了,那么当前线程就会获得锁。而如果超时时间内没有获得锁,那么锁就会进一步膨胀成重量级锁。
4.当膨胀为重量级锁之后,所有竞争但没有获得锁的线程都会进入阻塞状态,直到锁被释放后去唤醒阻塞线程。
思考
最后思考一个问题:obj.wait(1000) 和 Thread.sleep(1000)的区别是什么?
答案是:wait方法来自于Object,在使用该方法时,会释放掉锁,而sleep方法来自于Thread类,并不会释放锁。而且要注意的是,wait(1000)等待一秒后,只是激活线程重新取争夺锁,如果这个时候没有争夺到锁,线程会和普通的线程一样继续休眠,所以并不能保证在1秒后会继续运行。