Java多线程之Lock深入理解

1 Lock原理

Java中已经有了synchronized重量级锁,那么为什么还得有Lock,之所以引入Lock,得先了解synchronized原理,继而就会发现其缺陷

1.1 synchronized的缺陷

synchronizedjava中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  • 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  • 线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

  1. Lock不是Java语言内置的,synchronizedJava语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
  2. Locksynchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

1.2 Lock锁控制并发原理

Java 中,Lock 接口的实现类(如 ReentrantLock)可以在多个线程之间共享。多个线程可以通过同一个 Lock 对象来获取锁并执行临界区代码。
当一个线程调用 Lock 对象的 lock() 方法时,如果锁是可用的,该线程将获取到锁并继续执行。如果锁已经被其他线程持有,那么该线程将被阻塞,直到锁被释放。
Lock 对象的锁是可重入的,也就是说同一个线程可以多次获取同一个锁,而不会产生死锁。当一个线程重复获取锁时,需要相应地释放相同次数的锁才能完全释放锁。
通过 Lock 对象,可以实现更灵活的锁定解锁机制,可以使用 tryLock() 方法尝试获取锁,使用 lockInterruptibly() 方法可以响应中断请求,还可以使用 Condition 对象实现更细粒度的线程间通信。

需要注意的是,Lock 对象的锁不会自动释放,必须手动调用 unlock() 方法来释放锁,通常在 finally 块中进行释放操作,以确保锁的正确释放,避免死锁的发生。

1.3 AQS

Lock 之所以能实现线程安全的锁,主要的核心是AQS(AbstractQueuedSynchronizer)AQS提供了一个FIFO队列,可以看做是一个用来实现锁以及其他需要同步功能的框架。AQS的使用依靠继承来完成,子类通过继承自AQS并实现所需的方法来管理同步状态。例如常见的ReentrantLock,CountDownLatch等。
从使用上来说,AQS的功能可以分为两种:独占共享

  • 独占锁模式下,每次只能有一个线程持有锁,ReentrantLock就是以独占方式实现的互斥锁。
  • 共享锁模式下,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock

独占共享特点

  • 相同点:
    • 获取锁前都会判断是否有权限,只有满足条件才可能获取到锁
    • 未获取到锁的线程会创建新节点放入队列尾部
  • 不同点:
    • 独占锁只会释放头部后节点的线程,而共享锁会依次释放所有线程
    • 独占锁存在非公平锁的情况,新的线程可能抢占队列中线程的锁,共享锁则不存在这种情况

1.3.1 内部类Node

AQS 有两个内部类:NodeConditionObject,这里只介绍 Node

Node 类是等待队列中的节点类,是一个基于 FIFO 的双向队列
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

Node的主要属性:

static final class Node {
    //表示节点的状态,包含SIGNAL、CANCELLED、CONDITION、PROPAGATE、INITIAL
    volatile int waitStatus;
    //前继节点
    volatile Node prev;
    //后继节点
    volatile Node next;
    //入队节点的线程, 结束之后会被置空
    volatile Thread thread;
    //存储在 Condition 队列中的下一个等待节点
    Node nextWaiter; 
}

waitStatus节点状态
waitStatus节点的几种状态:

  • CANCELLED,值为1,由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态将不会变化。
  • SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点得以运行。
  • CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。
  • PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件地被传播下去。
  • INITAL,值为0,初始状态。

1.3.2 主要属性

AQS 主要属性说明如下

/**
 * 等待队列的头节点, 赖加载
 * 除了初始化之外, 只能通过 setHead 方法来改变其值
 * 如果 head 不为 null, waitStatus 值就一定不会是 CANCELLED
 */
private transient volatile Node head;
 
/**
 * 等待队列的尾结点, 懒加载
 * 只能通过 enq 方法添加新节点时才会去改变尾结点
 */
private transient volatile Node tail;
 
/**
 * 同步器的状态
 * 以 ReentrantLock 为例, 0 表示可以获取到锁, 其他的正整数表示无法获取到锁
 */
private volatile int state;
  • 设置尾节点
    当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取,转而被构造成为节点并加入同步队列,而这个过程必须保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Nodeupdate),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
  • 设置首节点
    同步队列遵循 FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。
    设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。
  • state
    AbstractQueuedSynchronizer 还有一个重要变量 state,用于记录是否有线程获取锁,可重入锁还记录了获取多少次锁, 没有锁住时,state0,有线程获取锁时为1,可重入锁,每重新获取一次锁,state加1,释放一次就减1

1.3.3 方法

这里主要分析获取和释放锁相关的代码

1.3.3.1 获取锁

  • acquire
    获取独占锁,方法的逻辑是先尝试获取锁,如果失败,就往队列末尾添加一个节点,再尝试从队列里获取锁
  • tryAcquire
    尝试获取独占锁,需要由子类自己实现,体现了 Abstract
  • addWaiter
    往尾部添加一个节点作为新的尾结点。如果尾结点未初始化,则通过 enq 完成初始化
  • acquireQueued
    不断尝试从队列中获取锁,当成功获取到锁或者线程被打断时会成功退出循环,竞争锁失败的线程会等待直到被唤醒,唤醒之后会再次进入循环尝试去获取锁,不断的重复整个过程
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 不断尝试去获取锁直到成功或线程被打断
        for (;;) {
            // 获取当前节点的前节点
            final Node p = node.predecessor();
            // 如果前节点是头部节点, 就尝试去获取锁
            if (p == head && tryAcquire(arg)) {
                // 成功之后将节点设置为新的头部节点
                // 可以理解为头部节点就是获取到锁的节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 获取锁失败后进到这里
            // 先判断线程是否需要 park, 如果是就执行 park, 否则再进入循环
            // 这里还判断了线程是否已经被打断了, 如果是, 就会执行 cancelAcquire
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • acquireShared
    获取共享锁,方法逻辑是先尝试获取锁,如果失败,就不断尝试去获取锁
  • tryAcquireShared
    尝试获取共享锁,需要由子类自己实现
  • doAcquireShared
    不断尝试从队列中获取共享锁,当成功获取到锁或者线程被打断时会成功退出循环,竞争锁失败的线程会被 park 直到被唤醒,唤醒之后会再次进入循环尝试去获取锁,不断的重复整个过程

1.3.3.2 释放锁

  • release
    释放独占锁,方法逻辑是首先尝试释放锁,成功后,如果头节点不为空而且 waitStatus 不为 0(即节点不是初始化的状态),就唤醒后节点的线程
  • tryRelease
    尝试释放独占锁,需要由子类实现
  • releaseShared
    是否共享锁,逻辑是先尝试释放锁,如果成功,再依次去释放节点上阻塞的线程
  • tryReleaseShared
    尝试释放共享锁,需要由子类实现
  • doReleaseShared
    释放共享锁

1.4 LockSupport

Lock中核心加锁释放锁用的是LockSupport
在这里插入图片描述
LockSupportJava并发编程中的一个工具类,它主要用于实现基于线程的阻塞和唤醒。这个类是JDK1.5以后引入的,主要用于实现Java的并发编程。
LockSupport类提供了一种线程阻塞和唤醒的机制,它是通过控制线程的阻塞状态来实现线程的挂起和恢复的。LockSupport中的park()unpark()方法就是用来实现线程的阻塞和唤醒的。park()方法可以阻塞当前线程,而unpark()方法则可以唤醒一个指定的线程。这两个方法的使用比Object类中的wait()notify()方法更加灵活,因为它们不需要在同步代码块中使用。

以下是 LockSupport 类的一些核心方法:

  • void park():阻塞当前线程,直到许可可用。如果调用 park() 时线程已经拥有许可,则它会立即返回并消耗掉这个许可。
  • void parkNanos(long nanos):阻塞当前线程,直至以下三种情况之一发生:许可可用、线程被中断,或者等待时间超出设定的纳秒数。
  • void parkUntil(long deadline):阻塞当前线程直至以下三种情况之一发生:许可可用、线程被中断,或者直到指定的绝对时间(自纪元以来的毫秒数)。
  • void unpark(Thread thread):为指定线程提供许可(如果它还没有),并且在有可能的情况下立即唤醒它。如果线程在调用时没有被阻塞,则下次它调用 park() 方法时会立即返回。

LockSupport 的优点包括:

  • 它可以响应中断,但是不抛出 InterruptedException 异常。相比 Object.wait() 和 Thread.join() 方法,在某些场景下更加灵活。
  • park() 和 unpark() 方法对于处理先发起唤醒信号,再实施阻塞这种时序问题比 wait()/notify() 要简单。即使先调用了 unpark(),再调用 park() 的线程依然能够正确的被唤醒,而不会产生死锁。

注意LockSupportpark方法不会释放锁,它只是让当前线程进入等待状态,也就是阻塞当前线程。

1.5 Lock和synchronized的选择

总结来说,Locksynchronized有以下几点不同:

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。
  6. Lock不像synchronized那样是获取实例对象锁类锁的,它获取的是Lock对象的锁

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

2 java.util.concurrent.locks包下常用的类

下面我们就来探讨一下java.util.concurrent.locks包中常用的类和接口。

2.1 Lock接口

图片
首先要说明的就是Lock,通过查看Lock的源码可知,Lock是一个接口:

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

下面来逐个讲述Lock接口中每个方法的使用,lock()、tryLock()、tryLock(long time, TimeUnit unit)lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。newCondition()这个方法稍后讲解

Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?

2.1.1 lock方法

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

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

2.1.2 tryLock方法

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
所以,一般情况下通过tryLock来获取锁时是这样使用的:

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

2.1.3 lockInterruptibly方法

lockInterruptibly为可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException
因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意: 当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去

2.1.4 newCondition方法

newCondition这里的条件是没有实际含义的,仅仅是个标记而已,并且条件的含义往往通过代码来赋予其含义
条件变量都实现了java.util.concurrent.locks.Condition接口,条件变量的实例化是通过一个Lock对象上调用newCondition()方法来获取的,这样,条件就和一个锁对象绑定起来了。因此,Java中的条件变量只能和锁配合使用,来控制并发程序访问竞争资源的安全

一个锁可以有多个条件,每个条件上可以有多个线程等待,通过调用await()方法,可以让线程在该条件下等待。当调用signalAll()方法,又可以唤醒该条件下的等待的线程,signal()会唤醒其condition中的一个等待线程

条件变量比较抽象,原因是他不是自然语言中的条件概念,而是程序控制的一种手段
看下Condition接口中方法

public interface Condition {
     //使当前线程加入 await() 等待队列中,并释放当锁,当其他线程调用signal()会重新请求锁。与Object.wait()类似。
    void await() throws InterruptedException;
    //调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。
    //调用该方法后,结束等待的唯一方法是其它线程调用该条件对象的signal()或signalALL()方法。等待过程中如果当前线程被中断,该方法仍然会继续等待,同时保留该线程的中断状态。 
    void awaitUninterruptibly();
    // 调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。
    //nanosTimeout指定该方法等待信号的的最大时间(单位为纳秒)。若指定时间内收到signal()或signalALL()则返回nanosTimeout减去已经等待的时间;
    //若指定时间内有其它线程中断该线程,则抛出InterruptedException并清除当前线程的打断状态;若指定时间内未收到通知,则返回0或负数。 
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    //与await()基本一致,唯一不同点在于,指定时间之内没有收到signal()或signalALL()信号或者线程中断时该方法会返回false;其它情况返回true。
    boolean await(long time, TimeUnit unit) throws InterruptedException;

   //适用条件与行为与awaitNanos(long nanosTimeout)完全一样,唯一不同点在于它不是等待指定时间,而是等待由参数指定的某一时刻。
    boolean awaitUntil(Date deadline) throws InterruptedException;    
    //唤醒一个在 await()等待队列中的线程。与Object.notify()相似
    void signal();
   //唤醒 await()等待队列中所有的线程。与object.notifyAll()相似
    void signalAll();
}

2.2 ReentrantLock

ReentrantLock意思是可重入锁

2.2.1 可重入锁概念

如果锁具备可重入性,则称作为可重入锁。像synchronizedReentrantLock都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2
看下面这段代码就明白了:

class MyClass {
    public synchronized void method1() {
        method2();
    }
     
    public synchronized void method2() {}
}

上述代码中的两个方法method1method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,那么此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronizedLock都具备可重入性,所以不会发生上述现象。

2.2.2 ReentrantLock使用

ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例看具体看一下如何使用ReentrantLock

2.2.2.1 lock用法

lock()的正确使用方法

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        Lock lock = new ReentrantLock();    //注意这个地方
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}

运行完后,也许有疑惑,第二个线程怎么会在第一个线程释放锁之前得到了锁?原因在于,在insert方法中的lock变量是局部变量,每个线程执行该方法时都会保存一个副本,那么理所当然每个线程执行到lock.lock()处获取的是不同的锁,所以就不会发生冲突。
知道了原因改起来就比较容易了,只需要将lock声明为类的属性即可

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}
运行结果:
Thread-0得到了锁
Thread-0释放了锁
Thread-1得到了锁
Thread-1释放了锁

这样就是正确地使用Lock的方法了
由上可知,Lock不像synchronized那样是获取实例对象锁或类锁的,它获取的是Lock对象的锁,如果获取不到Lock对象,就不存在竞争,否则就会有竞争情况

2.2.2.2 tryLock用法

例子2,tryLock()的使用方法

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        if(lock.tryLock()) {
            try {
                System.out.println(thread.getName()+"得到了锁");
                for(int i=0;i<5;i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                System.out.println(thread.getName()+"释放了锁");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName()+"获取锁失败");
        }
    }
}

运行结果:
Thread-0得到了锁
Thread-0释放了锁
Thread-1获取锁失败

2.2.2.3 lockInterruptibly用法

例子3,lockInterruptibly()响应中断的使用方法:

public class Test {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.start();
        thread2.start();
         
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  
     
    public void insert(Thread thread) throws InterruptedException{
        lock.lockInterruptibly();   //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
        try {  
            System.out.println(thread.getName()+"得到了锁");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入数据
            }
        }
        finally {
            System.out.println(Thread.currentThread().getName()+"执行finally");
            lock.unlock();
            System.out.println(thread.getName()+"释放了锁");
        }  
    }
}
 
class MyThread extends Thread {
    private Test test = null;
    public MyThread(Test test) {
        this.test = test;
    }
    @Override
    public void run() {
         
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中断");
        }
    }
}

运行结果:
Thread-0得到了锁
Thread-1被中断

运行之后,发现thread-1能够被正确中断。

2.2.2.4 newCondition用法

ReentrantLock中的newCondition用法

public class NewConditionDemo {
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();

    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                lock.lock();//请求锁
                try {
                    System.out.println(Thread.currentThread().getName() + "==》进入等待");
                    condition.await();//设置当前线程进入等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();//释放锁
                }
                System.out.println(Thread.currentThread().getName() + "==》继续执行");
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                lock.lock();//请求锁
                try {
                    System.out.println(Thread.currentThread().getName() + "=》进入");
                    Thread.sleep(2000);//休息2秒
                    condition.signal();//随机唤醒等待队列中的一个线程
                    System.out.println(Thread.currentThread().getName() + "休息结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();//释放锁
                }
            }
        }.start();
    }
}

2.3 ReadWriteLock

ReadWriteLock读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock()获取读锁,通过writeLock()获取写锁。

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();
 
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

2.4 ReentrantReadWriteLock

ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()writeLock()用来获取读锁和写锁。
下面通过几个例子来看一下ReentrantReadWriteLock具体用法。
假如有多个线程要同时进行读操作的话,先看一下synchronized达到的效果:

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在进行读操作");
        }
        System.out.println(thread.getName()+"读操作完毕");
    }
}

这段程序的输出结果会是,直到thread0执行完读操作之后,才会打印thread1执行读操作的信息。

而改成用读写锁的话:

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
             
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在进行读操作");
            }
            System.out.println(thread.getName()+"读操作完毕");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

说明thread0和thread1在同时进行读操作。
这样就大大提升了读操作的效率。
不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

2.5 公平锁和非公平锁

2.5.1 公平锁和非公平锁定义及创建

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

其中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLockReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
看一下这2个类的源代码就清楚了:

/**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    } 

/**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    } 

ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。
我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:

ReentrantLock lock = new ReentrantLock(true);

如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。

另外在ReentrantLock类中定义了很多方法,比如:

  • isFair() //判断锁是否是公平锁
  • isLocked() //判断锁是否被任何线程获取了
  • isHeldByCurrentThread() //判断锁是否被当前线程获取了
  • hasQueuedThreads() //判断是否有线程在等待该锁

ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口

2.5.2 公平锁

使用公平锁时,加锁方法lock()的方法调用轨迹如下:

  1. ReentrantLock : lock()
  2. FairSync : lock()
  3. AbstractQueuedSynchronizer : acquire(int arg)
  4. ReentrantLock : tryAcquire(int acquires)

在第4步真正开始加锁,下面是该方法的源代码:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();   //获取锁的开始,首先读volatile变量state
    if (c == 0) {
        if (isFirst(current) &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)  
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

从上面源代码中我们可以看出,加锁方法首先读volatile变量state
在使用公平锁时,解锁方法unlock()的方法调用轨迹如下:

  1. ReentrantLock : unlock()
  2. AbstractQueuedSynchronizer : release(int arg)
  3. Sync : tryRelease(int releases)

在第3步真正开始释放锁,下面是该方法的源代码:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);           //释放锁的最后,写volatile变量state
    return free;
}

从上面的源代码我们可以看出,在释放锁的最后写volatile变量state
公平锁在释放锁的最后写volatile变量state;在获取锁时首先读这个volatile变量。根据volatilehappens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变的对获取锁的线程可见

2.5.3 非公平锁

非公平锁的释放和公平锁完全一样,所以这里仅仅分析非公平锁的获取。
使用公平锁时,加锁方法lock()的方法调用轨迹如下:

  1. ReentrantLock : lock()
  2. NonfairSync : lock()
  3. AbstractQueuedSynchronizer : compareAndSetState(int expect, int update)

在第3步真正开始加锁,下面是该方法的源代码:

protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

该方法以原子操作的方式更新state变量,把javacompareAndSet()方法调用简称为CASJDK文档对该方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有 volatile 读和写的内存语义。
这里我们分别从编译器和处理器的角度来分析,CAS如何同时具有volatile读和volatile写的内存语义。
编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CASCAS前面和后面的任意内存操作重排序。

现在对公平锁非公平锁的内存语义做个总结:

  • 公平锁和非公平锁释放时,最后都要写一个volatile变量state
  • 公平锁获取时,首先会去读这个volatile变量。
  • 非公平锁获取时,首先会用CAS更新这个volatile变量,这个操作同时具有volatile读和volatile写的内存语义。
posted @ 2021-12-25 10:08  上善若泪  阅读(650)  评论(0编辑  收藏  举报