Java 线程同步器:从 synchronized、Lock 到 StampedLock
synchronized 和 lock 的对比
synchronized 关键字和 juc 中的 Lock 可以说是 Java 最为常用的线程同步器了,以下我们从多个角度来比较这两者的区别;
首先我们先看一个实际例子中的两者代码的区别:
一个线程共享对象 TimerBean
public class TimerBean {
private int num = 10;
//读资源
public int getNum() {
//模拟读取等待
try {
Thread.sleep(new Random().nextInt(1000) + 800);
} catch (InterruptedException e) {
e.printStackTrace();
}
return num;
}
//写资源
public void setNum(int num) {
//模拟写入等待
try {
Thread.sleep(new Random().nextInt(1000) + 800);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.num = num;
}
}
synchronized 测试类 SynchronziedTest
public class SynchronziedTest {
final Object monitor = new Object(); //监视器对象
public SynchronziedTest() throws InterruptedException {
TimerBean bean = new TimerBean();
CountDownLatch countDownLatch = new CountDownLatch(10);
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 1; i <= 10; i++){
exec.execute(new Task(bean,countDownLatch));
}
countDownLatch.await();
System.out.println(bean.getNum());
exec.shutdown();
}
public static void main(String[] args) throws InterruptedException {
new SynchronziedTest();
}
class Task extends Thread{
private TimerBean bean;
private CountDownLatch countDownLatch;
public Task(TimerBean bean, CountDownLatch countDownLatch) {
this.bean = bean;
this.countDownLatch = countDownLatch;
}
//通过 synzhronized 对监视器对象加锁
public void run() {
synchronized (monitor){
int initNum = bean.getNum();
bean.setNum(initNum - 1);
}
countDownLatch.countDown();
}
}
Lock 测试类 LockTest
public class LockTest {
ReentrantLock lock = new ReentrantLock();
public LockTest() throws InterruptedException {
TimerBean bean = new TimerBean();
CountDownLatch countDownLatch = new CountDownLatch(10);
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 1; i <= 10; i++){
exec.execute(new Task(bean,countDownLatch));
}
countDownLatch.await();
System.out.println(bean.getNum());
exec.shutdown();
}
public static void main(String[] args) throws InterruptedException {
new LockTest();
}
class Task extends Thread{
private TimerBean bean;
private CountDownLatch countDownLatch;
public Task(TimerBean bean, CountDownLatch countDownLatch) {
this.bean = bean;
this.countDownLatch = countDownLatch;
}
//通过 Lock 加线程锁
public void run() {
lock.lock();
int initNum = bean.getNum();
bean.setNum(initNum - 1);
lock.unlock();
countDownLatch.countDown();
}
}
}
1)存在层面
- synchronized 是 Java 内置的关键字,存在于 JVM 层面上;
- Lock 是 Java1.5 引入的 juc 包中的基础同步器,具体的实现类为 ReentrantLock;
2)锁的获取方式
- synchronized 只能阻塞式获取锁,当线程A获取锁,B线程只能等待,假如此时线程A阻塞,B线程会被一直阻塞;
- Lock 有多种获取锁的方式:
> Lock # lock 阻塞式锁获取,同 synchronized
> Lock # tryLock 尝试式锁获取,当当前线程获取不到锁,会立即返回,可以设置一个尝试持续时间;
>Lock # lock.lockInterruptibly 可中断式锁获取,当当前线程获取到锁后,可以被中断;
3)锁状态是否可判断
- synchronized 无法判断锁的状态;
- Lock 提供了当前线程对于所获取状态的API;
4)锁状态的控制
- synchronzied 通过锁对于线程的控制,是通过对加锁的监视器对象的行为来控制的;
- Lock 通过锁对线程行为的控制,是通过对 Lock 的 Condition 对象来控制的;
可以比较以下2个仿真延时多线程输入开关的例子:
//使用 synchronized 监视器控制线程行为
public class SyncLight {
private boolean theSwitch = false;
public void turnOn() throws InterruptedException{
synchronized(this){
while(theSwitch){
this.wait();
}
theSwitch = true;
System.out.println("SyncLight turn on ");
this.notifyAll();
}
}
public void turnOff() throws InterruptedException{
synchronized(this){
while(!theSwitch){
this.wait();
}
theSwitch = false;
System.out.println("SyncLight turn off ");
this.notify();
}
}
}
//使用 Lock Condition 控制线程行为
public class LockLight {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean theSwitch = false;
public void turnOn() throws InterruptedException {
lock.lock();
while(theSwitch){
condition.await();
}
TimeUnit.SECONDS.sleep(2);
theSwitch = true;
condition.signalAll();
System.out.println("LockLight turn on");
lock.unlock();
}
public void turnOff() throws InterruptedException {
lock.lock();
while(!theSwitch){
condition.await();
}
theSwitch = false;
condition.signalAll();
System.out.println("LockLight turn off");
lock.unlock();
}
}
从锁的类型对比 synchronized 和 lock
关于锁的类型参考:https://blog.csdn.net/a314773862/article/details/54095819
1)可重入锁
synchronized 和 Lock 都是可重入锁,可重入锁的分配是基于线程的分配,如下示例,当一个线程执行到 method1,method1 调用 method2 ,此时线程不需要重新申请监视器锁,可以直接执行方法method2;
class Demo {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
2)可中断锁
synchronized 是不可中断锁,Lock 是可中断锁(通过lockInterruptibly获取可中断锁);
如果使用 synshronized 监视器锁,当A获取改监视器锁,并执行锁中的代码,而线程B在等待获取该监视器锁,在这种情况下,是无法让B线程中断自己或者在别的线程中中断B线程;而使用 Lock # lockInterruptibly 获取到的可中断锁可以实现这一点;
3)公平锁
synchronized 是非公平锁,当多个线程在等待同一个锁,无法保证线程获取锁的顺序是按照请求锁的顺序来的;
Lock 的实现类 ReentrantLock 定义了2个静态内部类 NotFairSync,FairSync,分别用于实现非公平锁、公平锁,在默认情况下也是非公平锁,可以直接通过构建函数的参数来设置具体使用为 NotFairSync 还是 FairSync;
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
4)读写锁
Lock 的具有一个读写锁实现 ReentrantReadWriteLock,获取读锁或写锁,读锁和读锁之间共享,读锁和写锁之间互斥,写锁和写锁之间互斥,通过读写锁的分离,可以很大程度地减少线程等待锁的阻塞时间,同时保证悲观读取;
以下一个简单的例子演示读写锁,多个线程获取读锁的时候,实际上每个线程在自己的工作内存中,对于共享资源都保存了自己的一个工作副本,此时表现在锁上,是线程共享读锁的状态,当其中一个线程获取写锁后,读锁对所有其他线程互斥,等待获取读锁的线程都在等待写锁线程释放写锁;
一个线程共享对象 TimerBean
//示例Bean
public class TimerBean {
private int num = 10;
//读资源
public int getNum() {
//模拟读取等待
try {
Thread.sleep(new Random().nextInt(1000) + 800);
} catch (InterruptedException e) {
e.printStackTrace();
}
return num;
}
//写资源
public void setNum(int num) {
//莫比写入等待
try {
Thread.sleep(new Random().nextInt(1000) + 800);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.num = num;
}
}
测试 ReadWriteLockTest
public class ReadWriteLockTest {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public ReadWriteLockTest() throws InterruptedException {
TimerBean bean = new TimerBean();
CountDownLatch countDownLatch = new CountDownLatch(10);
ExecutorService exec = Executors.newCachedThreadPool();
//执行4个获取读锁线程
for(int i = 1; i <= 4; i++){
exec.execute(new ReadTask(bean,countDownLatch));
}
//执行1个获取写锁线程
exec.execute(new WriteTask(bean,countDownLatch));
//执行5个获取读锁线程
for(int i = 6; i <= 10; i++){
exec.execute(new ReadTask(bean,countDownLatch));
}
countDownLatch.await();
exec.shutdown();
}
public static void main(String[] args) throws InterruptedException {
new ReadWriteLockTest();
}
//读线程
class ReadTask extends Thread{
private TimerBean bean;
private CountDownLatch countDownLatch;
public ReadTask(TimerBean bean, CountDownLatch countDownLatch) {
this.bean = bean;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
lock.readLock().lock();
int num = bean.getNum();
System.out.println(String.format("[%s] > %d", Thread.currentThread().getName(), num));
lock.readLock().unlock();
countDownLatch.countDown();
}
}
//写线程
class WriteTask extends Thread{
private TimerBean bean;
private CountDownLatch countDownLatch;
public WriteTask(TimerBean bean, CountDownLatch countDownLatch) {
this.bean = bean;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
lock.writeLock().lock();
System.out.println("Write Thread in comming! > 5");
bean.setNum(5);
lock.writeLock().unlock();
countDownLatch.countDown();
}
}
}
输出结果:
[pool-1-thread-1] > 10 //读锁线程可以共享读锁
[pool-1-thread-7] > 10
[pool-1-thread-6] > 10
[pool-1-thread-2] > 10
[pool-1-thread-3] > 10
[pool-1-thread-4] > 10
Write Thread in comming! > 5 //在写锁释放前,所有等待获取读锁线程都被阻塞
[pool-1-thread-8] > 5
[pool-1-thread-9] > 5
[pool-1-thread-10] > 5
JDK1.6 以来对 synchronized 的优化
从线程竞争的效率来讲,synchronzied 竞争效率是不如 Lock 的,自从 JDK1.6 以来,JDK 对于 synchronized 提供了多种优化的方案;
1)优化部分阻塞锁为自旋锁
自旋锁本质上就是执行几个空方法,稍微等一等,事项上可能是一段时间的循环、或者几行空的字节码指令;
自旋锁不能代替阻塞锁,自旋锁本身虽然避免了线程切换的开销,但是要占用CPU时间,如果锁被占用的时间很短,自旋等待的效果就就很显著,但是锁被占用的时候很长,反而会带来CPU资源上的浪费;
JDK 1.6 默认开启,可以使用参数-XX:+UseSpinning -XX:PreBlockSpin 控制;
JDK 1.7 之后JVM不允许用户配置自旋锁,自旋锁会在合适的时机执行,自旋锁次数由 JVM 决定;
2)尽可能进行锁消除
JIT 在运行时,通过逃逸分析,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,如果在以下代码中,如果 JIT 观察变量 sb,发现 sb 变量的引用永远不会逃逸到 main 函数之外,其他线程无法访问该变量,那么在即时编译后,该段代码的同步会被忽略掉而直接执行了;
public static void main() {
StringBuffer sb = new StringBuffer();
sb.append("a").append("b").append("c");
System.out.println(sb.toString());
}
3)进行锁粗化
通过锁粗化,使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁,如以下的例子中 StringBuffer 本来每一个 append 都会进行同步操作,但是 JVM 会优化为只在第1个和最后1个 append 操作进行同步;
StringBuffer sb = new StringBuffer();
sb.append("a").append("b").append("c");
4)提供轻量级锁
轻量级锁能提升程序同步性能的依据是 “对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢;
5)提供偏向锁
引入偏向锁的目的是为了消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能,如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了;
实质就是设置一个变量,判断这个变量是否是当前线程,是就避免再次加锁解锁操作,从而避免了多次的CAS操作;
详细可参考:http://www.cnblogs.com/longshiyVip/p/5213771.html
JDK1.8 引入的新的读写锁 StampedLock
JDK 1.8 引入了新的读写锁 StampedLock,具有在高并发下更强大的吞吐量;
synchronzied 和 ReentrantLock 都是悲观锁,在 ReentrantLock 的读写锁版本 ReentrantReadWriteLock 中 ,只有在沒有任何读写锁时,才可以取得写入锁,这个特性一般用于实现悲观读取,即如果执行中进行读取时,经常可能有另一执行要写入的需求,在读写频繁的情况下可以保证线程同步;
但是在读多写少的情况下,使用 ReentrantReadWriteLock 经常使写入线程迟迟无法竞争到锁定,而一直处于等待状态;
而 StampedLock 可以很大地改善这个问题;
StampedLock控制锁有三种模式:读锁、写锁、乐观读锁,一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为 stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问,通过实现乐观读锁可以极大提升程序地吞吐量;
我们来看一下 JDK8 API StampedLock 的例子:
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
//乐观读锁示例
double distanceFromOrigin() { // A read-only method
//获取乐观读锁
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
//操作后验证是否有写锁发生
if (!sl.validate(stamp)) {
//当发生了写锁,尝试获取悲观读锁,并同步数据
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
//悲观读锁示例
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
}
else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}
推荐阅读
The j.u.c Synchronizer Framework翻译(一)背景与需求 https://yq.aliyun.com/articles/26446
The j.u.c Synchronizer Framework翻译(二)设计与实现 https://yq.aliyun.com/articles/26430
The j.u.c Synchronizer Framework翻译(三)使用、性能与总结 https://yq.aliyun.com/articles/88422