ReentrantLock

实现线程同步,保证线程安全。

jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

ReentrantLock简介

  1. ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
  2. 顾名思义,ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取。ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。“锁”是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentraantLock是通过一个FIFO(先进先出)的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。

ReentrantLock函数列表

// 创建一个 ReentrantLock ,默认是“非公平锁”。
ReentrantLock()

// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。
ReentrantLock(boolean fair)

// 查询当前线程保持此锁的次数。
int getHoldCount()

// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner()

// 返回一个 collection,它包含可能正等待获取此锁的线程。
protected Collection<Thread> getQueuedThreads()

// 返回正等待获取此锁的线程估计数。
int getQueueLength()

// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition)

// 返回等待与此锁相关的给定条件的线程估计数。
int getWaitQueueLength(Condition condition)

// 查询给定线程是否正在等待获取此锁。
boolean hasQueuedThread(Thread thread)

// 查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads()

// 查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition)

// 如果是“公平锁”返回true,否则返回false。
boolean isFair()
// 查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()

// 查询此锁是否由任意线程保持。
boolean isLocked()

// 获取锁。
void lock()

// 如果当前线程未被中断,则获取锁。
void lockInterruptibly()

// 返回用来与此 Lock 实例一起使用的 Condition 实例。
Condition newCondition()

// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
boolean tryLock()

// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)

// 试图释放此锁。
void unlock()



可重入锁指在同一个线程中,可以重入的锁。当然,当这个线程获得锁后,其他线程将等待这个锁被释放后,才可以获得这个锁。

通常的使用方法:

ReentrantLock lock = new ReentrantLock(); 
lock.lock();

try {



} finally {
    lock.unlock(); //保证这个锁一定会释放
}



示例分析:测试可重入

那使用ReentrantLock加入锁,代码如下:

public class TestLock {

    private ReentrantLock lock = null;

    public TestLock() {
        // 创建一个自由竞争的可重入锁
        lock = new ReentrantLock();
    }

    public static void main(String[] args) {

        TestLock tester = new TestLock();

        try{
            // 测试可重入,方法testReentry() 在同一线程中,可重复获取锁,执行获取锁后,显示信息的功能
            tester.testReentry();
            // 能执行到这里而不阻塞,表示锁可重入
            tester.testReentry();
            // 再次重入
            tester.testReentry();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            // 释放重入测试的锁,要按重入的数量解锁,否则其他线程无法获取该锁。
            tester.getLock().unlock();
            tester.getLock().unlock();
            tester.getLock().unlock();

        }
    }

    public ReentrantLock getLock() {
        return lock;
    }

    public void testReentry() {
        lock.lock();

        Calendar now = Calendar.getInstance();

        System.out.println(now.getTime() + " " + Thread.currentThread().getName()
                + " get lock.");
    }

}


** 运行结果:


Thu Oct 12 22:01:47 CST 2020 main get lock.
Thu Oct 12 22:01:47 CST 2020 main get lock.
Thu Oct 12 22:01:47 CST 2020 main get lock.

公平锁与非公平锁

    在Java的ReentrantLock构造函数中提供了两种锁:创建公平锁和非公平锁(默认)。代码如下:
/**
 * 默认构造方法,非公平锁
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * true公平锁,false非公平锁
 * @param fair
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync(); //三元运算符
}


如果获取一个锁是按照请求的顺序得到的,那么就是公平锁,否则就是非公平锁。

在没有深入了解内部机制及实现之前,先了解下为什么会存在公平锁和非公平锁。公平锁保证一个阻塞的线程最终能够获得锁,因为是有序的,所以总是可以按照请求的顺序获得锁。非公平锁意味着后请求锁的线程可能在其前面排列的休眠线程恢复前拿到锁,这样就有可能提高并发的性能。这是因为通常情况下挂起的线程重新开始与它真正开始运行,二者之间会产生严重的延时。因此非公平锁就可以利用这段时间完成操作。这是非公平锁在某些时候比公平锁性能要好的原因之一。

锁Lock分为“公平锁”和“非公平锁”,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。

公平锁和非公平锁的差异

public class Service {

    private ReentrantLock lock ;  

    public Service(boolean isFair) {  
        lock = new ReentrantLock(isFair);  
    }  

    public void serviceMethod() {  
        try {  
            lock.lock();  
            System.out.println("ThreadName=" + Thread.currentThread().getName()  
                    + " 获得锁定");  
        } finally {  
            lock.unlock();  
        }  
    }
}

测试

public class Run {
    public static void main(String[] args) throws InterruptedException {  
        final Service service = new Service(true);  //改为false就为非公平锁了  
        Runnable runnable = new Runnable() {  
            public void run() {  
                System.out.println("**线程: " + Thread.currentThread().getName()  
                        +  " 运行了 " );  
                service.serviceMethod();  
            }  
        };  

        Thread[] threadArray = new Thread[10];  

        for (int i=0; i<10; i++) {  
            threadArray[i] = new Thread(runnable);  
        }  
        for (int i=0; i<10; i++) {  
            threadArray[i].start();  
        }  
    }  
}

打印的结果是按照线程加锁的顺序输出的,即线程运行了,则会先获得锁。

final Service service = new Service(true);  //改为false就为非公平锁了

**运行结果:

**

**线程: Thread-1 运行了 
**线程: Thread-2 运行了 
ThreadName=Thread-2 获得锁定
ThreadName=Thread-1 获得锁定
**线程: Thread-6 运行了 
ThreadName=Thread-6 获得锁定
**线程: Thread-7 运行了 
ThreadName=Thread-7 获得锁定
**线程: Thread-0 运行了 
ThreadName=Thread-0 获得锁定
**线程: Thread-4 运行了 
**线程: Thread-9 运行了 
**线程: Thread-5 运行了 
**线程: Thread-3 运行了 
ThreadName=Thread-4 获得锁定
**线程: Thread-8 运行了 
ThreadName=Thread-8 获得锁定
ThreadName=Thread-9 获得锁定
ThreadName=Thread-5 获得锁定
ThreadName=Thread-3 获得锁定

是乱序的,说明先start()启动的线程不代表先获得锁。

ReentrantLock 与 synchronized 的比较

相同:ReentrantLock提供了synchronized类似的功能和内存语义。

不同:

(1)与synchronized相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
(2)ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合(下面会阐述Condition)。
(3)ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。
(4)ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。注:ReentrantLock的锁释放一定要在finally中处理,否则可能会产生严重的后果。
(5)ReentrantLock支持中断处理,且性能较synchronized会好些。

ReentrantLock 不好与需要注意的地方

(1) lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放

(2) 当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象

死锁

死锁的定义

多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题: 死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

下面我们通过一些实例来说明死锁现象。

先看生活中的一个实例,两个人面对面过独木桥,甲和乙都已经在桥上走了一段距离,即占用了桥的资源,甲如果想通过独木桥的话,乙必须退出桥面让出桥的资源,让甲通过,但是乙不服,为什么让我先退出去,我还想先过去呢,于是就僵持不下,导致谁也过不了桥,这就是死锁。

死锁产生的原因


  • 系统资源的竞争
  • 进程间推进顺序非法

死锁产生的必要条件


  • 互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源

  • 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放

  • 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放

  • 环路等待条件:是指进程发生死锁后,必然存在一个进程—资源之间的环形链

下面再来通俗的解释一下死锁发生时的条件:

(1)互斥条件:一个资源每次只能被一个进程使用。独木桥每次只能通过一个人。

(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。乙不退出桥面,甲也不退出桥面。

(3)不剥夺条件: 进程已获得的资源,在未使用完之前,不能强行剥夺。甲不能强制乙退出桥面,乙也不能强制甲退出桥面。

(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。如果乙不退出桥面,甲不能通过,甲不退出桥面,乙不能通过。

死锁实例

/**
 * 一个简单的死锁类
 * t1先运行,这个时候flag==true,先锁定obj1,然后睡眠1秒钟
 * 而t1在睡眠的时候,另一个线程t2启动,flag==false,先锁定obj2,然后也睡眠1秒钟
 * t1睡眠结束后需要锁定obj2才能继续执行,而此时obj2已被t2锁定
 * t2睡眠结束后需要锁定obj1才能继续执行,而此时obj1已被t1锁定
 * t1、t2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。 
 */
public class DeadLock implements Runnable{

    private static Object obj1 = new Object();
    private static Object obj2 = new Object();
    private boolean flag;

    public DeadLock(boolean flag){
        this.flag = flag;
    }

    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName() + "运行");

        if(flag){
            synchronized(obj1){
                System.out.println(Thread.currentThread().getName() + "已经锁住obj1");
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                synchronized(obj2){
                    // 执行不到这里
                    System.out.println("1秒钟后,"+Thread.currentThread().getName()
                                + "锁住obj2");
                }
            }
        }else{
            synchronized(obj2){
                System.out.println(Thread.currentThread().getName() + "已经锁住obj2");
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                synchronized(obj1){
                    // 执行不到这里
                    System.out.println("1秒钟后,"+Thread.currentThread().getName()
                                + "锁住obj1");
                }
            }
        }
    }

}

**测试代码

public class DeadLockTest {

     public static void main(String[] args) {

         Thread t1 = new Thread(new DeadLock(true), "线程1");
         Thread t2 = new Thread(new DeadLock(false), "线程2");

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



运行结果

线程1运行
线程1已经锁住obj1
线程2运行
线程2已经锁住obj2

线程1锁住了obj1,线程2锁住了obj2,线程1企图锁住obj2,进入阻塞,线程2企图锁住obj1,进入阻塞,死锁了。
从这个例子也可以反映出,死锁是因为多线程访问共享资源,由于访问的顺序不当所造成的,通常是一个线程锁定了一个资源A,而又想去锁定资源B;在另一个线程中,锁定了资源B,而又想去锁定资源A以完成自身的操作,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在等待,而无法执行的情况。

例子2:

public class SyncThread implements Runnable{

    private Object obj1;
    private Object obj2;

    public SyncThread(Object o1, Object o2){
        this.obj1=o1;
        this.obj2=o2;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        synchronized (obj1) {
            System.out.println(name + " acquired lock on "+obj1);
            work();
            synchronized (obj2) {
                System.out.println("After, "+name + " acquired lock on "+obj2);
                work();
            }
            System.out.println(name + " released lock on "+obj2);
        }
        System.out.println(name + " released lock on "+obj1);
        System.out.println(name + " finished execution.");
    }

    private void work() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


测试:

    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();

        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");
        Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");

        t1.start();
        Thread.sleep(1000);
        t2.start();
        Thread.sleep(1000);
        t3.start();

    }

**运行结果:

t1 acquired lock on java.lang.Object@5e1077
t2 acquired lock on java.lang.Object@1db05b2
t3 acquired lock on java.lang.Object@181ed9e



在这个例子中,形成了一个锁依赖的环路。以t1为例,它先将第一个对象锁住,但是当它试着向第二个对象获取锁时,它就会进入等待状态,因为第二个对象已经被另一个线程锁住了。这样以此类推,t1依赖t2锁住的对象obj2,t2依赖t3锁住的对象obj3,而t3依赖t1锁住的对象obj1,从而导致了死锁。在线程引起死锁的过程中,就形成了一个依赖于资源的循环。

如何避免死锁

在有些情况下死锁是可以避免的。下面介绍三种用于避免死锁的技术:

  • 加锁顺序(线程按照一定的顺序加锁)
  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 死锁检测

1、加锁顺序

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:

Thread t2 = new Thread(new DeadLock(false), "线程2");



改为:

Thread t2 = new Thread(new DeadLock(true), "线程2");


现在应该不会出现死锁了,因为线程1和线程2都是先对obj1加锁,然后再对obj2加锁,当t1启动后,锁住了obj1,而t2也启动后,只有当t1释放了obj1后t2才会执行,从而有效的避免了死锁。

**运行结果:

线程1运行
线程1已经锁住obj1
线程2运行
1秒钟后,线程1锁住obj2
线程2已经锁住obj1
1秒钟后,线程2锁住obj2



**例子2改造:

public class SyncThread1 implements Runnable{

    private Object obj1;
    private Object obj2;

    public SyncThread1(Object o1, Object o2){
        this.obj1=o1;
        this.obj2=o2;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        synchronized (obj1) {
            System.out.println(name + " acquired lock on "+obj1);
            work();
        }
        System.out.println(name + " released lock on "+obj1);
        synchronized(obj2){
            System.out.println("After, "+ name + " acquired lock on "+obj2);
            work();
        }
        System.out.println(name + " released lock on "+obj2);
        System.out.println(name + " finished execution.");
    }

    private void work() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


** 测试代码:

    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();

        Thread t1 = new Thread(new SyncThread1(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread1(obj2, obj3), "t2");
        Thread t3 = new Thread(new SyncThread1(obj3, obj1), "t3");

        t1.start();
        Thread.sleep(1000);
        t2.start();
        Thread.sleep(1000);
        t3.start();

    }


** 运行结果:

t1 acquired lock on java.lang.Object@60e128
t2 acquired lock on java.lang.Object@18b3364
t3 acquired lock on java.lang.Object@76fba0
t1 released lock on java.lang.Object@60e128
t2 released lock on java.lang.Object@18b3364
After, t1 acquired lock on java.lang.Object@18b3364
t3 released lock on java.lang.Object@76fba0
After, t2 acquired lock on java.lang.Object@76fba0
After, t3 acquired lock on java.lang.Object@60e128
t1 released lock on java.lang.Object@18b3364
t1 finished execution.
t2 released lock on java.lang.Object@76fba0
t3 released lock on java.lang.Object@60e128
t3 finished execution.
t2 finished execution.


从结果中看,没有出现死锁的局面。因为在run()方法中,不存在嵌套封锁。
避免嵌套封锁:这是死锁最主要的原因的,如果你已经有一个资源了就要避免封锁另一个资源。如果你运行时只有一个对象封锁,那是几乎不可能出现一个死锁局面的。

2、加锁时限

这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。

3、死锁检测

虽然造成死锁的原因是因为我们设计得不够好,但是可能写代码的时候不知道哪里发生了死锁。
JDK提供了两种方式来给我们检测:

  • JconsoleJDK自带的图形化界面工具,使用JDK给我们的的工具JConsole
  • Jstack是JDK自带的命令行工具,主要用于线程Dump分析。

总结:避免死锁的方式

  • 让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实。

  • 设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量。

  • 既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后便会返回一个失败信息。