java之ReentrantLock详解

前言

如果一个代码块被synchronized修饰了,当一个线程获取了相应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的释放,现在有这么一种情况,这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程只能干巴巴地等着,在这种情况下,非常影响程序执行效率

所以Lock应运而生,可以不让等待的线程一直等待下去(比如只等待一定的时间或者能够响应中断)

一、Lock接口

(1)与synchronzed区别

synchronized是JVM层面的内置锁,而Lock则是java层面的显示锁,Lock提供了一种可重入的、可轮询的、定时的以及可中断的锁获取操作。

采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

(2)lock接口源码详解

public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
Condition newCondition();
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
}

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。 newCondition()方法返回一个Condition对象

lock():此方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
 
}finally{
    lock.unlock();   //释放锁
}

tryLock():此方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

Lock lock = ...;
if(lock.tryLock()) {
    try{
         //处理任务
     }catch(Exception ex){
     
     }finally{
         lock.unlock();   //释放锁
    } 
}else {
//如果不能获取锁,则直接做其他事情
}

tryLock(long time, TimeUnit unit):此方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

二、ReentrantLock

(1)模拟可中断的锁获取

Lock中的lockInterruptibly() 可以在获得锁的同时保持对中断的响应,但是内置锁synchronized却很难实现这个功能。

synchronized

如下程序,创建一任务,假设该任务需要执行很长时间才能结束(使用死循环来模拟时长)。现在有两个线程竞争该资源的内置锁,在等待一段时间后,想要终止线程t2的锁获取等待操作,使用t2.interrupt(); 尝试中断线程t2。遗憾的是,此时t2根本不会响应这个中断操作,它会继续等待直到获得资源锁。

public class InterruptedLockTest implements Runnable{
public synchronized void doCount(){
    //使用死循环表示此操作要进行很长的一段时间才能结束
    while(true){}
}

@Override
public void run() {
    doCount();
}
}


public static void main(String[] args) throws InterruptedException {
    InterruptedLockTest test = new InterruptedLockTest();

    Thread t1 = new Thread(test);
    Thread t2 = new Thread(test);

    t1.start();
    t2.start();

      //等待两秒,尝试中断线程t2的等待
    TimeUnit.SECONDS.sleep(2);
    t2.interrupt();

    //等待1秒,让 t2.interrupt(); 执行生效
    TimeUnit.SECONDS.sleep(1);
    System.out.println("线程t1是否存活:" + t1.isAlive());
    System.out.println("线程t2是否存活:" + t2.isAlive());
}

console打印:
线程t1是否存活:true
线程t2是否存活:true

Lock

public class LockDemo implements Runnable {
@Override
public void run() {
    try {
        doCount();
    } catch (InterruptedException e) {
        System.out.println("被中断....");
    }
}
Lock lock = new ReentrantLock();

public void doCount() throws InterruptedException {
    lock.lockInterruptibly();
    try {
        while (true){

        }
    }catch (Exception e){

    }finally {
        lock.unlock();
    }
}

public static void main(String[] args) throws InterruptedException {
    LockDemo lockDemo = new LockDemo();

    Thread t1 = new Thread(lockDemo);
    Thread t2 = new Thread(lockDemo);

    t1.start();
    t2.start();

    TimeUnit.SECONDS.sleep(2);
    t2.interrupt();

    //等待1秒,让 t2.interrupt(); 执行生效
    TimeUnit.SECONDS.sleep(1);
    System.out.println("线程t1是否存活:" + t1.isAlive());
    System.out.println("线程t2是否存活:" + t2.isAlive());
}
}

console打印:
被中断....
线程t1是否存活:true
线程t2是否存活:false

(2)模拟可轮询(避免死锁)

相比于synchronized内置锁的无条件锁获取模式,Lock提供了tryLock() 实现可定时和可轮询的锁获取模式,这也使Lock具有更完善的错误恢复机制。在内置锁中,死锁是一个很严重的问题,造成死锁的原因之一可能是,锁获取顺序不一致导致程序死锁。比如说,线程1持有A对象锁,正在等待获取B对象锁;线程2持有B对象锁,正在等待获取A对象锁。这样,两个线程都会由于获取不到想要的锁而陷入死锁的境地。解决办法可以是,两个线程要么同时获取两个锁,要么一个锁都不获取。Lock 的可定时和可轮询锁就可以很好的满足该条件,从而避免死锁的发生

synchronized死锁问题

public class TestDeadLock implements Runnable{  
int flag ;
static Object o1 = new Object();
static Object o2 = new Object();

@Override
public void run() {
	if(flag==0){
		synchronized (o1) {
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (o2) {
				System.out.println("flag==0");
			}
		}
	}
	else if (flag==1){
		synchronized (o2) {
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (o1) {
				System.out.println("flag==1");
			}
		}
	}
}


public static void main(String[] args) {
	TestDeadLock td1 = new TestDeadLock();
	TestDeadLock td2 = new TestDeadLock();
	td1.flag = 0;
	td2.flag = 1;
	Thread t1 = new Thread(td1);
	Thread t2 = new Thread(td2);
	t1.start();
	t2.start();
}

}

ReentrantLock可轮询

// 资源类
public class Resource {
    //资源总和
    private int resourceNum;
    // 显示锁
    public Lock lock = new ReentrantLock();

    public Resource(int resourceNum){
        this.resourceNum = resourceNum;
    }
    //返回此资源的总量
    public int getResourceNum(){
        return resourceNum;
    }
}

public class LockTest1 {
  //传入两个资源类和预期操作时间,在此期间内返回两个资源的数量总和
public int getResource(Resource resourceA, Resource resourceB, long timeout, TimeUnit unit)
      throws InterruptedException {
    // 获取当前时间,算出操作截止时间
    long stopTime = System.nanoTime() + unit.toNanos(timeout);

    while(true){
        try {
            // 尝试获得资源A的锁
            if (resourceA.lock.tryLock()) {
                try{
                    // 如果获得资源A的锁,尝试获得资源B的锁
                    if(resourceB.lock.tryLock()){
                        //同时获得两资源的锁,进行相关操作后返回
                        return getSum(resourceA, resourceB);
                    }
                }finally {
                    resourceB.lock.unlock();
                }
            }
        }finally {
            resourceA.lock.unlock();
        }
      
        // 判断当前是否超时,规定-1为错误标识
        if(System.nanoTime() > stopTime)
            return -1;
        
        //睡眠1秒,继续尝试获得锁
        TimeUnit.SECONDS.sleep(1);
    }
}

// 获得资源总和
public int getSum(Resource resourceA,Resource resourceB){
    return resourceA.getResourceNum()+resourceB.getResourceNum();
}
}

(3)公平锁

背景

CPU在调度线程的时候是在等待队列里随机挑选一个线程,由于这种随机性所以是无法保证线程先到先得的(synchronized控制的锁就是这种非公平锁)。但这样就会产生饥饿现象,即有些线程(优先级较低的线程)可能永远也无法获取cpu的执行权,优先级高的线程会不断的强制它的资源。那么如何解决饥饿问题呢,这就需要公平锁了。

公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较地,因为要实现顺序执行,需要维护一个有序队列。

// 也可以指定公平性
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
//默认创建非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}

Demo

package com.jalja.base.threadTest;

import java.util.concurrent.locks.ReentrantLock;

public class LockFairTest implements Runnable{
//创建公平锁
private static ReentrantLock lock=new ReentrantLock(true);
public void run() {
    while(true){
        lock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+"获得锁");
        }finally{
            lock.unlock();
        }
    }
}
public static void main(String[] args) {
    LockFairTest lft=new LockFairTest();
    Thread th1=new Thread(lft);
    Thread th2=new Thread(lft);
    th1.start();
    th2.start();
}
}

console打印:
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁

分析结果可看出两个线程是交替执行的,几乎不会出现同一个线程连续执行多次。

三、Synchronized与Lock的区别

锁类型

  • 可重入锁:在执行对象中所有同步方法不用再次获得锁

  • 可中断锁:在等待获取锁过程中可中断

  • 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利

  • 读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

区别

类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是一个类
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态 无法判断 可以判断
锁类型 可重入 不可中断 非公平 可重入 可中断 可公平(两者皆可)
性能 少量同步 大量同步

关于读写锁

我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。

原文地址:https://www.cnblogs.com/sxkgeek/p/9401632.html