Java多线程并发07——锁在Java中的实现

上一篇文章中,我们已经先容过了种种锁,让列位对锁有了一定的领会。接下来将为列位先容锁在Java中的实现。关注我的民众号「Java面典」领会更多 Java 相关知识点。

在 Java 中主要通过使用synchronized 、 volatile关键字,及 Lock 接口的子类 ReentrantLock 和 ReadWriteLock 等来实现加锁。

synchronized

属性

synchronized 属于独占式的消极锁,同时属于可重入锁。

作用

synchronized 可以把随便一个非 NULL 的工具看成锁。其在差别场景下的作用局限如下:

  1. 作用于方式时,锁住的是工具的实例(this)
  2. 作用于静态方式时,锁住的是Class实例,会锁住所有挪用该方式的线程。(又由于Class的相关数据存储在永远代 PermGen【Jdk1.8 则是 metaspace】,永远代是全局共享的,因此静态方式锁相当于类的一个全局锁);
  3. 作用于一个工具实例时,锁住的是所有以该工具为锁的代码块。

实现

它有多个行列,当多个线程一起接见某个工具监视器的时刻,工具监视器会将这些线程存储在差别的容器中。

Java多线程并发07——锁在Java中的实现

  1. Wait Set:存储挪用 wait 方式被壅闭的线程;
  2. Contention List(竞争行列):所有请求锁的线程首先被放在这个竞争行列中;
  3. Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
  4. OnDeck:随便时刻,最多只有一个线程正在竞争锁资源,该线程成为 OnDeck;
  5. Owner:当前已经获取到所资源的线程被称为 Owner;
  6. !Owner:当前释放锁的线程。

参考资料

volatile

属性

比 sychronized 更轻量级的同步锁

适用场景

使用 volatile 必须同时知足下面两个条件才气保证在并发环境的线程平安:

  1. 对变量的写操作不依赖于当前值(好比 i++),或者说是单纯的变量赋值(boolean flag = true);
  2. 差别的 volatile 变量之间,不能相互依赖,只有在状态真正独立于程序内其他内容时才气使用 volatile。

对 volatile 变量的单次读/写操作可以保证原子性的,如 long 和 double 类型变量,然则并不能保证 i++ 这种操作的原子性,由于本质上 i++ 是读、写两次操作。

Lock

Java 中的锁都实现于 Lock 接口,主要方式有:

  1. void lock(): 用于获取锁。若是锁可用,则获取锁。 若锁不可用, 将禁用当前线程,直到取到锁;
  2. boolean tryLock():实验获取锁。若是锁可用,则获取锁,并返回 true, 否则返回 false;
    该方式和lock()的区别在于,若是锁不可用,tryLock()不会导致当前线程被禁用。
  3. tryLock(long timeout TimeUnit unit):若是锁在给定守候时间内没有被另一个线程保持,则获取该锁
  4. void unlock():释放锁。锁只能由持有者释放,若是线程并不持有锁,却执行该方式。可能导致异常的发生;
  5. Condition newCondition():条件工具,获取守候通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才气挪用该组件的 await()方式,而挪用后,当前线程将缩放锁;
  6. getHoldCount() :查询当前线程保持此锁的次数
  7. getQueueLength():返回正守候获取此锁的线程估计数。好比启动 10 个线程,1 个线程获得锁,此时返回的是 9;
  8. getWaitQueueLength:(Condition condition)返回守候与此锁相关的给定条件的线程估计数。好比 10 个线程,用同一个 condition 工具,而且此时这 10 个线程都执行了condition 工具的 await 方式,那么此时执行此方式返回 10;
  9. hasWaiters(Condition condition):查询是否有线程守候与此锁有关的给定条件(condition)。对于指定 contidion 工具,有若干线程执行了 condition.await 方式;
  10. hasQueuedThread(Thread thread):查询给定线程是否守候获取此锁
  11. hasQueuedThreads():是否有线程守候此锁
  12. isFair():该锁是否公正锁
  13. isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方式的前后划分是 false 和 true;
  14. isLock():此锁是否有随便线程占用
  15. lockInterruptibly():若是当前线程未被中止,获取锁

tryLock 和 lock 和 lockInterruptibly 的区别

  1. tryLock 能获得锁就返回 true,不能就立刻返回 false,tryLock(long timeout,TimeUnit unit),可以增添时间限制,若是跨越该时间段还没获得锁,返回 false;
  2. lock 能获得锁就返回 true,不能的话一直守候获得锁;
  3. lock 和 lockInterruptibly,若是两个线程划分执行这两个方式,但此时中止这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常。

Condition

作用

Condition 的作用是对锁举行更正确的控制。对于同一个锁,我们可以建立多个 Condition,在差别的情形下使用差别的 Condition。

Condition 和 Object

  • 相似之处
  1. Condition 类的 awiat 方式和 Object 类的 wait 方式等效;
  2. Condition 类的 signal 方式和 Object 类的 notify 方式等效;
  3. Condition 类的 signalAll 方式和 Object 类的 notifyAll 方式等效。
  • 差别处
  1. ReentrantLock 类可以叫醒指定条件的线程,而 object 的叫醒是随机的;
  2. Object中的 wait()、notify()、notifyAll() 方式是和 “同步锁”(synchronized关键字) 捆绑使用的;而Condition是需要与 “互斥锁”/”共享锁” 捆绑使用的。

ReentrantLock

属性

可重入锁。

特点

除了能完成 synchronized 所能完成的所有事情外,还提供了诸如可响应中止锁、可轮询锁请求、准时锁等制止多线程死锁的方式。

JS排序算法–冒泡排序和选择排序

与 synchronized 的区别

  1. ReentrantLock 需要通过方式 lock() 与 unlock() 手动举行加锁与解锁操作,而 synchronized 会 被 JVM 自动加锁、解锁;
  2. ReentrantLock 相比 synchronized 的优势是可中止、公正锁、多个锁。
public class MyLock {

    private Lock lock = new ReentrantLock();
//    Lock lock = new ReentrantLock(true); //公正锁
//    Lock lock = new ReentrantLock(false); //非公正锁
    private Condition condition = lock.newCondition(); //建立 Condition

    public void testMethod() {
        try {
            lock.lock(); //lock 加锁
            // 1:wait 方式守候:
            //System.out.println("最先 wait");
            condition.await();
            // 通过建立 Condition 工具来使线程 wait,必须先执行 lock.lock 方式获得锁
            // 2:signal 方式叫醒
            condition.signal(); //condition 工具的 signal 方式可以叫醒 wait 线程
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

ReadWriteLock

属性

共享锁(读-写锁)

特点

  1. 若是没有写锁的情形下,读是无壅闭的,在一定程度上提高了程序的执行效率;
  2. 读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥(这是由 JVM 自己控制的,代码只要上好响应的锁即可)。

使用原则

  1. 若是你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;
  2. 若是你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。

CountDownLatch(线程计数器 )

作用

CountDownLatch 是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直守候。

final CountDownLatch latch = new CountDownLatch(2);
new Thread() {
    public void run() {
        System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
        Thread.sleep(3000);
        System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
        latch.countDown();
    }

    ;
}.start();
new Thread() {
    public void run() {
        System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
        Thread.sleep(3000);
        System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
        latch.countDown();
    }

    ;
}.start();
System.out.println("守候 2 个子线程执行完毕...");
latch.await();
System.out.println("2 个子线程已经执行完毕");
System.out.println("继续执行主线程");

CyclicBarrierr(回环栅栏-守候至 barrier 状态再所有同时执行)

作用

CyclicBarrier 是一个同步辅助类,允许一组线程相互守候,直到到达某个公共屏障点 (common barrier point)。由于该 barrier 在释放守候线程后可以重用,以是称它为循环 的 barrier。

主要方式

CyclicBarrier 中最主要的方式就是 await 方式,它有 2 个重载版本:

  1. public int await():用来挂起当前线程,直至所有线程都到达 barrier 状态再同时执行后续义务;
  2. public int await(long timeout, TimeUnit unit):让这些线程守候至一定的时间,若是另有线程没有到达 barrier 状态就直接让到达 barrier 的线程执行后续义务。
public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier = new CyclicBarrier(N);
        for (int i = 0; i < N; i++)
            new Writer(barrier).start();
    }

    static class Writer extends Thread {
        private CyclicBarrier cyclicBarrier;

        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(5000); //以睡眠来模拟线程需要预定写入数据操作
                System.out.println("线程" + Thread.currentThread().getName() + "写入数据完 毕,守候其他线程写入完毕");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println("所有线程写入完毕,继续处置其他义务,好比数据操作");
        }
    }

CountDownLatch 和 CyclicBarrier 的区别

  1. CountDownLatch 的作用是允许 1 或 N 个线程守候其他线程完成执行;而 CyclicBarrier 则是允许 N 个线程相互守候;
  2. CountDownLatch 的计数器无法被重置;CyclicBarrier 的计数器可以被重置后使用,因此它被称为是循环的 barrier。

Semaphore

Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取允许信号,做完自己的申请后送还,跨越阈值后,线程申请允许信号将会被壅闭。Semaphore 可以用来构建一些工具池,资源池之类的,好比数据库连接池。

实现互斥锁(计数器为 1)

我们也可以建立计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,示意两种互斥状态。

代码实现

它的用法如下:

// 建立一个计数阈值为 5 的信号量工具
// 只能 5 个线程同时接见
Semaphore semp = new Semaphore(5);
try { // 申请允许
    semp.acquire();
    try {
        // 营业逻辑
    } catch (Exception e) {
    } finally {
        // 释放允许
        semp.release();
    }
} catch (InterruptedException e) {
}

Semaphore 与 ReentrantLock 相似处

Semaphore 基本能完成 ReentrantLock 的所有事情,使用方式也有许多类似之处:

  1. 都需要手动加锁。通过 acquire() 与 release() 方式来获得和释放资源;
  2. Semaphone.acquire()方式默以为可响应中止锁,与 ReentrantLock.lockInterruptibly() 作用效果一致,也就是说在守候临界资源的过程中可以被 Thread.interrupt() 方式中止;
  3. Semaphore 也实现了可轮询的锁请求与准时锁的功效,除了方式名 tryAcquire 与 tryLock差别,其使用方式与 ReentrantLock 险些一致;
  4. Semaphore 也提供了公正与非公正锁的机制,也可在组织函数中举行设定;
  5. 锁释放方式相同。与 ReentrantLock 一样 Semaphore 的锁释放操作也由手动举行。同时,为制止线程因抛出异常而无法正常释放锁的情形发生,释放锁的操作也必须在 finally 代码块中完成。

多线程与并发系列推荐

Java多线程并发06——CAS与AQS

Java多线程并发05——那么多的锁你都领会了吗

Java多线程并发04——合理使用线程池

Java多线程并发03——什么是线程上下文,线程是若何调剂的

Java多线程并发02——线程的生命周期与常用方式,你都掌握了吗

Java多线程并发01——线程的建立与终止,你会几种方式

原创文章,作者:7h28新闻网,如若转载,请注明出处:https://www.7h28.com/archives/2150.html