看了这篇 ReentrantLock, Doug Lea 都说服

JDK 1.5 是一个大版本更新, 这个版本引入了 枚举, 泛型, 注解 可变参数, 自动装箱, for-each循环, 还引入了基于老年代的垃圾回收器 CMS, 最重要的是引入了并发包 java.util.concurrent , 由著名的并发编程大师 Doug Lea 亲自操刀, 简化了 Java 开发人员在并发编程中需要考虑的种种事情

今天, 就来带你瞅瞅 Doug Lea 是如何编写代码的, 相信看完之后你会对这个人说一句 : 我草牛逼

先来看看这玩意与 synchronized 的区别, 为什么有了 synchronized , 还要出现 ReentrantLock ?

ReentrantLock和synchronized区别:

  1. ReentrantLock是一个类,而 synchronized 是 Java 的关键字,synchronized 是内置的语言 (C/C++) 实现
  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 ReentrantLock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用 ReentrantLock 时需要在 finally 块中释放锁
  3. ReentrantLock 可以让等待锁的线程响应中断 (通过设置超时方法 tryLock(long, TimeUnit), 和使用lockInterruptibly() ),而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断.
  4. ReentrantLock 可以通过参数类实现公平锁和非公平锁, 而 synchronized 则是使用非公平锁 (无法修改)
  5. 锁绑定多个条件 Condition, ReentrantLock 用来实现分组唤醒需要唤醒的线程们, 而不是像 synchronized 要么随机唤醒一个线程要么唤醒所有线程

这里需要注意的是, ReentrantLock 是通过 Java 代码实现线程同步的, 并没有使用 OS 底层的互斥锁

那么, 我该如何选择呢?

如果是 JDK 1.6 之前, 我肯定选择的是 ReentrantLock , 他的性能远远优于 synchronized.

但是, JDK 1.6 HotSpot 对 synchronized 进行了优化, 加了自旋锁, 偏向锁, 和轻量级锁, 来避免线程数较少的情况下直接上重量级锁

经过测试, 无论是在线程竞争较少, 还是竞争激烈, 无论是代码量大, 还是代码量小, 两者的性能差异不大

但是 ReentrantLock 也有他的优势 :

  1. 能够显示的获取到和释放锁, 锁的运用更灵活
  2. 可以方便的实现公平锁
  3. 灵活度更高, 能够响应中断, 非阻塞获取锁 (不进行线程阻塞或唤醒来获取锁, tryLock() )
public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(1000);
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                test(); // 1133
//                test1(); // 1100 左右
                count.countDown();
            }).start();
        }
        count.await();
        System.out.println(System.currentTimeMillis() - start1);
    }
	static int i;
    static synchronized void test1() {
        // i++;
        sort(1000);
    }
    static Lock lock = new ReentrantLock();

    static void test() {
        lock.lock();
        try {
            // i++;
            sort(1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    static void sort(int num) {
        int[] arr = new int[num];
        for (int j = 0; j < arr.length; j++) {
            arr[j] = (int) (Math.random() * 100000);
        }
        // 冒泡排序
        int tem;
        for (int j = 1; j < arr.length; j++) {
            for (int k = 0; k < arr.length - j; k++) {
                if (arr[k] > arr[k + 1]) {
                    tem = arr[k];
                    arr[k] = arr[k + 1];
                    arr[k + 1] = tem;
                }
            }
        }
    }

ReentrantLock 内部结构

ReentrantLock 实现了 Lock 接口, 对外暴露好几个加锁 API

public class ReentrantLock implements Lock, java.io.Serializable {
    // 加锁的方式由内部类 Sync 实现, 该内部类有两个子类, 分别是公平锁和非公平锁
    private final Sync sync;
       
    // 最重要的就是他的父类, 简称 AQS (队列同步器)
    abstract static class Sync extends AbstractQueuedSynchronizer {...}
	
	static final class NonfairSync extends Sync {...}
    
    static final class FairSync extends Sync {...}
    
    // fair 默认为 false, 所以默认使用的 非公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}      

AQS 内部结构

// AQS 内部维护了两个双向队列, 队列的节点由内部类 Node 实现
// 一个是同步队列, 存储没有获取到同步状态的线程节点
// 另一个是等待队列, 存放 Condition 正在等待的线程节点
public abstract class AbstractQueuedSynchronizer ...{
    
    private transient volatile Node head;

    private transient volatile Node tail;

    // 声明为 volatile, 这是锁的标志位, 如果为 0, 说明处于无锁状态
    private volatile int state;
   
    // 一个节点有两种模式, 独占模式和共享模式
    // 两者的最大区别就是同一时刻是否有多个线程同时获取到同步状态
    // 独占模式 : 对文件的写操作, 只允许一个线程获取到同步状态 (同步队列节点就是独占模式)
    // 共享模式 : 对文件的读操作, 就允许多个线程同时获取到同步状态 (Semaphore 就是共享模式)
    static final class Node {
        /** 表示该节点对应线程是获取共享资源被阻塞挂起放入队列 */
        static final Node SHARED = new Node();
        /**与 SHARED 相反,获取独占资源被阻塞挂起放入队列 */
        static final Node EXCLUSIVE = null;
   
        /** 指示线程已取消, 如果同步队列中等待的线程等待超时或者被打断
        需要从同步队列中取消等待 */
        static final int CANCELLED =  1;
        /** 后继节点的线程处于等待状态, 如果当前节点的线程释放锁或者被取消, 将会通知后继节点, 
        调用 unpark 唤醒后继节点的线程 */
        static final int SIGNAL    = -1;
        /** 节点在等待队列中, 节点线程等待在 Condition 上, 当前其他线程对 Condition 调用了
         singal() 方法后, 该节点将会从等待队列转移到同步队列中, 加入到对锁的获取中 */
        static final int CONDITION = -2;
        /** 表示下一次共享式同步状态将会被无条件的传播下去 */
        static final int PROPAGATE = -3;  

        /** 等待队列的后继节点, 因为等待队列仅在处于独占模式时才被访问 */
        volatile int waitStatus;

        volatile Node prev;

        volatile Node next;
		// 一个队列节点保存了一个线程对象
        volatile Thread thread;
        ...
}

FairSync 加锁

// 查看 FairSync 的 lock 方法
final void lock() {acquire(1);}

-----------------------------------------
// 接着查看 acquire() 方法	
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
------------------------------------------
//查看 tryAcquire()方法,  独占式获取同步状态
protected final boolean tryAcquire(int acquires) {
    final Thread   = Thread.currentThread();
    int c = getState();
    if (c == 0) { // 如果标志位为 0, 说明此时处于无锁状态
        // 接下来判断当前线程是否需要排队, 我们查看这个方法 (下面)
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 如果不需要排队且成功利用 CAS 将标志位从 0 改为 1
            // 说明当前线程得到 '锁', 并保存当前线程
            setExclusiveOwnerThread(current);
            // 这里返回 true, 上面 acquire 方法取反, 直接返回退出
            return true;
        }
    }
    // 锁的标志位不为 0, 如果当前线程为已获得锁的线程 (这里就可以说明 ReentrantLock 具有可重入性)
    else if (current == getExclusiveOwnerThread()) {
        // 重入, 直接将标志位 +1, 解锁时将标志位 -1, 当标志位减为 0, 才是真正的释放锁
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
-------------------------------------
// 如果返回 false, 上面取反, 就会尝试获取锁, 所以只有这两种情况才会返回 false
// 1.如果队列为空
// 2.队列不为空, 队列至少存在两个节点且第二个节点保存的线程为当前线程
public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}


如果 tryAcquire 返回 false, 说明加锁失败, 接着执行 acquireQueued(addWaiter(Node), int)

// 先来看看 addWaiter(Node) 方法
// 首先创建一个新的节点, 保存当前线程对象, 这里传了一个 Node.EXCLUSIVE 进来, 说明这是一个独占式节点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
    node.prev = pred;
    // 采用 CAS 将节点追加到队列尾部
    if (compareAndSetTail(pred, node)) {
        pred.next = node;
        return node;
    }
}
// 如果队列还没有初始化, 初始化这个队列
enq(node);
return node;
------------------
// 再来看看队列如何初始化的
// 初始化队列后, 队列的长度为 2, 而不是 1 (这点很重要)
// 且队列的头结点的 thread 对象永远为 null 
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { 
            // 利用 CAS 将一个空节点设置为队列的头结点 (该节点的 thread 对象为 null)
            if (compareAndSetHead(new Node()))
                tail = head;
        } 
        else {
            node.prev = t;
            // 利用 CAS 将保存了当前线程对象的节点设置为队列的尾节点
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
-----------------------
// 接下来看 acquireQueued(Node, int) 

队列里面的的头结点的thread属性永远为null, 持有锁的线程永远不在队列

//接着进入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() 方法就会看到, 将当前节点的 thread 对象置为 null
                // 并断开与前驱节点的引用
                setHead(node);
                // p 是头结点, 现在没有任何一个引用与之关联
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 如果获取锁失败, 执行 shouldParkAfterFailedAcquire 方法, 查看该方法 (下面)
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())// 调用 LockSupport#park 方法使线程休眠
                // 如果被唤醒, 代码从这里开始执行
                // 如果线程被打断过, 就会进入这里, 将 interrupted 改为 true, 如果获取同步状态成功
                // 就会把 interrupted 返回出去, 然后调用 selfInterrupt(), 
                // 该方法可以防止用户主动的打断线程而造成不稳定因素
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
------------------
// 查看 shouldParkAfterFailedAcquire 方法
// 可以看到, 只有第二次进入该方法才会返回 true, 所以每个节点最多自旋两次
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;// 获取前节点的 waitStatus, 默认为 0
    // 如果为 -1, 直接返回 (如果第二次进来就会进入 if, 返回 true)
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 利用 CAS 将前节点的状态改为 -1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

在自旋两次之后才会休眠线程, 新的节点入队后, 如果无法获取到同步状态, 会把前驱节点状态值改为 -1 (新节点的状态为 0) , 尾结点所有的前驱节点状态值都为 -1

解锁过程

// unlock() -> release(1)
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        // 如果头结点的状态不为零
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
---------------
// tryRelease()方法
protected final boolean tryRelease(int releases) {
    // 每执行一次 unLock(), 就将锁标志位 -1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 锁标志位为 0, 才真正释放锁
    if (c == 0) {
        free = true;
        // 将持有锁的线程置为 null
        setExclusiveOwnerThread(null);
    }
    // 设置最新的锁标志
    setState(c);
    return free;
}
---------------------
// unparkSuccessor()
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;// 获取到头结点的状态位
    if (ws < 0)
        // 利用 CAS 将状态改为 0
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果头结点存在后继节点, 则唤醒该线程
    if (s != null)
        LockSupport.unpark(s.thread);
}

如果感兴趣 NonFairSync 是如何实现的, 可以自己看看源码

猜你喜欢

转载自blog.csdn.net/Gp_2512212842/article/details/107296843
lea
今日推荐