关于java:深入理解JAVA并发锁

30次阅读

共计 25186 个字符,预计需要花费 63 分钟才能阅读完成。

深刻了解 Java 并发锁

1. 并发锁简介

确保线程平安最常见的做法是利用锁机制(Locksychronized)来对共享数据做互斥同步,这样在同一个时刻,只有一个线程能够执行某个办法或者某个代码块,那么操作必然是原子性的,线程平安的。

在工作、面试中,常常会听到各种形形色色的锁,听的人云里雾里。锁的概念术语很多,它们是针对不同的问题所提出的,通过简略的梳理,也不难理解。

1.1. 可重入锁

可重入锁,顾名思义,指的是线程能够反复获取同一把锁。即同一个线程在外层办法获取了锁,在进入内层办法会主动获取锁。

可重入锁能够在肯定水平上防止死锁

  • ReentrantLockReentrantReadWriteLock 是可重入锁。这点,从其命名也不难看出。
  • synchronized 也是一个可重入锁

【示例】synchronized 的可重入示例

synchronized void setA() throws Exception{Thread.sleep(1000);
    setB();}

synchronized void setB() throws Exception{Thread.sleep(1000);
}

下面的代码就是一个典型场景:如果应用的锁不是可重入锁的话,setB 可能不会被以后线程执行,从而造成死锁。

【示例】ReentrantLock 的可重入示例

class Task {

    private int value;
    private final Lock lock = new ReentrantLock();

    public Task() {this.value = 0;}

    public int get() {
        // 获取锁
        lock.lock();
        try {return value;} finally {
            // 保障锁能开释
            lock.unlock();}
    }

    public void addOne() {
        // 获取锁
        lock.lock();
        try {
            // 留神:此处曾经胜利获取锁,进入 get 办法后,又尝试获取锁,// 如果锁不是可重入的,会导致死锁
            value = 1 + get();} finally {
            // 保障锁能开释
            lock.unlock();}
    }

}

1.2. 偏心锁与非偏心锁

  • 偏心锁 – 偏心锁是指 多线程依照申请锁的程序来获取锁
  • 非偏心锁 – 非偏心锁是指 多线程不依照申请锁的程序来获取锁。这就可能会呈现优先级反转(后来者居上)或者饥饿景象(某线程总是抢不过别的线程,导致始终无奈执行)。

偏心锁为了保障线程申请程序,势必要付出肯定的性能代价,因而其吞吐量个别低于非偏心锁。

偏心锁与非偏心锁 在 Java 中的典型实现:

  • synchronized 只反对非偏心锁
  • ReentrantLockReentrantReadWriteLock,默认是非偏心锁,但反对偏心锁

1.3. 独享锁与共享锁

独享锁与共享锁是一种狭义上的说法,从理论用处上来看,也常被称为互斥锁与读写锁。

  • 独享锁 – 独享锁是指 锁一次只能被一个线程所持有
  • 共享锁 – 共享锁是指 锁可被多个线程所持有

独享锁与共享锁在 Java 中的典型实现:

  • synchronizedReentrantLock 只反对独享锁
  • ReentrantReadWriteLock 其写锁是独享锁,其读锁是共享锁。读锁是共享锁使得并发读是十分高效的,读写,写读,写写的过程是互斥的。

1.4. 乐观锁与乐观锁

乐观锁与乐观锁不是指具体的什么类型的锁,而是 解决并发同步的策略

  • 乐观锁 – 乐观锁对于并发采取乐观的态度,认为: 不加锁的并发操作肯定会出问题 乐观锁适宜写操作频繁的场景
  • 乐观锁 – 乐观锁对于并发采取乐观的态度,认为: 不加锁的并发操作也没什么问题。对于同一个数据的并发操作,是不会产生批改的 。在更新数据的时候,会采纳一直尝试更新的形式更新数据。 乐观锁适宜读多写少的场景

乐观锁与乐观锁在 Java 中的典型实现:

  • 乐观锁在 Java 中的利用就是通过应用 synchronizedLock 显示加锁来进行互斥同步,这是一种阻塞同步。
  • 乐观锁在 Java 中的利用就是采纳 CAS 机制(CAS 操作通过 Unsafe 类提供,但这个类不间接裸露为 API,所以都是间接应用,如各种原子类)。

1.5. 偏差锁、轻量级锁、重量级锁

所谓轻量级锁与重量级锁,指的是锁管制粒度的粗细。显然,管制粒度越细,阻塞开销越小,并发性也就越高。

Java 1.6 以前,重量级锁个别指的是 synchronized,而轻量级锁指的是 volatile

Java 1.6 当前,针对 synchronized 做了大量优化,引入 4 种锁状态:无锁状态、偏差锁、轻量级锁和重量级锁。锁能够单向的从偏差锁降级到轻量级锁,再从轻量级锁降级到重量级锁。

  • 偏差锁 – 偏差锁是指一段同步代码始终被一个线程所拜访,那么该线程会主动获取锁。升高获取锁的代价。
  • 轻量级锁 – 是指当锁是偏差锁的时候,被另一个线程所拜访,偏差锁就会降级为轻量级锁,其余线程会通过自旋的模式尝试获取锁,不会阻塞,进步性能。
  • 重量级锁 – 是指当锁为轻量级锁的时候,另一个线程尽管是自旋,但自旋不会始终继续上来,当自旋肯定次数的时候,还没有获取到锁,就会进入阻塞,该锁收缩为重量级锁。重量级锁会让其余申请的线程进入阻塞,性能升高。

1.6. 分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁。所谓分段锁,就是把锁的对象分成多段,每段独立管制,使得锁粒度更细,缩小阻塞开销,从而进步并发性。这其实很好了解,就像高速公路上的收费站,如果只有一个收费口,那所有的车只能排成一条队缴费;如果有多个收费口,就能够分流了。

Hashtable 应用 synchronized 润饰办法来保障线程安全性,那么面对线程的拜访,Hashtable 就会锁住整个对象,所有的其它线程只能期待,这种阻塞形式的吞吐量显然很低。

Java 1.7 以前的 ConcurrentHashMap 就是分段锁的典型案例。ConcurrentHashMap 保护了一个 Segment 数组,个别称为分段桶。

final Segment<K,V>[] segments;

当有线程拜访 ConcurrentHashMap 的数据时,ConcurrentHashMap 会先依据 hashCode 计算出数据在哪个桶(即哪个 Segment),而后锁住这个 Segment

1.7. 显示锁和内置锁

Java 1.5 之前,协调对共享对象的拜访时能够应用的机制只有 synchronizedvolatile。这两个都属于内置锁,即锁的申请和开释都是由 JVM 所管制。

Java 1.5 之后,减少了新的机制:ReentrantLockReentrantReadWriteLock,这类锁的申请和开释都能够由程序所管制,所以常被称为显示锁。

留神:如果不须要 ReentrantLockReentrantReadWriteLock 所提供的高级同步个性,应该优先思考应用 synchronized。理由如下:

  • Java 1.6 当前,synchronized 做了大量的优化,其性能曾经与 ReentrantLockReentrantReadWriteLock 基本上持平。
  • 从趋势来看,Java 将来更可能会优化 synchronized,而不是 ReentrantLockReentrantReadWriteLock,因为 synchronized 是 JVM 内置属性,它能执行一些优化。
  • ReentrantLockReentrantReadWriteLock 申请和开释锁都是由程序控制,如果使用不当,可能造成死锁,这是很危险的。

以下比照一下显示锁和内置锁的差别:

  • 被动获取锁和开释锁

    • synchronized 不能被动获取锁和开释锁。获取锁和开释锁都是 JVM 管制的。
    • ReentrantLock 能够被动获取锁和开释锁。(如果遗记开释锁,就可能产生死锁)。
  • 响应中断

    • synchronized 不能响应中断。
    • ReentrantLock 能够响应中断。
  • 超时机制

    • synchronized 没有超时机制。
    • ReentrantLock 有超时机制。ReentrantLock 能够设置超时工夫,超时后主动开释锁,防止始终期待。
  • 反对偏心锁

    • synchronized 只反对非偏心锁。
    • ReentrantLock 反对非偏心锁和偏心锁。
  • 是否反对共享

    • synchronized 润饰的办法或代码块,只能被一个线程拜访(独享)。如果这个线程被阻塞,其余线程也只能期待
    • ReentrantLock 能够基于 Condition 灵便的管制同步条件。
  • 是否反对读写拆散

    • synchronized 不反对读写锁拆散;
    • ReentrantReadWriteLock 反对读写锁,从而使阻塞读写的操作离开,无效进步并发性。

2. Lock 和 Condition

2.1. 为何引入 Lock 和 Condition

并发编程畛域,有两大外围问题:一个是 互斥 ,即同一时刻只容许一个线程访问共享资源;另一个是 同步,即线程之间如何通信、合作。这两大问题,管程都是可能解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题

synchronized 是管程的一种实现,既然如此,何必再提供 Lock 和 Condition。

JDK 1.6 以前,synchronized 还没有做优化,性能远低于 Lock。然而,性能不是引入 Lock 的最重要因素。真正关键在于:synchronized 使用不当,可能会呈现死锁。

synchronized 无奈通过 毁坏不可抢占条件 来防止死锁。起因是 synchronized 申请资源的时候,如果申请不到,线程间接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也开释不了线程曾经占有的资源。

与内置锁 synchronized 不同的是,Lock 提供了一组无条件的、可轮询的、定时的以及可中断的锁操作,所有获取锁、开释锁的操作都是显式的操作。

  • 可能响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦产生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程可能响应中断信号,也就是说当咱们给阻塞的线程发送中断信号的时候,可能唤醒它,那它就有机会开释已经持有的锁 A。这样就毁坏了不可抢占条件了。
  • 反对超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个谬误,那这个线程也有机会开释已经持有的锁。这样也能毁坏不可抢占条件。
  • 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是间接返回,那这个线程也有机会开释已经持有的锁。这样也能毁坏不可抢占条件。

2.2. 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() – 获取锁。
  • unlock() – 开释锁。
  • tryLock() – 尝试获取锁,仅在调用时锁未被另一个线程持有的状况下,才获取该锁。
  • tryLock(long time, TimeUnit unit) – 和 tryLock() 相似,区别仅在于限定工夫,如果限定工夫内未获取到锁,视为失败。
  • lockInterruptibly() – 锁未被另一个线程持有,且线程没有被中断的状况下,能力获取锁。
  • newCondition() – 返回一个绑定到 Lock 对象上的 Condition 实例。

2.3. Condition

Condition 实现了管程模型外面的条件变量

前文中提过 Lock 接口中 有一个 newCondition() 办法用于返回一个绑定到 Lock 对象上的 Condition 实例。Condition 是什么?有什么作用?本节将一一解说。

在单线程中,一段代码的执行可能依赖于某个状态,如果不满足状态条件,代码就不会被执行(典型的场景,如:if ... else ...)。在并发环境中,当一个线程判断某个状态条件时,其状态可能是因为其余线程的操作而扭转,这时就须要有肯定的协调机制来确保在同一时刻,数据只能被一个线程锁批改,且批改的数据状态被所有线程所感知。

Java 1.5 之前,次要是利用 Object 类中的 waitnotifynotifyAll 配合 synchronized 来进行线程间通信。

waitnotifynotifyAll 须要配合 synchronized 应用,不适用于 Lock。而应用 Lock 的线程,彼此间通信应该应用 Condition。这能够了解为,什么样的锁配什么样的钥匙。内置锁(synchronized)配合内置条件队列(waitnotifynotifyAll),显式锁(Lock)配合显式条件队列(Condition

Condition 的个性

Condition 接口定义如下:

public interface Condition {void await() throws InterruptedException;
    void awaitUninterruptibly();
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    boolean awaitUntil(Date deadline) throws InterruptedException;
    void signal();
    void signalAll();}

其中,awaitsignalsignalAllwaitnotifynotifyAll 绝对应,性能也类似。除此以外,Condition 相比内置条件队列(waitnotifynotifyAll),提供了更为丰盛的性能:

  • 每个锁(Lock)上能够存在多个 Condition,这意味着锁的状态条件能够有多个。
  • 反对偏心的或非偏心的队列操作。
  • 反对可中断的条件期待,相干办法:awaitUninterruptibly()
  • 反对可定时的期待,相干办法:awaitNanos(long)await(long, TimeUnit)awaitUntil(Date)
Condition 的用法

这里以 Condition 来实现一个消费者、生产者模式。

产品类

class Message {private final Lock lock = new ReentrantLock();

    private final Condition producedMsg = lock.newCondition();

    private final Condition consumedMsg = lock.newCondition();

    private String message;

    private boolean state;

    private boolean end;

    public void consume() {
        //lock
        lock.lock();
        try {
            // no new message wait for new message
            while (!state) {producedMsg.await(); }

            System.out.println("consume message :" + message);
            state = false;
            // message consumed, notify waiting thread
            consumedMsg.signal();} catch (InterruptedException ie) {System.out.println("Thread interrupted - viewMessage");
        } finally {lock.unlock();
        }
    }

    public void produce(String message) {lock.lock();
        try {
            // last message not consumed, wait for it be consumed
            while (state) {consumedMsg.await(); }

            System.out.println("produce msg:" + message);
            this.message = message;
            state = true;
            // new message added, notify waiting thread
            producedMsg.signal();} catch (InterruptedException ie) {System.out.println("Thread interrupted - publishMessage");
        } finally {lock.unlock();
        }
    }

    public boolean isEnd() {return end;}

    public void setEnd(boolean end) {this.end = end;}

}

消费者

class MessageConsumer implements Runnable {

    private Message message;

    public MessageConsumer(Message msg) {message = msg;}

    @Override
    public void run() {while (!message.isEnd()) {message.consume(); }
    }

}

生产者

class MessageProducer implements Runnable {

    private Message message;

    public MessageProducer(Message msg) {message = msg;}

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

    public void produce() {List<String> msgs = new ArrayList<>();
        msgs.add("Begin");
        msgs.add("Msg1");
        msgs.add("Msg2");

        for (String msg : msgs) {message.produce(msg);
            try {Thread.sleep(100);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }

        message.produce("End");
        message.setEnd(true);
    }

}

测试

public class LockConditionDemo {public static void main(String[] args) {Message msg = new Message();
        Thread producer = new Thread(new MessageProducer(msg));
        Thread consumer = new Thread(new MessageConsumer(msg));
        producer.start();
        consumer.start();}
}

3. ReentrantLock

ReentrantLock 类是 Lock 接口的具体实现,与内置锁 synchronized 雷同的是,它是一个 可重入锁

3.1. ReentrantLock 的个性

ReentrantLock 的个性如下:

  • ReentrantLock 提供了与 synchronized 雷同的互斥性、内存可见性和可重入性
  • ReentrantLock 反对偏心锁和非偏心锁(默认)两种模式。
  • ReentrantLock 实现了 Lock 接口,反对了 synchronized 所不具备的 灵活性

    • synchronized 无奈中断一个正在期待获取锁的线程
    • synchronized 无奈在申请获取一个锁时无休止地期待

3.2. ReentrantLock 的用法

前文理解了 ReentrantLock 的个性,接下来,咱们要讲述其具体用法。

ReentrantLock 的构造方法

ReentrantLock 有两个构造方法:

public ReentrantLock() {}
public ReentrantLock(boolean fair) {}
  • ReentrantLock() – 默认构造方法会初始化一个 非偏心锁(NonfairSync)
  • ReentrantLock(boolean)new ReentrantLock(true) 会初始化一个 偏心锁(FairSync)
lock 和 unlock 办法
  • lock()无条件获取锁。如果以后线程无奈获取锁,则以后线程进入休眠状态不可用,直至以后线程获取到锁。如果该锁没有被另一个线程持有,则获取该锁并立刻返回,将锁的持有计数设置为 1。
  • unlock() – 用于 开释锁

留神:请务必牢记,获取锁操作 lock() 必须在 try catch 块外进行,并且将开释锁操作 unlock() 放在 finally 块中进行,以保障锁肯定被被开释,避免死锁的产生

示例:ReentrantLock 的基本操作

public class ReentrantLockDemo {public static void main(String[] args) {Task task = new Task();
        MyThread tA = new MyThread("Thread-A", task);
        MyThread tB = new MyThread("Thread-B", task);
        MyThread tC = new MyThread("Thread-C", task);
        tA.start();
        tB.start();
        tC.start();}

    static class MyThread extends Thread {

        private Task task;

        public MyThread(String name, Task task) {super(name);
            this.task = task;
        }

        @Override
        public void run() {task.execute();
        }

    }

    static class Task {private ReentrantLock lock = new ReentrantLock();

        public void execute() {lock.lock();
            try {for (int i = 0; i < 3; i++) {System.out.println(lock.toString());

                    // 查问以后线程 hold 住此锁的次数
                    System.out.println("\t holdCount:" + lock.getHoldCount());

                    // 查问正等待获取此锁的线程数
                    System.out.println("\t queuedLength:" + lock.getQueueLength());

                    // 是否为偏心锁
                    System.out.println("\t isFair:" + lock.isFair());

                    // 是否被锁住
                    System.out.println("\t isLocked:" + lock.isLocked());

                    // 是否被以后线程持有锁
                    System.out.println("\t isHeldByCurrentThread:" + lock.isHeldByCurrentThread());

                    try {Thread.sleep(500);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            } finally {lock.unlock();
            }
        }

    }

}

输入后果:

java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-A]
     holdCount: 1
     queuedLength: 2
     isFair: false
     isLocked: true
     isHeldByCurrentThread: true
java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-C]
     holdCount: 1
     queuedLength: 1
     isFair: false
     isLocked: true
     isHeldByCurrentThread: true
// ...
tryLock 办法

与无条件获取锁相比,tryLock 有更欠缺的容错机制。

  • tryLock()可轮询获取锁 。如果胜利,则返回 true;如果失败,则返回 false。也就是说,这个办法 无论成败都会立刻返回,获取不到锁(锁已被其余线程获取)时不会始终期待。
  • tryLock(long, TimeUnit)可定时获取锁 。和 tryLock() 相似,区别仅在于这个办法在 获取不到锁时会期待肯定的工夫,在工夫期限之内如果还获取不到锁,就返回 false。如果如果一开始拿到锁或者在期待期间内拿到了锁,则返回 true。

示例:ReentrantLocktryLock() 操作

批改上个示例中的 execute() 办法

public void execute() {if (lock.tryLock()) {
        try {for (int i = 0; i < 3; i++) {// 略...}
        } finally {lock.unlock();
        }
    } else {System.out.println(Thread.currentThread().getName() + "获取锁失败");
    }
}

示例:ReentrantLocktryLock(long, TimeUnit) 操作

批改上个示例中的 execute() 办法

public void execute() {
    try {if (lock.tryLock(2, TimeUnit.SECONDS)) {
            try {for (int i = 0; i < 3; i++) {// 略...}
            } finally {lock.unlock();
            }
        } else {System.out.println(Thread.currentThread().getName() + "获取锁失败");
        }
    } catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + "获取锁超时");
        e.printStackTrace();}
}
lockInterruptibly 办法
  • lockInterruptibly()可中断获取锁。可中断获取锁能够在取得锁的同时放弃对中断的响应。可中断获取锁比其它获取锁的形式略微简单一些,须要两个 try-catch 块(如果在获取锁的操作中抛出了 InterruptedException,那么能够应用规范的 try-finally 加锁模式)。

    • 举例来说:假如有两个线程同时通过 lock.lockInterruptibly() 获取某个锁时,若线程 A 获取到了锁,则线程 B 只能期待。若此时对线程 B 调用 threadB.interrupt() 办法可能中断线程 B 的期待过程。因为 lockInterruptibly() 的申明中抛出了异样,所以 lock.lockInterruptibly() 必须放在 try 块中或者在调用 lockInterruptibly() 的办法外申明抛出 InterruptedException

留神:当一个线程获取了锁之后,是不会被 interrupt() 办法中断的。独自调用 interrupt() 办法不能中断正在运行状态中的线程,只能中断阻塞状态中的线程。因而当通过 lockInterruptibly() 办法获取某个锁时,如果未获取到锁,只有在期待的状态下,才能够响应中断。

示例:ReentrantLocklockInterruptibly() 操作

批改上个示例中的 execute() 办法

public void execute() {
    try {lock.lockInterruptibly();

        for (int i = 0; i < 3; i++) {// 略...}
    } catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + "被中断");
        e.printStackTrace();} finally {lock.unlock();
    }
}
newCondition 办法

newCondition() – 返回一个绑定到 Lock 对象上的 Condition 实例。

3.3. ReentrantLock 的原理

ReentrantLock 的可见性
class X {
  private final Lock rtl =
  new ReentrantLock();
  int value;
  public void addOne() {
    // 获取锁
    rtl.lock();
    try {value+=1;} finally {
      // 保障锁能开释
      rtl.unlock();}
  }
}

ReentrantLock,外部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如上面所示)。也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。依据相干的 Happens-Before 规定:

  1. 程序性规定:对于线程 T1,value+=1 Happens-Before 开释锁的操作 unlock();
  2. volatile 变量规定:因为 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;
  3. 传递性规定:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操作。
ReentrantLock 的数据结构

浏览 ReentrantLock 的源码,能够发现它有一个外围字段:

private final Sync sync;
  • sync – 外部抽象类 ReentrantLock.Sync 对象,Sync 继承自 AQS。它有两个子类:
  • ReentrantLock.FairSync – 偏心锁。
  • ReentrantLock.NonfairSync – 非偏心锁。

查看源码能够发现,ReentrantLock 实现 Lock 接口其实是调用 ReentrantLock.FairSyncReentrantLock.NonfairSync 中各自的实现,这里不一一列举。

ReentrantLock 的获取锁和开释锁

ReentrantLock 获取锁和开释锁的接口,从表象看,是调用 ReentrantLock.FairSyncReentrantLock.NonfairSync 中各自的实现;从实质上看,是基于 AQS 的实现。

仔细阅读源码很容易发现:

  • void lock() 调用 Sync 的 lock() 办法。
  • void lockInterruptibly() 间接调用 AQS 的 获取可中断的独占锁 办法 lockInterruptibly()
  • boolean tryLock() 调用 Sync 的 nonfairTryAcquire()
  • boolean tryLock(long time, TimeUnit unit) 间接调用 AQS 的 获取超时期待式的独占锁 办法 tryAcquireNanos(int arg, long nanosTimeout)
  • void unlock() 间接调用 AQS 的 开释独占锁 办法 release(int arg)

间接调用 AQS 接口的办法就不再赘述了,其原理在 AQS 的原理 中曾经用很大篇幅进行过解说。

nonfairTryAcquire 办法源码如下:

// 偏心锁和非偏心锁都会用这个办法区尝试获取锁
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {if (compareAndSetState(0, acquires)) {
         // 如果同步状态为 0,将其设为 acquires,并设置以后线程为排它线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

解决流程很简略:

  • 如果同步状态为 0,设置同步状态设为 acquires,并设置以后线程为排它线程,而后返回 true,获取锁胜利。
  • 如果同步状态不为 0 且以后线程为排它线程,设置同步状态为以后状态值 +acquires 值,而后返回 true,获取锁胜利。
  • 否则,返回 false,获取锁失败。
偏心锁和非偏心锁

ReentrantLock 这个类有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的偏心策略,如果传入 true 就示意须要结构一个偏心锁,反之则示意要结构一个非偏心锁。

锁都对应着一个期待队列,如果一个线程没有取得锁,就会进入期待队列,当有线程开释锁的时候,就须要从期待队列中唤醒一个期待的线程。如果是偏心锁,唤醒的策略就是谁期待的工夫长,就唤醒谁,很偏心;如果是非偏心锁,则不提供这个偏心保障,有可能等待时间短的线程反而先被唤醒。

lock 办法在偏心锁和非偏心锁中的实现:

二者的区别仅在于申请非偏心锁时,如果同步状态为 0,尝试将其设为 1,如果胜利,间接将以后线程置为排它线程;否则和偏心锁一样,调用 AQS 获取独占锁办法 acquire

// 非偏心锁实现
final void lock() {if (compareAndSetState(0, 1))
    // 如果同步状态为 0,将其设为 1,并设置以后线程为排它线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
    // 调用 AQS 获取独占锁办法 acquire
        acquire(1);
}

// 偏心锁实现
final void lock() {
    // 调用 AQS 获取独占锁办法 acquire
    acquire(1);
}

4. ReentrantReadWriteLock

ReadWriteLock 实用于 读多写少的场景

ReentrantReadWriteLock 类是 ReadWriteLock 接口的具体实现,它是一个可重入的读写锁。ReentrantReadWriteLock 保护了一对读写锁,将读写锁离开,有利于进步并发效率。

读写锁,并不是 Java 语言特有的,而是一个广为应用的通用技术,所有的读写锁都恪守以下三条根本准则:

  • 容许多个线程同时读共享变量;
  • 只容许一个线程写共享变量;
  • 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。

读写锁与互斥锁的一个重要区别就是 读写锁容许多个线程同时读共享变量 ,而互斥锁是不容许的,这是读写锁在读多写少场景下性能优于互斥锁的要害。但 读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不容许其余线程执行写操作和读操作。

4.1. ReentrantReadWriteLock 的个性

ReentrantReadWriteLock 的个性如下:

  • ReentrantReadWriteLock 实用于读多写少的场景。如果是写多读少的场景,因为 ReentrantReadWriteLock 其外部实现比 ReentrantLock 简单,性能可能反而要差一些。如果存在这样的问题,须要具体问题具体分析。因为 ReentrantReadWriteLock 的读写锁(ReadLockWriteLock)都实现了 Lock 接口,所以要替换为 ReentrantLock 也较为容易。
  • ReentrantReadWriteLock 实现了 ReadWriteLock 接口,反对了 ReentrantLock 所不具备的读写锁拆散。ReentrantReadWriteLock 保护了一对读写锁(ReadLockWriteLock)。将读写锁离开,有利于进步并发效率。ReentrantReadWriteLock 的加锁策略是:容许多个读操作并发执行,但每次只容许一个写操作
  • ReentrantReadWriteLock 为读写锁都提供了可重入的加锁语义。
  • ReentrantReadWriteLock 反对偏心锁和非偏心锁(默认)两种模式。

ReadWriteLock 接口定义如下:

public interface ReadWriteLock {Lock readLock();
    Lock writeLock();}
  • readLock – 返回用于读操作的锁(ReadLock)。
  • writeLock – 返回用于写操作的锁(WriteLock)。

在读写锁和写入锁之间的交互能够采纳多种实现形式,ReadWriteLock 的一些可选实现包含:

  • 开释优先 – 当一个写入操作开释写锁,并且队列中同时存在读线程和写线程,那么应该优先选择读线程、写线程,还是最先发出请求的线程?
  • 读线程插队 – 如果锁是由读线程持有,但有写线程正在期待,那么新达到的读线程是否立刻取得拜访权,还是应该在写线程前面期待?如果容许读线程插队到写线程之前,那么将进步并发性,但可能造成线程饥饿问题。
  • 重入性 – 读锁和写锁是否是可重入的?
  • 降级 – 如果一个线程持有写入锁,那么它是否在不开释该锁的状况下取得读锁?这可能会使得写锁被降级为读锁,同时不容许其余写线程批改被爱护的资源。
  • 降级 – 读锁是否优先于其余正在期待的读线程和写线程而降级为一个写锁?在大多数的读写锁实现中并不反对降级,因为如果没有显式的降级操作,那么很容易造成死锁。

4.2. ReentrantReadWriteLock 的用法

前文理解了 ReentrantReadWriteLock 的个性,接下来,咱们要讲述其具体用法。

ReentrantReadWriteLock 的构造方法

ReentrantReadWriteLockReentrantLock 一样,也有两个构造方法,且用法类似。

public ReentrantReadWriteLock() {}
public ReentrantReadWriteLock(boolean fair) {}
  • ReentrantReadWriteLock() – 默认构造方法会初始化一个 非偏心锁(NonfairSync)。在非偏心的锁中,线程取得锁的程序是不确定的。写线程降级为读线程是能够的,但读线程降级为写线程是不能够的(这样会导致死锁)。
  • ReentrantReadWriteLock(boolean)new ReentrantLock(true) 会初始化一个 偏心锁(FairSync)。对于偏心锁,等待时间最长的线程将优先取得锁。如果这个锁是读线程持有,则另一个线程申请写锁,那么其余读线程都不能取得读锁,直到写线程开释写锁。
ReentrantReadWriteLock 的应用实例

ReentrantReadWriteLock 的个性 中曾经介绍过,ReentrantReadWriteLock 的读写锁(ReadLockWriteLock)都实现了 Lock 接口,所以其各自独立的应用形式与 ReentrantLock 一样,这里不再赘述。

ReentrantReadWriteLockReentrantLock 用法上的差别,次要在于读写锁的配合应用。本文以一个典型应用场景来进行解说。

【示例】基于 ReadWriteLock 实现一个简略的泛型无界缓存

/**
 * 简略的无界缓存实现
 * <p>
 * 应用 WeakHashMap 存储键值对。WeakHashMap 中存储的对象是弱援用,JVM GC 时会主动革除没有被援用的弱援用对象。*/
static class UnboundedCache<K, V> {private final Map<K, V> cacheMap = new WeakHashMap<>();

    private final ReadWriteLock cacheLock = new ReentrantReadWriteLock();

    public V get(K key) {cacheLock.readLock().lock();
        V value;
        try {value = cacheMap.get(key);
            String log = String.format("%s 读数据 %s:%s", Thread.currentThread().getName(), key, value);
            System.out.println(log);
        } finally {cacheLock.readLock().unlock();}
        return value;
    }

    public V put(K key, V value) {cacheLock.writeLock().lock();
        try {cacheMap.put(key, value);
            String log = String.format("%s 写入数据 %s:%s", Thread.currentThread().getName(), key, value);
            System.out.println(log);
        } finally {cacheLock.writeLock().unlock();}
        return value;
    }

    public V remove(K key) {cacheLock.writeLock().lock();
        try {return cacheMap.remove(key);
        } finally {cacheLock.writeLock().unlock();}
    }

    public void clear() {cacheLock.writeLock().lock();
        try {this.cacheMap.clear();
        } finally {cacheLock.writeLock().unlock();}
    }

}

阐明:

  • 应用 WeakHashMap 而不是 HashMap 来存储键值对。WeakHashMap 中存储的对象是弱援用,JVM GC 时会主动革除没有被援用的弱援用对象。
  • Map 写数据前加写锁,写完后,开释写锁。
  • Map 读数据前加读锁,读完后,开释读锁。

测试其线程安全性:

/**
 * @author <a href="mailto:forbreak@163.com">Zhang Peng</a>
 * @since 2020-01-01
 */
public class ReentrantReadWriteLockDemo {static UnboundedCache<Integer, Integer> cache = new UnboundedCache<>();

    public static void main(String[] args) {ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 20; i++) {executorService.execute(new MyThread());
            cache.get(0);
        }
        executorService.shutdown();}

    /** 线程工作每次向缓存中写入 3 个随机值,key 固定 */
    static class MyThread implements Runnable {

        @Override
        public void run() {Random random = new Random();
            for (int i = 0; i < 3; i++) {cache.put(i, random.nextInt(100));
            }
        }

    }

}

阐明:示例中,通过线程池启动 20 个并发工作。工作每次向缓存中写入 3 个随机值,key 固定;而后主线程每次固定读取缓存中第一个 key 的值。

输入后果:

main 读数据 0:null
pool-1-thread-1 写入数据 0:16
pool-1-thread-1 写入数据 1:58
pool-1-thread-1 写入数据 2:50
main 读数据 0:16
pool-1-thread-1 写入数据 0:85
pool-1-thread-1 写入数据 1:76
pool-1-thread-1 写入数据 2:46
pool-1-thread-2 写入数据 0:21
pool-1-thread-2 写入数据 1:41
pool-1-thread-2 写入数据 2:63
main 读数据 0:21
main 读数据 0:21
// ...

4.3. ReentrantReadWriteLock 的原理

后面理解了 ReentrantLock 的原理,了解 ReentrantReadWriteLock 就容易多了。

ReentrantReadWriteLock 的数据结构

浏览 ReentrantReadWriteLock 的源码,能够发现它有三个外围字段:

/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock;}
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock;}
  • sync – 外部类 ReentrantReadWriteLock.Sync 对象。与 ReentrantLock 相似,它有两个子类:ReentrantReadWriteLock.FairSyncReentrantReadWriteLock.NonfairSync,别离示意偏心锁和非偏心锁的实现。
  • readerLock – 外部类 ReentrantReadWriteLock.ReadLock 对象,这是一把读锁。
  • writerLock – 外部类 ReentrantReadWriteLock.WriteLock 对象,这是一把写锁。
ReentrantReadWriteLock 的获取锁和开释锁
public static class ReadLock implements Lock, java.io.Serializable {

    // 调用 AQS 获取共享锁办法
    public void lock() {sync.acquireShared(1);
    }

    // 调用 AQS 开释共享锁办法
    public void unlock() {sync.releaseShared(1);
    }
}

public static class WriteLock implements Lock, java.io.Serializable {

    // 调用 AQS 获取独占锁办法
    public void lock() {sync.acquire(1);
    }

    // 调用 AQS 开释独占锁办法
    public void unlock() {sync.release(1);
    }
}

5. StampedLock

ReadWriteLock 反对两种模式:一种是读锁,一种是写锁。而 StampedLock 反对三种模式,别离是:写锁 乐观读锁 乐观读。其中,写锁、乐观读锁的语义和 ReadWriteLock 的写锁、读锁的语义十分相似,容许多个线程同时获取乐观读锁,然而只容许一个线程获取写锁,写锁和乐观读锁是互斥的。不同的是:StampedLock 里的写锁和乐观读锁加锁胜利之后,都会返回一个 stamp;而后解锁的时候,须要传入这个 stamp。

留神这里,用的是“乐观读”这个词,而不是“乐观读锁”,是要揭示你,乐观读这个操作是无锁的,所以相比拟 ReadWriteLock 的读锁,乐观读的性能更好一些。

StampedLock 的性能之所以比 ReadWriteLock 还要好,其要害是 StampedLock 反对乐观读 的形式。

  • ReadWriteLock 反对多个线程同时读,然而当多个线程同时读的时候,所有的写操作会被阻塞;
  • 而 StampedLock 提供的乐观读,是容许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。

对于读多写少的场景 StampedLock 性能很好,简略的利用场景基本上能够代替 ReadWriteLock,然而StampedLock 的性能仅仅是 ReadWriteLock 的子集,在应用的时候,还是有几个中央须要留神一下。

  • StampedLock 不反对重入
  • StampedLock 的乐观读锁、写锁都不反对条件变量。
  • 如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 办法,会导致 CPU 飙升。应用 StampedLock 肯定不要调用中断操作,如果须要反对中断性能,肯定应用可中断的乐观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()

【示例】StampedLock 阻塞时,调用 interrupt() 导致 CPU 飙升

final StampedLock lock
  = new StampedLock();
Thread T1 = new Thread(()->{
  // 获取写锁
  lock.writeLock();
  // 永远阻塞在此处,不开释写锁
  LockSupport.park();});
T1.start();
// 保障 T1 获取写锁
Thread.sleep(100);
Thread T2 = new Thread(()->
  // 阻塞在乐观读锁
  lock.readLock());
T2.start();
// 保障 T2 阻塞在读锁
Thread.sleep(100);
// 中断线程 T2
// 会导致线程 T2 所在 CPU 飙升
T2.interrupt();
T2.join();

【示例】StampedLock 读模板:

final StampedLock sl =
  new StampedLock();

// 乐观读
long stamp =
  sl.tryOptimisticRead();
// 读入办法局部变量
......
// 校验 stamp
if (!sl.validate(stamp)){
  // 降级为乐观读锁
  stamp = sl.readLock();
  try {
    // 读入办法局部变量
    .....
  } finally {
    // 开释乐观读锁
    sl.unlockRead(stamp);
  }
}
// 应用办法局部变量执行业务操作
......

【示例】StampedLock 写模板:

long stamp = sl.writeLock();
try {
  // 写共享变量
  ......
} finally {sl.unlockWrite(stamp);
}

6. AQS

AbstractQueuedSynchronizer(简称 AQS)是 队列同步器,顾名思义,其次要作用是解决同步。它是并发锁和很多同步工具类的实现基石(如 ReentrantLockReentrantReadWriteLockCountDownLatchSemaphoreFutureTask 等)。

6.1. AQS 的要点

AQS 提供了对独享锁与共享锁的反对

java.util.concurrent.locks 包中的相干锁(罕用的有 ReentrantLockReadWriteLock)都是基于 AQS 来实现。这些锁都没有间接继承 AQS,而是定义了一个 Sync 类去继承 AQS。为什么要这样呢?因为锁面向的是应用用户,而同步器面向的则是线程管制,那么在锁的实现中聚合同步器而不是间接继承 AQS 就能够很好的隔离二者所关注的事件。

6.2. AQS 的利用

AQS 提供了对独享锁与共享锁的反对

独享锁 API

获取、开释独享锁的次要 API 如下:

public final void acquire(int arg)
public final void acquireInterruptibly(int arg)
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
public final boolean release(int arg)
  • acquire – 获取独占锁。
  • acquireInterruptibly – 获取可中断的独占锁。
  • tryAcquireNanos – 尝试在指定工夫内获取可中断的独占锁。在以下三种状况下回返回:

    • 在超时工夫内,以后线程胜利获取了锁;
    • 以后线程在超时工夫内被中断;
    • 超时工夫完结,仍未取得锁返回 false。
  • release – 开释独占锁。
共享锁 API

获取、开释共享锁的次要 API 如下:

public final void acquireShared(int arg)
public final void acquireSharedInterruptibly(int arg)
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
public final boolean releaseShared(int arg)
  • acquireShared – 获取共享锁。
  • acquireSharedInterruptibly – 获取可中断的共享锁。
  • tryAcquireSharedNanos – 尝试在指定工夫内获取可中断的共享锁。
  • release – 开释共享锁。

6.3. AQS 的原理

ASQ 原理要点:

  • AQS 应用一个整型的 volatile 变量来 保护同步状态。状态的意义由子类赋予。
  • AQS 保护了一个 FIFO 的双链表,用来存储获取锁失败的线程。

AQS 围绕同步状态提供两种基本操作“获取”和“开释”,并提供一系列判断和解决办法,简略说几点:

  • state 是独占的,还是共享的;
  • state 被获取后,其余线程须要期待;
  • state 被开释后,唤醒期待线程;
  • 线程等不及时,如何退出期待。

至于线程是否能够取得 state,如何开释 state,就不是 AQS 关怀的了,要由子类具体实现。

AQS 的数据结构

浏览 AQS 的源码,能够发现:AQS 继承自 AbstractOwnableSynchronize

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /** 期待队列的队头,懒加载。只能通过 setHead 办法批改。*/
    private transient volatile Node head;
    /** 期待队列的队尾,懒加载。只能通过 enq 办法增加新的期待节点。*/
    private transient volatile Node tail;
    /** 同步状态 */
    private volatile int state;
}
  • state – AQS 应用一个整型的 volatile 变量来 保护同步状态

    • 这个整数状态的意义由子类来赋予,如ReentrantLock 中该状态值示意所有者线程曾经反复获取该锁的次数,Semaphore 中该状态值示意残余的许可数量。
  • headtail – AQS 保护了一个 Node 类型(AQS 的外部类)的双链表来实现同步状态的治理 。这个双链表是一个双向的 FIFO 队列,通过 headtail 指针进行拜访。当 有线程获取锁失败后,就被增加到队列开端

再来看一下 Node 的源码

static final class Node {
    /** 该期待同步的节点处于共享模式 */
    static final Node SHARED = new Node();
    /** 该期待同步的节点处于独占模式 */
    static final Node EXCLUSIVE = null;

    /** 线程期待状态,状态值有: 0、1、-1、-2、-3 */
    volatile int waitStatus;
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    /** 前驱节点 */
    volatile Node prev;
    /** 后继节点 */
    volatile Node next;
    /** 期待锁的线程 */
    volatile Thread thread;

      /** 和节点是否共享无关 */
    Node nextWaiter;
}

很显然,Node 是一个双链表构造。

  • waitStatusNode 应用一个整型的 volatile 变量来 保护 AQS 同步队列中线程节点的状态。waitStatus 有五个状态值:

    • CANCELLED(1) – 此状态示意:该节点的线程可能因为超时或被中断而 处于被勾销 (作废) 状态,一旦处于这个状态,示意这个节点应该从期待队列中移除。
    • SIGNAL(-1) – 此状态示意:后继节点会被挂起 ,因而在以后节点开释锁或被勾销之后,必须唤醒(unparking) 其后继结点。
    • CONDITION(-2) – 此状态示意:该节点的线程 处于期待条件状态,不会被当作是同步队列上的节点,直到被唤醒(signal),设置其值为 0,再从新进入阻塞状态。
    • PROPAGATE(-3) – 此状态示意:下一个 acquireShared 应无条件流传。
    • 0 – 非以上状态。
独占锁的获取和开释
获取独占锁

AQS 中应用 acquire(int arg) 办法获取独占锁,其大抵流程如下:

  1. 先尝试获取同步状态,如果获取同步状态胜利,则完结办法,间接返回。
  2. 如果获取同步状态不胜利,AQS 会一直尝试利用 CAS 操作将以后线程插入期待同步队列的队尾,直到胜利为止。
  3. 接着,一直尝试为期待队列中的线程节点获取独占锁。

具体流程能够用下图来示意,请联合源码来了解(一图胜千言):

开释独占锁

AQS 中应用 release(int arg) 办法开释独占锁,其大抵流程如下:

  1. 先尝试获取解锁线程的同步状态,如果获取同步状态不胜利,则完结办法,间接返回。
  2. 如果获取同步状态胜利,AQS 会尝试唤醒以后线程节点的后继节点。
获取可中断的独占锁

AQS 中应用 acquireInterruptibly(int arg) 办法获取可中断的独占锁。

acquireInterruptibly(int arg) 实现形式 相较于获取独占锁办法(acquire)十分类似 ,区别仅在于它会 通过 Thread.interrupted 检测以后线程是否被中断,如果是,则立刻抛出中断异样(InterruptedException)。

获取超时期待式的独占锁

AQS 中应用 tryAcquireNanos(int arg) 办法获取超时期待的独占锁。

doAcquireNanos 的实现形式 相较于获取独占锁办法(acquire)十分类似,区别在于它会依据超时工夫和以后工夫计算出截止工夫。在获取锁的流程中,会一直判断是否超时,如果超时,间接返回 false;如果没超时,则用 LockSupport.parkNanos 来阻塞以后线程。

共享锁的获取和开释
获取共享锁

AQS 中应用 acquireShared(int arg) 办法获取共享锁。

acquireShared 办法和 acquire 办法的逻辑很类似,区别仅在于自旋的条件以及节点出队的操作有所不同。

胜利取得共享锁的条件如下:

  • tryAcquireShared(arg) 返回值大于等于 0(这意味着共享锁的 permit 还没有用完)。
  • 以后节点的前驱节点是头结点。
开释共享锁

AQS 中应用 releaseShared(int arg) 办法开释共享锁。

releaseShared 首先会尝试开释同步状态,如果胜利,则解锁一个或多个后继线程节点。开释共享锁和开释独享锁流程大体类似,区别在于:

对于独享模式,如果须要 SIGNAL,开释仅相当于调用头节点的 unparkSuccessor

获取可中断的共享锁

AQS 中应用 acquireSharedInterruptibly(int arg) 办法获取可中断的共享锁。

acquireSharedInterruptibly 办法与 acquireInterruptibly 简直统一,不再赘述。

获取超时期待式的共享锁

AQS 中应用 tryAcquireSharedNanos(int arg) 办法获取超时期待式的共享锁。

tryAcquireSharedNanos 办法与 tryAcquireNanos 简直统一,不再赘述。

7. 死锁

7.1. 什么是死锁

死锁是一种特定的程序状态,在实体之间,因为循环依赖导致彼此始终处于期待之中,没有任何个体能够继续前进。死锁不仅仅是在线程之间会产生,存在资源独占的过程之间同样也
可能呈现死锁。通常来说,咱们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,因为相互持有对方须要的锁,而永恒处于阻塞的状态。

7.2. 如何定位死锁

定位死锁最常见的形式就是利用 jstack 等工具获取线程栈,而后定位相互之间的依赖关系,进而找到死锁。如果是比拟显著的死锁,往往 jstack 等就能间接定位,相似 JConsole 甚至能够在图形界面进行无限的死锁检测。

如果咱们是开发本人的管理工具,须要用更加程序化的形式扫描服务过程、定位死锁,能够思考应用 Java 提供的规范治理 API,ThreadMXBean,其间接就提供了 findDeadlockedThreads() 办法用于定位。

7.3. 如何防止死锁

基本上死锁的产生是因为:

  • 互斥,相似 Java 中 Monitor 都是独占的。
  • 长期保持互斥,在应用完结之前,不会开释,也不能被其余线程抢占。
  • 循环依赖,多个个体之间呈现了锁的循环依赖,彼此依赖上一环开释锁。

由此,咱们能够剖析出防止死锁的思路和办法。

(1)防止一个线程同时获取多个锁。

防止一个线程在锁内同时占用多个资源,尽量保障每个锁只占用一个资源。

尝试应用定时锁 lock.tryLock(timeout),防止锁始终不能开释。

对于数据库锁,加锁和解锁必须在一个数据库连贯中里,否则会呈现解锁失败的状况。

8. 参考资料

  • 《Java 并发编程实战》
  • 《Java 并发编程的艺术》
  • Java 并发编程:Lock
  • 深刻学习 java 同步器 AQS
  • AbstractQueuedSynchronizer 框架

关注公众号:java 宝典

正文完
 0