java 并发包(JUC)(学习笔记)

JUC

​ JUC 是 java 中的显式锁,是使用纯 Java 语言实现的锁的功能。可以进行无条件,可轮询,定时的,可中断的锁获得和释放操作。

显示锁

​ 使用 java 内置锁时候,不需要 java 代码显式地同步对象监视器进行抢占和释放,这些是 java 的 JVM 底层实现的。任何一个 Java 对象都可以作为内置锁使用,但是 Java 内置锁功能单一,不具备高级的锁功能

  • 限时强锁:在有限的时间抢占,如果超时就会放弃

  • 可中断枪锁:可以在枪锁时,外部线程给枪锁线程发出中断信号,就能唤醒等待线程,终止抢占过程

  • 多个等待队列:为锁维持多个等待队列,提供枪锁的效率

​ java 对象锁还存在在激烈竞争的情况下会导致锁的膨胀成重量级锁,重量级锁会导致操作系统在内核态和用户态之间来回切换,导致性能降低。

​ Java 显示锁就是为了解决这些 Java 对象锁的功能问题和性能问题。

显示锁的 Lock 接口

​ JDK 5 版本引入了 java.util.concurrent(JUC) 并发包,并提供了操作的工具类。可以使用 JUC 工具包实现多线程的迸发操作。

Lock 的主要方法
方法作用
void lock();枪锁,若成功则继续执行,否则就会阻塞
void lockInterruptibly() throws InterruptedException;可中断枪锁,当前线程在枪锁的过程中可以响应中断信号
boolean tryLock();尝试枪锁,线程为非阻塞模式,在调用该方法后立刻返回,若成功返回 true 否则返回 false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;限时枪锁,到达时间后未抢到锁后就返回 false,也可以响应中断信号
void unlock();释放锁
Condition newCondition();绑定 Condition 对象,使用 Condition 进行线程间的通信
显示锁对于 Java 对象锁的优势
  • 可中断枪锁

    使用 synchronized 获取锁的时候,如果线程没获取到就会阻塞,阻塞期间该线程时不响应中断(interrupt)信号的,而调用 lockInterruptibly() 方法获取锁时,如果线程被中断,线程就会抛出中断异常

  • 可非阻塞获取锁

    使用 synchronized 关键字获取锁时,没有成功就只能被阻塞;而调用 tryLock() 方法获取时,如果没成功,线程不会被阻塞,而是返回 false

  • 可限时枪锁

    可以设置抢占锁的时间,而 synchronized 获取不到锁就会阻塞,如果一直获取不到就会一直阻塞

RenntrantLock 可重入锁

​ RenntrantLock 是 JUC 提供的对 Lock 的一个基础的实现类,可以实现 Java 显示锁的功能。RenntranLock 其中的**抽象队列同步器(Abstract Queue Synchroniezd,AQS)**实现,AQS 是同步机制基础设施,是 JUC 锁框架的基础

​ RenntrantLock 是可重入的互斥锁。可重入:一个线程可以对一个资源重复加所。互斥:同一时刻只能有一个线程获取到锁

  • 可重入

        public static void main(String[] args) {
            User user = new User();
            int j = 0;
            ReentrantLock lock = new ReentrantLock();
            for (int i = 0; i < 5; i++) {
                new Thread(() -> {
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + ":第一次加锁");
                        lock.lock();
                        System.out.println(Thread.currentThread().getName() + ":第二次加锁");
                        user.show();
                    } finally {
                        lock.unlock();
                        System.out.println(Thread.currentThread().getName() + ":第一次释放锁");
                        lock.unlock();
                        System.out.println(Thread.currentThread().getName() + ":第二次释放锁");
                    }
                }, "name-" + i).start();
            }
    
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                j++;
                System.out.println(Thread.currentThread().getName() + ",j:" + j);
            }
        }
    // 如果不释放两次,枪锁线程就会阻塞,但是 main 线程不会阻塞
    // 只能释放两次后其他线程才可以获取到锁
    
  • 独占:同一时间只会有一个线程获取到锁,只有释放锁后其他线程才会获取到锁

    // reentrantLock 的独占属性
    public class MonopolyTest {
    
        public static void main(String[] args) {
    
            // 执行论数
            final int TURNS = 1000;
            // 线程数
            final int THREADS = 10;
            // 获取线程池
            ExecutorService pool = Executors.newFixedThreadPool(THREADS);
    
            // 创建锁对象
            Lock lock = new ReentrantLock();
            // 倒数
            CountDownLatch latch = new CountDownLatch(THREADS);
            long start = System.currentTimeMillis();
            for (int i = 0; i < THREADS; i++) {
                pool.submit(() -> {
                    try {
                        for (int j = 0; j < TURNS; j++) {
                            IncrementData.lockAndFastIncrease(lock);
                        }
                    } catch (Exception e) {
    
                    }
                    latch.countDown();
                });
            }
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long time = (System.currentTimeMillis() - start) / 1000;
            System.out.println("time:" + time);
            System.out.println("sum:" + IncrementData.sum);
        }
    }
    
    class IncrementData {
        public static int sum = 0;
    
        public static void lockAndFastIncrease(Lock lock) {
            lock.lock();
            try {
                sum++;
            } finally {
                lock.unlock();
            }
        }
    }
    

显示锁的模板代码

lock() 方法枪锁的模板
        Lock lock = new ReentrantLock();
		lock.lock();
        try {
            sum++;
        } finally {
            lock.unlock();
        }
  1. 释放锁的 unlocl() 必须在 finally 中执行,不然就会导致锁不会被释放
  2. 枪锁的 lock() 方法必须在 try 语句块,而不是在 try 语句块之类
    1. lock() 方法没有申明抛出的异常,所有可以不用包含到 try 中
    2. lock() 不一定能够枪锁成功,如果没有抢占成功就不需要释放锁,而在没有锁定的情况下释放可能会导致允许时异常
  3. 独占锁的 locl() 方法和 try 中的临界区代码中不能插入其他代码,这样可能会导致锁不会释放
tryLock() 方法非阻塞枪锁

​ lock() 是阻塞式抢占,在没有抢到锁的情况下,当前线程会阻塞。如果不希望线程阻塞就可以使用 tryLock() 尝试枪锁,tryLock() 是非阻塞的,在没有抢到锁的情况下,线程会立即返回,不会被阻塞

        Lock lock = new ReentrantLock();
		if (lock.tryLock()){
            try {
                
            }catch (Exception e){
                lock.unlock();
            }
        }else{
        	// 枪锁失败的情况    
        }

​ boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 在实际开发中常用,限时阻塞枪锁场景中有用

tryLock(long time, TimeUnit unit) 枪锁模板
        Lock lock = new ReentrantLock();
		if (lock.tryLock(1,TimeUntil.SECONDS)){
            try {
                
            }catch (Exception e){
                lock.unlock();
            }
        }else{
            // 枪锁失败的情况
        }
三种方法的总结
  • lock():用于阻塞抢锁,抢不到就会一直阻塞
  • tryLock():尝试枪锁,如果尝试成功就返回 true 否则返回 false,不论成功失败就会立即返回,枪锁线程不会被阻塞
  • tryLock(long time, TimeUnit unit) :在限时中未抢到锁就会立即返回

基于显示锁的线程间通信

​ 在 Java 线程间通信中使用 wait,notify 作为线程的开关,用于线程间的通信。但是不能指定唤醒哪一个线程

​ 在 Java 显示锁中提供了 Condition 对象进行线程间的通信

Condition 主要方法
  • await():等待,相当于内置锁的 Object.wait() 语义,使其线程加入到 awite 等待队列中,当线程调用 singal() 等待队列中的线程就会被唤醒,重新去枪锁
  • signal():通知,相当于内置锁的 notify(),唤醒在 await 等待队列中的线程
  • signalAll():通知全部,唤醒所有在 awite() 等待队列中的所有线程,相当于内置锁的 notifyall() 功能
  • await(long Time,TimeUnit unit):限时等待,和 awite 相同,在等待指定时间后线程就会终止等待
Condition 的等待和 Object 的方法
  • Condition 类的 await 方法和 Object 类的 wait 方法等效
  • Condition 类的 signal 方法和 Object 类的 notify 方法等效
  • Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效

一个 Condition 对象的 signal 方法不能去唤醒其他 Condition 对象的 await 线程,Condition 对象基于显示锁,所有不能独立创建一个 Condition 对象,而需要借助显示锁实例去获取绑定的 Condition 对象。不过 Lock 显式锁实例都可以由任意数量的 Condition。可以通过 Lock.newCondition() 去获取一个与当前显式锁绑定的 Condition 实例,然后就可以通过这个 Condition 方式进行线程通信

实例
    // 创建显式锁
    static Lock lock = new ReentrantLock();
    // 获取 Condition 对象
    static private Condition condition = lock.newCondition();

    // 等地线程的异步任务
    static class WaitTarget implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println("等待");
                condition.await();
                ;
                System.out.println("继续执行");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    // 通知任务
    static class NotifyTarget implements Runnable {
        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println("通知");
                condition.signal();
                System.out.println("通知成功");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new WaitTarget(),"wait").start();
        new Thread(new NotifyTarget(),"notify").start();
    }

​ 在等待线程必须获取显示锁,await 方法会让当前线程加入 Condition 对象的等待队列中,在等待线程调用 await 方法后,线程就会是释放占用的显式锁,以通知线程能够抢到锁,通知线程抢到锁后才会进入临界区发送通知。

​ 在通知线程调用 signal 方法前,通知线程也必须获取到显式锁对象,在通知线程调用 signal 方法后,JUC 就会在 Condition 等待队列唤醒一个线程,等待线程被唤醒后,将会重新尝试获取 Condition 对象绑定显式锁,才能继续执行

​ 在通知线程调用 signal 后,一定记得释放当前占用的显式锁,只有这样被唤醒的线程才有获取锁的机会,才能继续执行

​ 由于 Lock 由公平锁和非公平锁之分,而 Condition 各 Lock 绑定,因此与 Lock 一样的公平特征:如果使公平锁等待线程就会按照先进先出(FIFO)的顺序从 Condition 等待队列唤醒,如果使非公平锁就不按照 FIFO 顺序了

LockSupport

​ LockSupport 是 JUC 提供的一个线程阻塞和唤醒的工具类,该工具类可以让线程在任意位置阻塞和唤醒,其所有的方法都是静态方法

// 无限期阻塞当前线程
public static void park();

// 唤醒某一个阻塞线程
public static void unpark(Thread thread);

// 阻塞当前线程,有超时时间限制
public static void parkNanos(long nanos);

// 阻塞当前线程,直到莫一个时间
public static void parkUntil(long deadline);

// 无限期阻塞当前线程,带 blocker 对象,用于给诊断工具确定线程受阻原因
public static void park(Object blocker);

// 限时阻塞当前线程,带 blocker 对象
public static void parkNanos(Object blocker, long nanos);

// 获取被阻塞线程的 blocker 对象,用于分析阻塞原因
public static Object getBlocker(Thread t);

​ 其中主要分为两类:park 和 unpark。主要是停止线程和启动线程

实例
	public static class ChangeObjectThread extends Thread {

        public ChangeObjectThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":即将进入无限时阻塞");

            LockSupport.park();

            if (Thread.currentThread().isInterrupted()) {
                System.out.println(Thread.currentThread().getName() + ":被中断了");
            } else {
                System.out.println(Thread.currentThread().getName() + ":被重新唤醒了");
            }

        }
    }

    public static void sleepSeconds(int time) {
        try {
            Thread.sleep(time * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        ChangeObjectThread t1 = new ChangeObjectThread("线程一");
        ChangeObjectThread t2 = new ChangeObjectThread("线程二");

        t1.start();
        sleepSeconds(1);
        t2.start();
        sleepSeconds(1);
        // 中断线程
        t1.isInterrupted();

        LockSupport.unpark(t2);
    }
LockSupport.park() 和 Thread.sleep() 区别

​ LockSupport.park() 和 Thread.sleep() 都是让线程阻塞

区别
  1. Thread.sleep() 没法从外部唤醒,只能自己唤醒,而 LockSupport.park() 方法阻塞的线程可以通过 LockSupport.unpark() 方法唤醒
  2. Thraed.sleep() 方法声明了 InterruptedExceotion 中断异常,这是一个受捡异常,调用者需要捕获这个异常或者抛出,而调用 LockSupport.park() 方法不需要捕获异常
  3. LockSupport.park() 方法,Thread.sleep() 方法都有一个特点,当阻塞线程的 Thread.interrupt() 方法被调用时,被注释线程的中断标志将被设置,该线程将被唤醒。主要是二者的响应方式不同:LockSupport.park() 方法不会抛出 InterruptedException 异常,仅仅设置了线程的中断标志,而 Thread.sleep() 方法会抛出 InterruptedException 异常
  4. Thread.sleep() 相比,调用 LockSupport.parl() 更精准,更加灵活地阻塞,唤醒指定线程
  5. Thread.sleep() 是一个 Native(本地方法)方法;LockSupport.park() 并不是 Native 方法,只是调用了 Unsafe 类地 Natice 方法实现
  6. LockSupport.park() 方法允许设置一个 Blocker 对象,主要用来监视工具或诊断工具确定线程受阻原因
LockSupport.park() 和 Object.wait() 区别
  1. Object.wait() 方法需要在 synchroized 方法块执行,而 LockSupport.park() 可以在任何地方执行
  2. 当被阻塞线程被中断时,Object.wait() 方法抛出了中断异常,调用者需要捕获或者在抛出;当被阻塞线程被中断时,LockSupport.park() 不会抛出异常,调用时不需要处理中断异常
  3. 如果线程没有被 Object.wait() 阻塞之前被 Object.notify() 唤醒,就是如果在 Object.awite() 之前执行 Object.notify() 会抛出 IllegalMonitorStateException 异常,是不被允许地;而线程在没有被 LockSupport.park() 阻塞之前被 LockSupport.unpark() 唤醒就不会抛出异常

显式锁地分类

可重入锁和不可重入锁

​ 可重入锁也叫递归锁:一个线程可以多次抢占同一个锁

​ 不可重入锁:一个线程只能占一次同一个锁

​ JUC 地 ReentrantLock 就是可重入锁地一个实现类

不可重入的自旋锁

​ 当一个线程在获取锁的时候,如果锁依据被其他线程获取,调用者就会一直在那循环检测是否已经被释放,一直到获取锁才会退出循环。

​ CAS 自旋锁的实现原理:枪锁线程不断进行 CAS 自旋操作去更新锁的 owner(拥有者),如果更新成功,就表明已经枪锁成功,退出枪锁方法。如果其他线程获取,调用者就会一直进行 CAS 更新操作,直到成功才会退出循环。

public class SpinLock implements Lock {

    private AtomicReference<Thread> owner = new AtomicReference<>();

    @Override
    public void lock() {
        Thread t = Thread.currentThread();

        // 自旋
        while (owner.compareAndSet(null,t)) {
            Thread.yield();// 让当前剩余的 CPU 时间片
        }
    }

    @Override
    public void unlock() {
        Thread t = Thread.currentThread();

        if (t == owner.get()){
            owner.set(null);
        }
    }
}
可重入的自旋锁
public class ReentrantSpinLock implements Lock {

    /**
     *  当前锁的拥有者
     *  作为 Thread 作为同步状态,而不是简单的整数作为同步状态
     */
    private AtomicReference<Thread> owner = new AtomicReference<>();

    /**
     *  记录一个线程获取锁的次数
     *  为同一个线程在操作
     */
    private int count = 0;

    @Override
    public void lock() {
        Thread t = Thread.currentThread();

        if (t == owner.get()){
            ++count;
            return;
        }

        // 自旋
        while (owner.compareAndSet(null,t)){
            Thread.yield();
        }
    }

    @Override
    public void unlock() {
        Thread t = Thread.currentThread();

        if (t == owner.get()){
            if (count > 0){
                --count;
            }
        }else {
            owner.set(null);
        }
    }
}
自旋锁的特点

​ 如果锁被其他线程持有,当前线程将循环等待,直到获取到锁,线程枪锁期间的状态不会改变,一直都是运行状态(RUNNABLE),操作系统就处于用户态

自旋锁的问题

​ 在激烈竞争的场景下,如果线程持有锁的时间过长,就会导致其他空自旋耗尽 CPU 资源,还会导致大量的线程进行空自旋,还可能引起总线风暴

CAS (自旋)可能导致“总线风暴”

​ 由于做数据计算的时候不是直接访问主内存而是通过缓存访问数据总线(Bus)传递数据,而且多个缓存是共用一条总线的。

​ 由于多核 CPU 操作数据的时候会通过数据总线将数据加载到缓存中进行计算,在操作完成后又将数据写回到数据总线中,由于 volatile 可以保证数据的一致性,一定其中 1 个 CPU 核心修改了数据并写入到内存后就会导致其他 CPU 核心中的对应的数据失效。但是如果缓存的一致性流量过大(多个数据同时保证一致性),总线将会成为瓶颈,这就是 “总线风暴(为了保证数据的一致性,而导致大量的数据失效,需要其他核心从数据总线获取最新的数据,但是总线瓶颈会影响其性能)”

​ Java 锁在激烈竞争下会膨胀为重量级锁,就是为了避免同一时间的总线风暴

​ 在轻量级锁中可以通过线程排队减少 CAS 的数量

CLH 自旋锁

​ CLH 是基于队列的排队的自旋锁,由于 Craig,Landin 和 Hagersten 发明的,所有就称为 CLH 锁。

​ 简单 CLH 可以基于单向链表实现的。申请锁的线程会先通过 CAS 在单向链表的尾部增加一个节点,之后该线程只需要在其前驱节点上进行普通自旋,等待前驱节点释放锁即可。由于 CLH 锁只有在节点入队时进行一下 CAS 的操作,在节点加入队列之后,抢锁线程不需要进行 CAS 自旋,只需要普通自旋即可

CLH 实现简单实例
public class CLHLock implements Lock {

    /**
     * 当前节点的线程本地变量
     */
    private static ThreadLocal<Node> curNodeLocal = new ThreadLocal<>();

    /**
     *  CLHLock 队列的尾部指针,使用 AtomicReference 方便进行 CAS 操作
     */
    private AtomicReference<Node> tail = new AtomicReference<>();

    public CLHLock() {
        // 设置尾部节点
        tail.getAndSet(Node.EMPTY);
    }

    @Override
    public void lock() {
        Node curNode = new Node(true,null);
        Node preNode = tail.get();
        // CAS 自旋:将当前阶段曹仁队列的尾部
        while (!tail.compareAndSet(preNode,curNode)){
            preNode = tail.get();
        }
        // 设置前节点
        curNode.setPrevNode(preNode);

        // 自旋监听前节点的 locked 变量,直到设置为 false
        // 去哦前驱节点 locked 状态为 true 则表示前一个线程还在抢占或者占有锁
        while (curNode.getPrevNode().isLocked()){
            // 让出 CPU,提高性能
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + ":获取到锁了");

        // 当前节点缓存在本地变量中,用于释放锁
        curNodeLocal.set(curNode);
    }

    @Override
    public void unlock() {
        Node curNode = curNodeLocal.get();
        curNode.setLocked(false);
        curNode.setPrevNode(null);
        curNodeLocal.set(null);
    }

    static class Node{

        volatile  boolean locked;

        Node prevNode;

        public static final Node EMPTY = new Node(false,null);

        public Node(boolean locked, Node prevNode) {
            this.locked = locked;
            this.prevNode = prevNode;
        }

        public Node getPrevNode() {
            return prevNode;
        }

        public void setPrevNode(Node prevNode) {
            this.prevNode = prevNode;
        }

        public boolean isLocked() {
            return locked;
        }

        public void setLocked(boolean locked) {
            this.locked = locked;
        }
    }
}
CLH 锁的原理

​ 枪锁线程在队列尾部加入一个节点,然后仅在前驱节点上进行普通自旋,不断轮询前一个节点状态。如果前节点释放锁,当前节点就枪锁成功

  1. 初始状态队列尾部属性(tail)指向一个 EMPTY 节点

        /**
         *  CLHLock 队列的尾部指针,使用 AtomicReference 方便进行 CAS 操作
         */
        private AtomicReference<Node> tail = new AtomicReference<>();
    
        public CLHLock() {
            // 设置尾部节点
            tail.getAndSet(Node.EMPTY);
        }
    

    tail 使用 AtomicReference 是为了线程并发操作不会发生安全问题

  2. Thrad 在枪锁会创建一个性的 Node 加入到队列的尾部;tail 会指向新的 Node,同时新的 Node 的 preNode 属性指向 tail 之前的指向节点,并且通过 CAS 自旋完成

            Node curNode = new Node(true,null);
            Node preNode = tail.get();
            // CAS 自旋:将当前阶段曹仁队列的尾部
            while (!tail.compareAndSet(preNode,curNode)){
                preNode = tail.get();
            }
            // 设置前节点
            curNode.setPrevNode(preNode);
    
  3. Thrad 加入枪锁队列后,会在前驱节点上自旋:循环判断前驱节点的 locked 属性是否时 false,如果 false 就表示释放了锁,当前线程抢占到锁

            // 自旋监听前节点的 locked 变量,直到设置为 false
            // 去哦前驱节点 locked 状态为 true 则表示前一个线程还在抢占或者占有锁
            while (curNode.getPrevNode().isLocked()){
                // 让出 CPU,提高性能
                Thread.yield();
            }
    
            System.out.println(Thread.currentThread().getName() + ":获取到锁了");
    
            // 当前节点缓存在本地变量中,用于释放锁
            curNodeLocal.set(curNode);
    
  4. Thrad 枪到锁后,locked 属性一直为 true,一直到临界区代码执行完,然后调用 unlock() 方法释放锁,释放后其 locked 属性才为 false

        @Override
        public void unlock() {
            Node curNode = curNodeLocal.get();
            curNode.setLocked(false);
            curNode.setPrevNode(null);
            curNodeLocal.set(null);
        }
    

    释放锁操作:线程从本地变量 curNodeLocal 中获取当前节点的 curNode 将其设置为 false 以便后节点能获取到锁

​ 线程设置当前节点 curNode 的 locked 状态为 false 前,为了 GC 内回收其节点,需要将 curNode 设置为 null,为了下一次枪锁不会出错,需要将线程本地变量 curNodeLocal 中节点应用设置为 null

CLH 的锁抢占国程

如果 A,B,C 三个线程同时抢占 CLHLock 锁的国程

  1. 线程 A 执行了 Lock 操作,创建一个 nodeA 节点,并设置 locked 状态为 true,然后设置前节点为 CLHLock.tail(此时为 EMPTY),并将 CLHLock.tail 设置为 nodeA ,之后线程 A 就会在前节点上进行普通自旋
  2. 如果线程 B 这时开始执行 lock 操作,就会创建 nodeB 节点,并设置为 locked 为 true,然后将 prevNode 设置为 nodeA,将 CLHLocl.tail 设置为 nodeB,之后线程 B 开始在 nodeA 上进行自旋
  3. 线程 C 开始执行 lock 操作,创建 nodeC 节点,设置 locked 为 true。设置前驱节点为 CLHLock.tail (此时为 nodeB),并将 CLHLock.tail 设置为 nodeC,之后线程 C 再前驱节点(nodeB)上进行自旋

过程总结

  1. CLHLock 的尾指针 tail 中指向最后一个线程节点
  2. CLHLock 中的枪锁线程一直进行普通自旋,循环判断前一个线程的 locked 状态,如果时 true,那说明前一个线程处于自旋等待状态或正在执行临界区代码,所有自己需要自旋等待
CLH 释放过程

​ 如果 A,B,C 线程已经或到锁,释放锁的过程如下

  1. 线程 A 执行完临界区代码后开始 unlock(释放)操作,设置 nodeA 的前驱引用为 null,锁的状态 locked 为 false

  2. 线程 B 执行枪锁并且执行完临界区代码后,来说 unlock(释放操作),设置 nodeB 的前驱引用为 null 设置锁状态为 locked 为 false

    线程 B 释放锁后,nodeA 对象已经没任何强引用,可以被 GC 回收了

  3. 线程 C 执行枪锁线程并且完成临界区代码后,开始 unlock(释放)操作,设置 nodeC 的前驱设置为 null,锁状态为 false

CLH 锁的优缺点

​ CLH 锁时一种队列锁,优点是空间复杂度低。如果有 N 个线程,L 个锁,每次线程就只会获取一个锁,那么存储空间就为 O(L+N),N 个线程就有 N 个 node,L 个锁就有 L 个 tail

​ 缺点时再 NUMA 架构的 CPU 平台上性能差,CLH 队列再锁 NUMA 架构的 CPU 平台上,每个 CPU 都有自己的内存,前驱节点的再不同的 CPU 核心上,当内存位置远时,判断其 locked 属性的时候就会降低性能。但是再 SMP 架构不存在这个问题

悲观锁和乐观锁

​ 线程进入临界区前是否锁住同步资源地角度来分

​ 悲观锁:每一次进入地时候就默认会被修改,所以线程在每次读写地时候都会上锁,锁住同步资源,这样其他线程读写这个数据地时候就会被阻塞,一直等待。悲观锁适用于写多读少地场景,遇到高并发写时性能提高,Java 的 synchronized 就是悲观锁

​ 乐观锁:每一次拿数据的时候默认别的线程不会被修改,所有不会上锁,但每次在操作的时候就会判断数据是否被修改过。Java 中的乐观锁都是通过 CAS 自旋操作实现的。但是在并发量高的时候会导致大量的空自旋。Java 中的 synchronized 轻量级锁时一直乐观锁,和 JUC 基于 AQS 实现的显式锁也是乐观锁

公平锁和非公平锁

​ 是基于抢占锁的枪锁机会是否是公平的、平等的

​ 公平锁:线程就是按照一定的规则获取到锁

​ 非公平锁:线程是随机获取到锁

​ ReentrantLock:默认是非公平锁,可以通过构造函数传入 true 修改为公平锁

悲观锁存在的问题

​ 就是每一次读取数据的时候都会上锁,其他线程获取数据的时候就会阻塞,直到拿到锁。传统的关系型数据库用到很多悲观锁:行锁,表锁,读锁,写锁

  1. 在多线程的竞争下,加锁和释放锁都会导致上下文切换和调度延时,引起性能问题
  2. 一个线程持有锁后,会早在其他枪锁线程挂起
  3. 如果一个优先级高的线程等待一个优先级低的线程释放锁,就会导致线程的优先级倒置,引起性能风险
通过 CAS 实现乐观锁

乐观锁主要是两个步骤:

  1. 冲突检测
  2. 数据更新

​ 乐观锁比较典型的就是 CAS 原子操作,JUC 强大的高并发性能是建立在 CAS 原子操作上:需要操作的内存位置(V),进行比较的预期原值(A)和改变后写入的新值(B)

  1. 检测位置 V 的值是否是 A
  2. 如果是就将 V 的值跟新为 B,否则就不会改变该位置的值

​ 通过 CAS 自旋,在不使用锁的情况下实现多线程之间的变量同步,在没有线程被阻塞的情况下实现变量的同步就是 “非阻塞同步”(Non-Blocking Synchronization)或叫无锁同步,使用 CAS 属于无锁编程(Lock Free)的一种实践

运行接口
public class IncrementData {

    public static void lockAndIncrease(Lock lock){

        System.out.println(Thread.currentThread().getName() + " ----> 开始抢锁了");
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " -----> 抢到锁了");
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}
非公平锁的实战

​ 非公平锁多个线程获取锁的顺序并不一定时申请锁的顺序。非公平锁吞吐量比公平锁大,但是可能会导致线程优先级反转(高,中,低 线程,如果线程低获取到资源,然后被线程等级为中的线程获取到资源,那么线程高的线程就不会获取到资源,线程中和线程高的线程就出现了优先级反转)线程饥饿现象(饥饿是指系统不能保证某个进程的等待时间上界,从而使该进程长时间等待,当等待时间给进程推进和响应带来明显影响时,称发生了进程饥饿。当饥饿到一定程度的进程所赋予的任务即使完成也不再具有实际意义时称该进程被饿死)

// 创建非公平锁
Lock lock = new ReentrantLock(false);

public class NonFairLock {

    public static void main(String[] args) throws InterruptedException {

        Lock lock = new ReentrantLock(false);

        Runnable r = () -> IncrementData.lockAndIncrease(lock);

        Thread[] tArray = new Thread[4];

        for (int i = 0; i < 4; i++) {
            tArray[i] = new Thread(r,"线程 - " + i);
        }

        for (int i = 0; i < 4; i++) {
            tArray[i].start();
        }
        Thread.sleep(Integer.MAX_VALUE);
    }

}
线程 - 1 ----> 开始抢锁了
线程 - 2 ----> 开始抢锁了
线程 - 3 ----> 开始抢锁了
线程 - 0 ----> 开始抢锁了
<------->
线程 - 1 -----> 抢到锁了
线程 - 3 -----> 抢到锁了
线程 - 2 -----> 抢到锁了
线程 - 0 -----> 抢到锁了
公平锁实战
public class FairLock {

    public static void main(String[] args) throws InterruptedException {
        // 公平锁
        Lock lock = new ReentrantLock(true);

        Runnable r = () -> IncrementData.lockAndIncrease(lock);

        Thread[] tArray = new Thread[4];

        for (int i = 0; i < 4; i++) {
            tArray[i] = new Thread(r,"线程 - " + i);
        }
        for (int i = 0; i < 4; i++) {
            tArray[i].start();
        }
        Thread.sleep(Integer.MAX_VALUE);
    }
}
线程 - 2 ----> 开始抢锁了
线程 - 2 -----> 抢到锁了
线程 - 0 ----> 开始抢锁了
线程 - 0 -----> 抢到锁了
线程 - 3 ----> 开始抢锁了
线程 - 3 -----> 抢到锁了
线程 - 1 ----> 开始抢锁了
线程 - 1 -----> 抢到锁了
可中断锁和不可中断锁

​ 可中断锁:A 线程在执行时,B 线程在阻塞枪锁,由于 B 线程等待时间过长,就可以中断自己阻塞等待,就是可中断锁

​ 不可中断锁:在等待的时候不能中断自己,只能等待线程获取到锁

​ 是否时可中断锁就是是否可以在等待阶段中断自己去做其他事情。Java 的 synchronized 内置锁就是不可中断锁,而 JUC 的显式锁是可中断锁

锁的可中断抢占
  1. lockInerruptibly()

    可中断抢占锁占过程处理 Thread,interupt() 中断信号,如果被中断,就会终止抢占并抛出 InterrupteException 异常

  2. tryLock(long timeout,TimeUnit unit)

    阻塞式“限时抢占”,在规定的时间内会处理 Thread.interrupt() 中断信号,如果线程被中断就会终止抢占并抛出 InterruptedExceotion 异常(线程中断异常)

共享锁和独占锁

​ JUC 的 ReentrantLock 类是一个标准的占锁实现类

​ 共享锁:就是允许多个线程同时获取锁,容许线程并发进入临界区。ReentrantLockWriteLock(读写锁)就是共享锁的一种实现,读操作可以多个线程获取,但是写操作只能由一个线程获取,而且其他的线程不能进行读操作

​ 独占锁:如果某个线程获取到锁,那么其他线程就只能等待

独占锁

​ 独占锁也称为 排它锁,互斥锁,独享锁。是锁在同一时刻只能被一个线程所持有,一个线程持有后其他任何线程获取加锁线程都会被阻塞,直到持有锁线程解锁。共享资源在同一时刻只能由一个线程进行方法和操作。

共享锁

​ 在任意时刻,同一资源可以由多个线程持有,线程只能读取临界区的数据而不能修改器数据

​ JUC 中的共享锁包括 Semaphore(型号量),ReadLock(读写锁)中的读锁,CountDownLatch(倒数)

Semaphore

​ Semaphore 可以用来控制在同一时刻访问共享资源的线程数量。通过协调各个线程以保证共享资源的合理运用。Semaphore 控制了虚拟许可,其数量可以同构造函数指定,线程在访问资源前必须调用 Semaphore 的 acquire() 方法获取许可。如果没有许可,获取线程就必须阻塞等待。必须调用 Semaphore 的 release() 方法释放许可

Semaphore 的主要方法

  1. Semaphore()

    构造 Semaphore 实例,初始数量为 permits 参数值

  2. Semaphore(permits,fair)

    构造 Semaphore 实例,初始化其关联的许可数量为 permits 参数值,已经是否以公平模式进行许可发放,默认情况时非公平模式

  3. availablePernits()

    获取 Semaphore 对象中的可用的许可数量

  4. acquire()

    当线程尝试获取 Semaphore 的许可的时候,此过程会被阻塞,线程会一直等待 Semaphore 发放一个许可

    当前线程获取到了一个可用的许可

    当前线程被中断,抛出 InterruptedException 异常,并停止等待,继续向下执行

  5. acquire(permits)

    尝试阻塞获取 permits 个许可,过程会阻塞,获取线程会一直等待 Semaphore 发放 permits 个许可,如果没有足够的许可就会中断线程抛出 InterruptedException 异常并终止阻塞

  6. acquireUninterruotibly()

    当前线程尝试阻塞的获取一个许可,阻塞过程不可中断,直到获取许可成功

  7. acquireUninterruptubly(permits)

    当前线程尝试阻塞的获取 permits 许可,阻塞过程不可中断,直到获取许可 permits 个许可成功

  8. tryAcquire()

    尝试获取一个许可,此过程非阻塞,只是进行一次尝试,会立即返回。获取成功放回 true,否则放回 false

  9. tryAcquire(permits)

    tryAcquire 的多个版本

  10. tryAcquire(time,TimeUnit)

    限时获取一个许可,过程时阻塞,会一直等待许可:

    线程获取到一个许可,停止等待,返回 true

    线程等待时间超出限制,停止等待,并返回 false

    线程再等待时间中被中断,会抛出 InterruptedException 异常,并停止等待,继续执行

  11. tryAcquire(permits,timeout,TimeUnit)

    是 tryAcquire(time,TimeUnit) 多个版本

  12. release()

    释放当前线程的许可

  13. release(permits)

    释放当前线程的指定个数的许可

  14. drainPermits()

    当前线程获取剩下的所有许可

  15. hasQueuedThreads()

    判断 Semaphore 对象是否存在等待许可的线程

  16. getQueuedLength()

    Semaphore 对象是否存在等待许可的线程数量

Semaphore 共享锁使用实例

public class SemaphoreTest {

    public static void main(String[] args) {

        // 排队的数量 (请求数量)
        final int USER_TOTAL = 10;
        // 可同时受理也i我的窗口数量(同时并发执行的线程数)
        final int PERMIT_TOTAL = 2;
        // 线程池,用于模拟测试
        final CountDownLatch latch = new CountDownLatch(USER_TOTAL);

        // 创建信号量
        final Semaphore semaphore = new Semaphore(PERMIT_TOTAL);

        AtomicInteger index = new AtomicInteger(0);

        Runnable r = () -> {
            try {
                semaphore.acquire(1);
                // 获取了一个许可
                System.out.println(new Date() + ",处理中:" + index.incrementAndGet());
                // 模拟业务操作,处理排队业务
                Thread.sleep(1000);
                // 是否信号
                semaphore.release(1);
            }catch (Exception e){
                e.printStackTrace();
            }
            latch.countDown();
        };

        Thread[] tArray = new Thread[USER_TOTAL];
        for (int i = 0; i < USER_TOTAL; i++) {
            tArray[i] = new Thread(r,"线程-" + i);
        }

        // 启动
        for (int i = 0; i < USER_TOTAL; i++) {
            tArray[i].start();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

运行结果

Wed Dec 14 20:39:48 CST 2022,处理中:1
Wed Dec 14 20:39:48 CST 2022,处理中:2
Wed Dec 14 20:39:49 CST 2022,处理中:3
Wed Dec 14 20:39:49 CST 2022,处理中:4
Wed Dec 14 20:39:50 CST 2022,处理中:5
Wed Dec 14 20:39:50 CST 2022,处理中:6
Wed Dec 14 20:39:51 CST 2022,处理中:7
Wed Dec 14 20:39:51 CST 2022,处理中:8
Wed Dec 14 20:39:52 CST 2022,处理中:9
Wed Dec 14 20:39:52 CST 2022,处理中:10
CountDownLatch

​ 是一个常用的共享锁,相当于一个多线程环境下的倒数。主要是计数器,再多线程的环境下进行减一操作,为 0 后被 awite 方法阻塞的线程会被唤醒。(作为倒计时,当为 0 是,被阻塞的线程会被唤醒)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值