前言
锁像synchronized同步块一样,是一种线程同步机制。让自Java 5开始,java.util.concurrent.locks包提供了另一种方式实现线程同步机制——Lock。那么问题来了既然都可以通过synchronized来实现同步访问了,那么为什么还需要提供Lock呢?这个问题我们下面讨论java.util.concurrent.locks包中包含了一些锁的实现,所以我们不需要重复造轮子了。但是我们仍然需要去了解怎样使用这些锁,且了解这些实现背后的理论也是很有用处的。
本文将从下面几个方面介绍
锁的相关概念
java.util.concurrent.locks下常用的几种锁
一、锁的相关概念
在学习或者使用Java的过程中进程会遇到各种各样的锁的概念:公平锁、非公平锁、自旋锁、可重入锁、偏向锁、轻量级锁、重量级锁、读写锁、互斥锁等待。下边总结了对各种锁的解释
1、公平锁/非公平锁
公平锁是指多个线程在等待同一个锁时按照申请锁的先后顺序来获取锁。相反的非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;非公平锁的好处是整体效率相对高一些,但是有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。其中的原因是公平锁是严格按照请求所的顺序来排队获得锁的,而非公平锁时可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。例:new ReentrantLock(true)是公平锁
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
2、可重入锁
重入锁也叫递归锁,是指在外层函数获得锁之后,内层递归函数仍然可以获取到该锁。即线程可以进入任何一个它已经拥有锁的代码块。在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。可重入锁最大的作用是避免死锁。
3、自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
JDK6中已经变为默认开启自旋锁,并且引入了自适应的自旋锁。自适应意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。自旋是在轻量级锁中使用的,在重量级锁中,线程不使用自旋。
4、偏向锁、轻量级锁和重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5后通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是JDK6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。但是对于锁竞争激励的场合,我其效果不佳。最坏的情况下就是每次都是不同的线程来请求相同的锁,这样偏向模式就会失效。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
5、悲观锁和乐观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度
乐观锁认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。即假定不会发生并发冲突,只在提交操作时检测是否违反数据完整性。(使用版本号或者时间戳来配合实现)。在java中就是 是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。即假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
6、共享锁和独占锁
共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。
独占锁:如果事务T对数据A加上独占锁后,则其他事务不能再对A加任何类型的锁。获得独占锁的事务即能读数据又能修改数据。如Synchronized
7、互斥锁和读写锁
互斥锁:就是指一次最多只能有一个线程持有的锁。在JDK中synchronized和JUC的Lock就是互斥锁。
读写锁:读写锁是一个资源能够被多个读线程访问,或者被一个写线程访问但不能同时存在读线程。Java当中的读写锁通过ReentrantReadWriteLock实现。ReentrantReadWriteLock运行一个资源可以被多个读操作访问,或者一个写操作访问,但两者不能同时进行。
下面开始介绍java.util.concurrent.locks下常用的几种锁,他们都实现了Lock
接口
二、Lock接口和Condition接口
1、Lock接口
void lock() //获得锁。
void lockInterruptibly() throws InterruptedException //获取锁定,除非当前线程是 interrupted 。
boolean tryLock() throws InterruptedException //只有在调用时才可以获得锁。
boolean tryLock(long time, TimeUnit unit) //如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁。
void unlock()//释放锁。
Condition newCondition() //返回一个新Condition绑定到该实例Lock实例。
2、Condition接口
注意使用这些方法必须持有锁
await方法会释放lock,放弃cpu执行权,直到被signal或者signalAll唤醒
//线程等待
void await() throws InterruptedException
void awaitUninterruptibly()
//规定时间内等待
boolean await(long time, TimeUnit unit) throws InterruptedException
long awaitNanos(long nanosTimeout) throws InterruptedException
boolean awaitUntil(Date deadline)throws InterruptedException
//唤醒等待线程
void signal()//唤醒一个等待线程
void signalAll()//唤醒所有等待线程
三、ReentrantLock
其类定义:
public class ReentrantLock extends Object implements Lock, Serializable
1、构造方法
ReentrantLock() //创建一个非公平的ReentrantLock的实例。
ReentrantLock(boolean fair) //根据给定的公平政策创建一个 ReentrantLock的实例,默认false非公平
2、获取竞争当前锁的线程的信息
boolean isFair() //如果此锁的公平设置为true,则返回 true 。
boolean isHeldByCurrentThread() //当前线程是否持有锁,如果持有锁返回true
boolean isLocked() //锁是否被持有 ,true表示有
boolean hasQueuedThread(Thread thread) //查询给定线程是否等待获取此锁。
boolean hasQueuedThreads() //查询是否有线程正在等待获取此锁。true 有
protected Thread getOwner() //返回当前拥有此锁的线程,如果不拥有,则返回 null
protected Collection<Thread> getQueuedThreads() //返回包含可能正在等待获取此锁的线程的集合。
int getHoldCount() //当前线程调用lock()方法的次数
int getQueueLength() //返回等待获取此锁的线程数的估计。
3、获取等待Condition线程的信息
//返回与此锁相关联的给定条件等待的线程数的估计。
int getWaitQueueLength(Condition condition)
//返回包含可能在与此锁相关联的给定条件下等待的线程的集合。
protected Collection<Thread> getWaitingThreads(Condition condition)
//查询任何线程是否等待与此锁相关联的给定条件。
boolean hasWaiters(Condition condition)
四、ReentrantReadWriteLock
其类定义:
public class ReentrantReadWriteLock extends Object implements ReadWriteLock, Serializable
ReentrantReadWriteLock
实现了ReadWriteLock
接口。相比于ReentrantLock
是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock
允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock
能够提供比排他锁更好的并发性和吞吐量。
1、父接口ReadWriteLock方法
Lock readLock() //返回用于读的锁。
Lock writeLock() //返回用于写入的锁。
2、构造方法
ReentrantReadWriteLock() //fair默认false不公平
ReentrantReadWriteLock(boolean fair) //自定义公平政策fair
3、获取读写锁
ReadLock和WriteLock都是ReentrantReadWriteLock的静态内部类,都实现了Lock接口
ReadLock readLock() //获取读锁
WriteLock WriteLock() //获取写锁
4、获取当前竞争写或读
取锁的线程的信息
protected Collection<Thread> getQueuedThreads() //返回当前等待写入或读取锁的线程的集合
int getQueueLength() //返回等待获取读取或写入锁定的线程数的估计
boolean hasQueuedThreads() //查询是否有线程正在等待获取读取或写入锁定。
boolean hasQueuedThread(Thread thread) //查询给定线程是否等待获取读取或写入锁定。
boolean isFair() //如果此锁的公平设置为true,则返回 true 。
5、获取当前竞争写
入锁的线程的信息
protected Thread getOwner() //返回当前拥有写锁的线程名字
protected Collection<Thread> getQueuedWriterThreads() //返回当前等待写锁的线程的集合
boolean isWriteLocked() //查询写锁是否由任何线程持有。
boolean isWriteLockedByCurrentThread() //查询写锁是否由当前线程持有。
int getWriteHoldCount() //查询当前线程对此锁的可重入写入数量
6、获取当前竞争读
取锁的线程的信息
protected Collection<Thread> getQueuedReaderThreads() //返回当前等待读锁的线程的集合
int getReadLockCount() //返回等待获取读锁定的线程数的估计
int getReadHoldCount()//查询当前线程对此锁的可重入读取保留数
五、StampedLock
其类的定义:
public class StampedLock extends Object implements Serializable
StampedLock
是Java8引入的一种新的所机制,简单的理解,可以认为它是读写锁的一个改进版本,读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,它使用的依然是悲观的锁策略,如果有大量的读线程,他也有可能引起写线程的饥饿。而StampedLock
则提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程。
StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
1、构造方法
StampedLock()
2、返回lock锁的视图
Lock asReadLock()
ReadWriteLock asReadWriteLock()
Lock asWriteLock() //
3、获取与释放写
锁
//获取写锁,会堵塞,返回的是stamp,如果锁不可用,则为零
long tryWriteLock() throws InterruptedException
long tryWriteLock(long time, TimeUnit unit) throws InterruptedException
long writeLock()
long writeLockInterruptibly() throws InterruptedException
//将其他锁转换为写锁
//当前邮戳为持有写锁模式,直接返回当前的邮戳;
//当前邮戳为持有读锁模式,则会释放读锁并获取写锁,并返回写锁邮戳;
//当前邮戳持有乐观锁,通过CAS立即获取写锁,成功则返回写锁邮戳;失败则返回0;
long tryConvertToWriteLock(long stamp)
//释放写锁
boolean tryUnlockWrite() //释放写入锁,如果锁被保持,返回true
void unlockWrite(long stamp)
4、获取与释放读
锁
悲观读锁:读线程和写线程不能同时存在
乐观读锁:读线程和写线程可以同时存在,只是读线程会判断当前锁的stamp是否改变了,如果改变了就认为数据是脏数据,读失败
//获取悲观读锁,会堵塞,返回的是stamp,如果锁不可用,则为零,这些锁和ReentrantReadWriteLock的读锁功能一样,都是悲观的读
long tryReadLock() throws InterruptedException
long tryReadLock(long time, TimeUnit unit) throws InterruptedException
long readLock()
long readLockInterruptibly() throws InterruptedException
//将其他锁转换为读锁
//当前邮戳为持有写锁模式,则会释放写锁并获取读锁,并返回读锁邮戳;
//当前邮戳为持有读锁模式,则直接返回当前读锁邮戳;
//当前邮戳持有乐观锁,通过CAS立即获取读锁,则返回读锁邮戳;否则,获取失败返回0
long tryConvertToReadLock(long stamp)
//获取乐观读锁,会堵塞,返回的是stamp,如果锁不可用,则为零
long tryOptimisticRead()
//将其他锁转换为乐观读锁
//当前邮戳为持有读或写锁,则直接释放读写锁,并返回释放后的观察者邮戳值;
//当前邮戳持有乐观锁,若乐观锁邮戳有效,则返回观察者邮戳;
long tryConvertToOptimisticRead(long stamp)
//释放读锁
void unlockRead(long stamp)
boolean tryUnlockRead()
5、验证版本号
boolean validate(long stamp) //验证版本号,如果版本号没有被修改,就返回true
其他方法
public void unlock(long stamp)//如果锁定状态与给定的标记匹配,则释放相应的锁定模式
int getReadLockCount() 查询为此锁持有的读取锁的数量
boolean isReadLocked() 返回 true如果当前锁是读锁
boolean isWriteLocked() 返回 true如果当前锁是写
案例
package com.hjc;
import java.util.concurrent.locks.StampedLock;
/**
* Created by cong on 2018/6/16.
*/
public class Point {
// 成员变量
private double x, y;
// 锁实例
private final StampedLock sl = new StampedLock();
// 排它锁-写锁(writeLock)
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 乐观读锁(tryOptimisticRead)
double distanceFromOrigin() {
// 尝试获取乐观读锁(1)
long stamp = sl.tryOptimisticRead();
// 将全部变量拷贝到方法体栈内(2)
double currentX = x, currentY = y;
// 检查在(1)获取到读锁票据后,锁有没被其它写线程排它性抢占(3)
if (!sl.validate(stamp)) {
// 如果被抢占则获取一个共享读锁(悲观获取)(4)
stamp = sl.readLock();
try {
// 将全部变量拷贝到方法体栈内(5)
currentX = x;
currentY = y;
} finally {
// 释放共享读锁(6)
sl.unlockRead(stamp);
}
}
// 返回计算结果(7)
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 使用悲观锁获取读锁,并尝试转换为写锁
void moveIfAtOrigin(double newX, double newY) {
// 这里可以使用乐观读锁替换(1)
long stamp = sl.readLock();
try {
// 如果当前点在原点则移动(2)
while (x == 0.0 && y == 0.0) {
// 尝试将获取的读锁升级为写锁(3)
long ws = sl.tryConvertToWriteLock(stamp);
// 升级成功,则更新票据,并设置坐标值,然后退出循环(4)
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试(5)
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
// 释放锁(6)
sl.unlock(stamp);
}
最特殊的其实是distanceFromOrigin方法,这个方法中使用了“Optimistic reading”乐观读锁,使得读写可以并发执行。
如上代码 Point 类里面有两个成员变量(x,y) 来标示一个点的二维坐标,和三个操作坐标变量的方法,另外实例化了一个 StampedLock 对象用来保证操作的原子性。
首先分析下 move 方法,该函数作用是使用参数的增量值,改变当前 point 坐标的位置;代码先获取到了写锁,然后对 point 坐标进行修改,然后释放锁。该锁是排它锁,这保证了其它线程调用 move 函数时候会被阻塞,也保证了其它线程不能获取读锁,读取坐标的值,直到当前线程显示释放了写锁,
也就是保证了对变量 x,y 操作的原子性和数据一致性。
接下来再看 distanceFromOrigin 方法,该方法作用是计算当前位置到原点(坐标为 0,0)的距离,代码(1)首先尝试获取乐观读锁,如果当前没有其它线程获取到了写锁,那么(1)会返回一个非 0 的 stamp 用来表示版本信息,代码(2)拷贝坐标变量到本地方法栈里面。
代码(3)检查在(1)获取到的 stamp 值是否还有效,之所以还要在此校验是因为代码(1)获取读锁时候并没有通过 CAS 操作修改锁的状态,而是简单的通过与或操作返回了一个版本信息,这里校验是看在在获取版本信息到现在的时间段里面是否有其它线程持有了写锁,如果有则之前获取的版本信息就无效了。
这里如果校验成功则执行(7)使用本地方法栈里面的值进行计算然后返回。需要注意的是在代码(3) 校验成功后,代码(7)计算期间,其它线程可能获取到了写锁并且修改了 x,y 的值,而当前线程执行代码(7)进行计算时候采用的还是修改前值的拷贝,也就是操作的值是对之前值的一个拷贝,一个快照,并不是最新的值。
也许我们会想,代码(2) 和(3)能否互换?。
答案是明显不能的,如果位置换了,那么首先执行validate ,假设验证通过了,要拷贝x,y 值到本地方法栈,而在拷贝的过程中很有可能其他线程已经修改过了 x,y 中的一个,这就造成了数据的不一致性了。
那么你可能还会这样会想,即使不交换代码 (2) 和(3),在拷贝 x,y 值到本地方法栈里面时,也会存在其他线程修改了x,y中的一个值,这不也会存在问题吗?
这个确实会存在,但是别忘记了拷贝后还有一道validate,如果这时候有线程修改了x,y 中的值,那么肯定是有线程在调用 validate 前,调用 sl.tryOptimisticRead 后获取了写锁,那么进行 validate 时候就会失败。
好了知道这么多原理后,我们就会惊叹这也是乐观读设计的精妙之处也是使用时候容易出问题的地方。下面继续分析 validate 失败后会执行代码(4)获取悲观读锁,如果这时候其他线程持有写锁则代码(4)会导致的当前线程阻塞直到其它线程释放了写锁。
如果这时候没有其他线程获取到写锁,那么当前线程就可以获取到读锁,然后执行代码(5)重新拷贝新的坐标值到本地方法栈,然后就是代码(6)释放了锁,拷贝的时候由于加了读锁,所以拷贝期间其它线程获取写锁时候会被阻塞,
这保证了数据的一致性,另外这里 x,y 没有被声明为 volatie,会不会存在内存不可见性问题那?答案是不会,因为加锁的语义保证了内存可见性,
最后代码(7)使用方法栈里面数据计算返回,同理这里在计算时候使用的数据也可能不是最新的,其它写线程可能已经修改过原来的 x,y 值了。
最后一个方法 moveIfAtOrigin 作用是如果当前坐标为原点则移动到指定的位置。代码(1)获取悲观读锁,保证其它线程不能获取写锁修改 x,y 值,然后代码(2)判断如果当前点在原点则更新坐标,
代码(3) 尝试升级读锁为写锁,这里升级不一定成功,因为多个线程都可以同时获取悲观读锁,当多个线程都执行到(3)时候只有一个可以升级成功,升级成功则返回非 0 的 stamp,否非返回 0。
这里假设当前线程升级成功,然后执行步骤(4)更新 stamp 值和坐标值,然后退出循环。如果升级失败则执行步骤(5)首先释放读锁然后申请写锁,获取到写锁后在循环重新设置坐标值。最后步骤(6) 释放锁。
使用乐观读锁还是很容易犯错误的,必须要严谨,必须要保证如下的使用顺序,用伪代码作为讲解,如下:
long stamp = lock.tryOptimisticRead(); //非阻塞获取版本信息
copyVaraibale2ThreadMemory();//拷贝变量到线程本地堆栈
if(!lock.validate(stamp)){ // 校验
long stamp = lock.readLock();//获取读锁
try {
copyVaraibale2ThreadMemory();//拷贝变量到线程本地堆栈
} finally {
lock.unlock(stamp);//释放悲观锁
}
}
useThreadMemoryVarables();
//使用线程本地堆栈里面的数据进行操作
总结:StampedLock 提供的读写锁与 ReentrantReadWriteLock 类似,只是前者的都是不可重入锁。但是前者通过提供乐观读锁在多线程多读的情况下提供更好的性能,这是因为获取乐观读锁时候不需要进行 CAS 操作设置锁的状态,而只是简单的测试状态。
六、几种锁效率的对象
1、5个读线程与5个写线程:叠加5个并发读线程和5个并发线程,我们看到标记锁(stamped lock)性能比synchronized好3倍。RW锁也表现良好。奇怪的是乐观锁(optimistic locking),表面上应该是最快的,实际上是最慢的。
2、10位读线程与10个写线程: 在这里,事情开始发生重大变化。 RW锁现在比在相同级别上执行的标记锁(stamped lock)和synchronized慢一个数量级。 请注意,令人惊讶的是乐观锁(optimistic locking)仍然最慢的
3、16个读线程与4个写线程:接下来,我保持较高的竞争水平,同时又使增加读者线程。 RW锁继续说明了其被替换的原因,速度慢了一百倍。 Stamped和Optimistic的性能很好,synchronized也不差。
4、19个读线程与1个写线程:最后,我研究了单个写线程和19个读线程情况下的效率对比。 请注意,结果要慢得多,因为单线程需要更长的时间才能完成工作。 在这里,我们得到一些非常有趣的结果。 RW锁需要很长很长时间才能完成,Stamped式锁的性能并没有好得多…… 但是,乐观式锁(Optimistic)显然效率是“ 出奇 ”的好,比RW锁高100倍。即使如此,请记住,这种锁定方式可能会使您失败,因为在此期间可能会出现写的操作。 而我们的老朋友,同步synchronized锁继续取得可喜的成果。
二、自定义锁
在介绍java.util.concurrent.locks下常用的几种锁前,我们先实现自己的锁
public class MyLock {
private final Object object = new Object();
//true 表示 当前已经有线程获取锁,false表示当前没有线程获取锁
private volatile boolean sign = false;
//储存当前获取锁的线程
private volatile Thread thread;
//储存被正在等待的线程
private final List<Thread> waitThreadList = new ArrayList<>();
/**
* 获取锁
*/
public void lock() {
synchronized (object){
//已经有线程获取锁
while (sign){
try {
waitThreadList.add(Thread.currentThread());
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
//没有线程获取锁,当前线程开始获取锁
sign = true;
thread = Thread.currentThread();
}
}
/**
* 获取锁
*/
public void lock(long mills) {
synchronized (object){
long start = System.currentTimeMillis();
judgeTime(mills,start);
//已经有线程获取锁
while (sign){
try {
judgeTime(mills,start);
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
//没有线程获取锁,当前线程开始获取锁
sign = true;
}
}
private void judgeTime(long myMills,long start){
long end = System.currentTimeMillis();
if(myMills<end-start){
throw new IllegalArgumentException("超时");
}
}
/**
* 释放锁
*/
public void unlock(){
if(null==thread){
return;
}
if(!Thread.currentThread().equals(thread)){
throw new IllegalArgumentException("当前线程还没有获取锁");
}
synchronized (object){
sign = false;
object.notifyAll();
}
}
public List<Thread> getWaitThreadList() {
return Collections.unmodifiableList(waitThreadList);
}
}