从Java 5之后,Java提供了Lock实现同步访问,需要说明的是Lock并不是用来替代synchronized的。
synchronized有以下不足
效率低:锁的释放情况少、不能设置锁的超时时间、不能中断正在试图获得锁的线程。
不够灵活:加锁、释放锁的时机单一,进入同步同步代码块获取锁,离开释放锁。
Lock可以提供更多高级功能。
ReentrantLock的基本使用
ReentrantLock直接翻译过来是可重入锁的意思,是Lock接口的实现类。
lock()获取锁,unlock()释放锁
class LockMustUnlock {
/**
* 创建可重入锁
*/
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
/**
* lock()获取锁。如果锁已被其他线程持有,则进行等待。
* Lock锁不会像synchronized一样在异常时自动释放锁。
* 最佳实践是在finally的第一行释放锁,以保证发生异常时锁一定被释放。
*
* 最好是写了lock.lock()之后,直接写try finally释放锁,然后再写业务代码
*/
lock.lock();
try {
System.out.println("创建锁,必须释放锁");
} finally {
lock.unlock();
}
}
}
tryLock()与tryLock(long time, TimeUnit unit)
class TryLock{
private static Lock lock = new ReentrantLock();
/**
* tryLock()用法
*/
private static Runnable runnable1 = () -> {
System.out.println(Thread.currentThread().getName()+"运行");
// 如果tryLock()返回true,获取到锁,才能执行释放锁操作
if (lock.tryLock()){
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
System.out.println(Thread.currentThread().getName()+"结束运行");
};
/**
* tryLock(long time, TimeUnit unit)用法
*/
private static Runnable runnable2 = () -> {
System.out.println(Thread.currentThread().getName()+"运行");
try {
/**
* tryLock(long time, TimeUnit unit)
* 如果在给定的等待时间内,其他线程没有持有该锁,并且当前线程没有被中断,则获取该锁。
* 由于有超时时间,可以避免死锁
*/
if (lock.tryLock(1, TimeUnit.SECONDS)){
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
TimeUnit.SECONDS.sleep(2);
}catch(InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}else {
System.out.println(Thread.currentThread().getName()+"未获取到锁");
}
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束运行");
};
public static void main(String[] args) {
//new Thread(runnable1).start();
//new Thread(runnable1).start();
new Thread(runnable2).start();
new Thread(runnable2).start();
}
}
lockInterruptibly() 线程等待锁期间可响应中断
class LockInterruptiblyDemo{
private static Lock lock = new ReentrantLock();
private static Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName()+"运行");
try {
/**
* 线程等待锁期间可以响应中断
*/
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
while (true){
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"等待锁期间被中断");
e.printStackTrace();
}
};
public static void main(String[] args) throws Exception{
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
TimeUnit.MILLISECONDS.sleep(10);
thread2.start();
TimeUnit.MILLISECONDS.sleep(10);
// thread2此时等待锁,处于BLOCKED状态,设置中断标记位
thread2.interrupt();
}
}
ReentrantLock 可重入性质
/**
* 可重入锁:线程持有锁期间,该线程可以反复获取锁
*/
class ReentrantLockRecursion{
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource(){
lock.lock();
try {
/**
* getHoldCount()当前线程持锁次数
*/
if (lock.getHoldCount() < 5){
System.out.println("递归前重入次数:" + lock.getHoldCount());
accessResource();
System.out.println("递归后重入次数:" + lock.getHoldCount());
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
ReentrantReadWriteLock 读写锁
读写锁规则:
1、多个线程只申请读锁,可以申请到。
2、一个线程持有读锁,其他线程申请写锁,申请锁的线程会阻塞,直到读锁被释放。
3、一个线程持有写锁,其他线程申请读锁或写锁,申请锁的线程会阻塞,直到写锁释放。
总结:读锁允许同一时刻被多个线程持有,写锁同一时刻只能被一个线程持有;读锁与写锁互斥,同一时刻不能同时被持有。
从排他性、共享性的角度分类。写锁属于排他锁(又称独占锁),同一时刻只能被一个线程持有。写锁属于共享锁,可以被多个线程同时持有。
class ReadWriteLockDemo{
// 创建读写锁
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// 返回读锁
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
// 返回写锁
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
/**
* 读取资源用读锁
*/
private static void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到读锁");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000) + 2000);
}catch (InterruptedException e){
e.printStackTrace();
}finally{
readLock.unlock();
}
}
/**
* 修改资源用写锁
*/
private static void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到写锁");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000) + 2000);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
writeLock.unlock();
}
}
public static void main(String[] args) throws Exception{
// 多个线程可同时读取数据
new Thread(() -> read()).start();
new Thread(() -> read()).start();
TimeUnit.MILLISECONDS.sleep(10);
/**
* 同一时刻只有一个线程能写入数据
* 并且数据正在被读取时也无法写入数据,因为读锁与写锁也是互斥的
*/
new Thread(() -> write()).start();
new Thread(() -> write()).start();
}
}
公平锁与非公平锁
公平锁:多线程下,线程按照请求锁的顺序得到锁,先请求锁的线程先得到锁。
非公平锁:多线程下,线程不按照请求锁的顺序得到锁。
ReentrantReadWriteLock可创建公平锁,也可创建非公平锁,其内部维护一个先进先出的队列,此队列保存因等待锁而阻塞的线程。
class FairLockDemo{
/**
* 创建公平的读写锁。
* ReentrantReadWriteLock 默认是非公平的
*/
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到读锁");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000) + 3000);
}catch (InterruptedException e){
e.printStackTrace();
}finally{
readLock.unlock();
}
}
private static void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到写锁");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000) + 3000);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
writeLock.unlock();
}
}
/**
* 公平锁,先请求锁的线程先得到锁
*/
public static void main(String[] args) throws Exception{
// 还没有线程持有锁,线程1能获取到读锁
new Thread(() -> read(), "线程1").start();
TimeUnit.MILLISECONDS.sleep(10);
// 线程1持有读锁,线程2请求读取,线程2也能直接获取读锁不需要阻塞
new Thread(() -> read(), "线程2").start();
TimeUnit.MILLISECONDS.sleep(10);
// 线程3请求写锁,但读锁被其他线程持有,线程3阻塞并进入队列排队
new Thread(() -> write(), "线程3").start();
TimeUnit.MILLISECONDS.sleep(10);
/**
* 线程4请求请求读锁,此时即便线程1、2持有的是读锁,
* 但由于线程3在排队,为了体现公平性,线程4不能早于线程3执行,所以线程4也会被放进队列中
*/
new Thread(() -> read(), "线程4").start();
TimeUnit.MILLISECONDS.sleep(10);
new Thread(() -> write(), "线程5").start();
}
}
new ReentrantReadWriteLock(true); 在构造函数中创建一个公平锁对象new FairSync(),FairSync部分源码:
/**
* ReentrantReadWriteLock.FairSync 公平锁部分源码
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
/**
* 锁被释放的瞬间刚好有一个线程请求写锁,当前的写线程是否需要阻塞
* 如果当前线程之前有一个排队的线程,则返回true,需要阻塞;如果当前线程位于队列的开头或队列为空,则返回false。
*/
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
/**
* 读线程与写线程的规则一样
*/
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
假设这样的场景:线程1获取到读锁,线程2请求写锁,读锁写锁互斥,线程2阻塞并进入等待队列;线程1释放锁,此时刚好线程3请求锁(读锁、写锁皆可),由于把线程2从阻塞状态唤醒是需要消耗CPU资源的,为了提高效率,可以让当前请求锁的线程3得到锁,避免了“唤醒线程2,设置线程3为阻塞”的资源消耗。可以认为线程3没去队列中排队,直接获取锁运行了。
这种做法虽然提高了效率,但也有弊端。假如每次释放锁的时候,都刚好有线程请求锁,则等待队列中的线程会因得不到锁长时间等待,这种现象有个名字,叫“线程饥饿”。
为了既能最大程度利用计算机资源,又能避免线程饥饿,ReentrantReadWriteLock对非公平锁定义了两个规则
1、锁释放的瞬间,线程请求写锁,写线程可不进入等待队列,直接得到写锁。
2、锁释放的瞬间,线程请求读锁,当等待队列的头结点不是排他锁的时候,线程可不进入等待队列,直接得到锁。
new ReentrantReadWriteLock(); 在构造函数中创建一个非公平锁对象new NonfairSync(),NonfairSync部分源码:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
/**
* 锁被释放的瞬间刚好有一个线程请求写锁,此线程可不阻塞
*/
final boolean writerShouldBlock() {
return false; // writers can always barge
}
/**
* 锁被释放的瞬间,线程请求读锁
* 如果等待队列头结点不是排他锁(对于读写锁来说,读锁不是排他锁),此线程可不阻塞。
*/
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
下面是一段演示抢锁的代码,可使用公平锁、非公平锁运行,查看两者的差异
class NonFairLockDemo{
/**
* 公平锁
*/
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
/**
* 非公平锁
*/
//private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到读锁");
TimeUnit.MILLISECONDS.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}finally{
readLock.unlock();
}
}
private static void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"得到写锁");
TimeUnit.MILLISECONDS.sleep(30);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
writeLock.unlock();
}
}
public static void main(String[] args) throws Exception {
// 抢锁的线程
Thread thread[] = new Thread[1000];
for (int i = 0; i < 1000; i++) {
// 写线程抢锁
//thread[i] = new Thread(() -> write(), "新的写线程" + i);
// 读线程抢锁
thread[i] = new Thread(() -> read(), "新的读线程" + i);
}
// 排队的读锁线程
Thread threadRead[] = new Thread[10];
for (int i = 0; i < 10; i++) {
threadRead[i] = new Thread(() -> read(), "排队的读线程" + i);
}
// 第一个运行的线程是写线程
new Thread(() -> write(), "写线程一 ").start();
TimeUnit.MILLISECONDS.sleep(5);
// 读线程将在队列中排队
for (int i = 0; i < 10; i++) {
threadRead[i].start();
}
// 大量抢锁线程启动,在“写线程一”释放锁的瞬间有线程抢锁。
TimeUnit.MILLISECONDS.sleep(10);
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
thread[i].start();
}
}).start();
}
}
使用公平锁,抢锁线程是读线程,线程按照请求锁的顺序得到锁。使用非公平锁,抢锁线程是读线程,部分线程能抢锁,控制台打印如下,标红的线程比排队的线程更早打印,抢锁成功。
写线程一 得到写锁
新的读线程192得到读锁
新的读线程193得到读锁
排队的读线程1得到读锁
排队的读线程0得到读锁
新的读线程197得到读锁
新的读线程195得到读锁
新的读线程201得到读锁
使用公平锁,抢锁线程是写线程,线程按照请求锁的顺序得到锁。使用非公平锁,抢锁线程是写线程,部分线程能抢锁,控制台打印如下,标红的线程比排队的线程更早打印,抢锁成功。
写线程一 得到写锁
新的写线程95得到写锁
新的写线程996得到写锁
排队的读线程3得到读锁
排队的读线程7得到读锁
排队的读线程1得到读锁
还有一点要注意,ReentrantLock的API中提到tryLock()方法不支持公平性设置,官方说明如下:
Also note that the untimed tryLock() method does not honor the fairness setting. It will succeed if the lock is available even if other threads are waiting.
还要注意,未定时的tryLock()方法不支持公平性设置。 如果锁定可用,即使其他线程正在等待,它将成功。