J.U.C 简介

Java.util.concurrent 是在并发编程中比较常用的东西类,里面包括许多用来在并发场景中运用的组件。比方线程池、堵塞行列、计时器、同步器、并发调集等等。并发包的作者是大名鼎鼎的 Doug Lea。

Lock

Lock 在 J.U.C 中是最核心的组件,锁最重要的特性便是处理并发安全问题。为什么要以 Lock 作为切入点呢?
假设你有看过 J.U.C 包中的一切组件,一定会发现绝大部分的组件都有用到了 Lock。所以经过 Lock 作为切入点使得在后续的学习进程中会更加轻松。

Lock 简介

Lock 接口呈现之前,Java 中的应用程序对于多线程的并发安全处理只能根据 synchronized 关键字来处理。可是 synchronized 在有些场景中会存在一些短板,也便是它并不适合于一切的并发场景。可是在 Java5 以后,Lock 的呈现可以处理 synchronized 在某些场景中的短板,它比 synchronized 更加灵敏。

Lock 的完结

Lock 本质上是一个接口,它界说了开释锁和取得锁的笼统办法,界说成接口就意味着它界说了锁的一个标准规范,也一起意味着锁的不同完结。
完结 Lock 接口的类有许多,以下为几个常见的锁完结

  • ReentrantLock:表明重入锁,它是仅有一个完结了 Lock 接口的类。重入锁指的是线程在取得锁之后,再次获取该锁不需求堵塞,而是直接关联一次计数器增加重入次数

  • ReentrantReadWriteLock:重入读写锁,它完结了 ReadWriteLock 接口,在这个类中维护了两个锁,一个是 ReadLock,一个是 WriteLock,他们都别离完结了 Lock 接口。读写锁是一种适合读多写少的场景下处理线程安全问题的东西,基本原则是: 读和读不互斥、读和写互斥、写和写互斥。也便是说涉及到影响数据改变的操作都会存在互斥。

  • StampedLockstampedLockJDK8 引进的新的锁机制,可以简单认为是读写锁的一个改善版别,读写锁虽然经过分离读和写的功用使得读和读之间可以彻底并发,可是读和写是有冲突的,假设大量的读线程存在,或许会引起写线程的饥饿。stampedLock 是一种达观的读策略,使得达观锁彻底不会堵塞写线程

Lock 的类联系图

Lock 有许多的锁的完结,可是直观的完结是 ReentrantLock 重入锁

ReentrantLock底层原理分析

常用API

void lock() // 假设锁可用就取得锁,假设锁不可用就堵塞直到锁开释
void lockInterruptibly() // 和lock()办法相似, 但堵塞的线程可中止,抛出java.lang.InterruptedException 反常
boolean tryLock() // 非堵塞获取锁;尝试获取锁,假设成功返回 true
boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁办法
void unlock() // 开释锁

ReentrantLock 重入锁

重入锁,表明支持从头进入的锁,也便是说,假设当时线程 t1 经过调用 lock 办法获取了锁之后,再次调用 lock,是不会再堵塞去获取锁的,直接增加重试次数就行了。synchronizedReentrantLock 都是可重入锁。那为什么锁会存在重入的特性?假设在下面这类的场景中,存在多个加锁的办法的相互调用,其实便是一种重入特性的场景。

重入锁的规划意图

比方调用 demo 办法取得了当时的目标锁,然后在这个办法中再去调用demo2,demo2 中的存在同一个实例锁,这个时分当时线程会由于无法取得demo2 的目标锁而堵塞,就会发生死锁。重入锁的规划意图是防止线程的死锁。

public class ReentrantDemo {
    public synchronized void demo() {
        System.out.println("begin:demo");
        demo2();
    }
    public void demo2() {
        System.out.println("begin:demo1");
        synchronized (this) {
        }
    }
    public static void main(String[] args) {
        ReentrantDemo rd = new ReentrantDemo();
        new Thread(rd::demo).start();
    }
}

ReentrantLock 的运用事例

public class AtomicDemo {
    private static int count = 0;
    static Lock lock = new ReentrantLock();
    public static void inc() {
        lock.lock();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
        lock.unlock();
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                AtomicDemo.inc();
            }).start();
            ;
        }
        Thread.sleep(3000);
        System.out.println("result:" + count);
    }
}

ReentrantReadWriteLock

咱们曾经了解的锁,基本都是排他锁,也便是这些锁在同一时间只答应一个线程进行拜访,而读写所在同一时间可以答应多个线程拜访,可是在写线程拜访时,一切的读线程和其他写线程都会被堵塞。读写锁维护了一对锁,一个读锁、一个写锁; 一般情况下,读写锁的功用都会比排它锁好,由于大多数场景读是多于写的。在读多于写的情况下,读写锁可以提供比排它锁更好的并发性和吞吐量。

public class LockDemo {
    static Map<String, Object> cacheMap = new HashMap<>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock read = rwl.readLock();
    static Lock write = rwl.writeLock();
    public static final Object get(String key) {
        System.out.println("开端读取数据");
        read.lock(); //读锁
        try {
            return cacheMap.get(key);
        } finally {
            read.unlock();
        }
    }
    public static final Object put(String key, Object value) {
        write.lock();
        System.out.println("开端写数据");
        try {
            return cacheMap.put(key, value);
        } finally {
            write.unlock();
        }
    }
}

在这个事例中,经过 hashmap 来模拟了一个内存缓存,然后运用读写所来确保这个内存缓存的线程安全性。当履行读操作的时分,需求获取读锁,在并发拜访的时分,读锁不会被堵塞,由于读操作不会影响履行结果。

在履行写操作是,线程必需求获取写锁,当已经有线程持有写锁的情况下,当时线程会被堵塞,只有当写锁开释以后,其他读写操作才干继续履行。运用读写锁提升读操作的并发性,也确保每次写操作对一切的读写操作的可见性。

  • 读锁与读锁可以同享
  • 读锁与写锁不可以同享(排他)
  • 写锁与写锁不可以同享(排他)

ReentrantLock 的完结原理

咱们知道锁的基本原理是,根据将多线程并行任务经过某一种机制完结线程的串行履行,从而到达线程安全性的意图。在 synchronized 中,咱们剖析了偏向锁、轻量级锁、达观锁。根据达观锁以及自旋锁来优化synchronized 的加锁开支,一起在重量级锁阶段,经过线程的堵塞以及唤醒来到达线程竞赛和同步的意图。那么在 ReentrantLock 中,也一定会存在这样的需求去处理的问题。便是在多线程竞赛重入锁时,竞赛失利的线程是如何完结堵塞以及被唤醒的呢?

AQS 是什么

Lock 中,用到了一个同步行列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步东西也是 Lock 用来完结线程同步的核心组件。假设你搞懂了 AQS,那么 J.U.C 中绝大部分的东西都能轻松掌握。

AQS 的两种功用

从运用层面来说,AQS 的功用分为两种:独占和同享 独占锁,每次只能有一个线程持有锁,比方前面给咱们演示的 ReentrantLock 便是 以独占方法完结的互斥锁 同享锁,答应多个线程一起获取锁,并发拜访同享资源,比方 ReentrantReadWriteLock

AQS 的内部完结

AQS 行列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,别离指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开端很方便的拜访前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失利后会封装成 Node 加入到 ASQ 行列中去;当获取锁的线程开释锁以后,会从行列中唤醒一个堵塞的节点(线程)。

ReentrantLock底层原理分析

Node 的组成

开释锁以及增加线程对于行列的改变

当呈现锁竞赛以及开释锁的时分,AQS 同步行列中的节点会发生改变,首先看一下增加节点的场景。

ReentrantLock底层原理分析
这里会涉及到两个改变

  1. 新的线程封装成 Node 节点追加到同步行列中,设置 prev 节点以及修正当时节点的前置节点的 next 节点指向自己
  2. 经过 CAStail 从头指向新的尾部节点

head 节点表明获取锁成功的节点,当头结点在开释同步状况时,会唤醒后继节点,假设后继节点取得锁成功,会把自己设置为头结点,节点的改变进程如下

ReentrantLock底层原理分析
这个进程也是涉及到两个改变

  1. 修正 head 节点指向下一个取得锁的节点
  2. 新的取得锁的节点,将 prev 的指针指向 null

设置 head 节点不需求用 CAS,原因是设置 head 节点是由取得锁的线程来完结的,而同步锁只能由一个线程取得,所以不需求 CAS 确保,只需求把 head 节点设置为原首节点的后继节点,而且断开原 head 节点的 next 引证即可

ReentrantLock 的源码剖析

ReentrantLock 作为切入点,来看看在这个场景中是如何运用 AQS 来完结线程的同步的

ReentrantLock 的时序图

调用 ReentrantLock 中的 lock() 办法,源码的调用进程我运用了时序图来展现。

ReentrantLock底层原理分析
ReentrantLock.lock() 这个是 reentrantLock 获取锁的入口

public void lock() {
 sync.lock();
}

sync 实际上是一个笼统的静态内部类,它承继了 AQS 来完结重入锁的逻辑,咱们前面说过 AQS 是一个同步行列,它可以完结线程的堵塞以及唤醒,但它并不具有业务功用,所以在不同的同步场景中,会承继 AQS 来完结对应场景的功用,Sync 有两个具体的完结类,别离是:

  • NofairSync:表明可以存在抢占锁的功用,也便是说不论当时行列上是否存在其他线程等待,新线程都有时机抢占锁
  • FailSync: 表明一切线程严厉依照 FIFO 来获取锁

NofairSync.lock

以非公正锁为例,来看看 lock 中的完结

  1. 非公正锁和公正锁最大的区别在于,在非公正锁中我抢占锁的逻辑是,不论有没有线程排队,我先上来 cas 去抢占一下
  2. CAS 成功,就表明成功取得了锁
  3. CAS 失利,调用 acquire(1) 走锁竞赛逻辑
final void lock() {
 if (compareAndSetState(0, 1))
   setExclusiveOwnerThread(Thread.currentThread());
 else
  acquire(1);
}

CAS 的完结原理

protected final boolean compareAndSetState(int expect, int update) {
 // See below for intrinsics setup to support this
 return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

经过 cas 达观锁的方法来做比较并替换,这段代码的意思是,假设当时内存中的 state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false
这个操作是原子的,不会呈现线程安全问题,这里面涉及到Unsafe这个类的操作,以及涉及到 state 这个属性的意义。 stateAQS 中的一个属性,它在不同的完结中所表达的意义不一样,对于重入锁的完结来说,表明一个同步状况。它有两个意义的表明

  1. 当 state=0 时,表明无锁状况
  2. 当 state>0 时,表明已经有线程取得了锁,也便是 state=1,可是由于ReentrantLock 答应重入,所以同一个线程多次取得同步锁的时分,state 会递加,比方重入 5 次,那么 state=5。而在开释锁的时分,同样需求开释 5 次直到 state=0其他线程才有资历取得锁