Java线程并发

目录

  • [一、Java 内存模型](#一、Java 内存模型)
  • 二、线程安全
    • 1.volatile
    • 2.乐观锁
      • [2.1 CAS](#2.1 CAS)
      • [2.2 Unsafe类](#2.2 Unsafe类)
      • [2.3 原子类](#2.3 原子类)
        • [2.3.1 AtomicInteger](#2.3.1 AtomicInteger)
        • [2.3.2 AtomicReference](#2.3.2 AtomicReference)
        • [2.3.3 AtomicIntegerFieldUpdater](#2.3.3 AtomicIntegerFieldUpdater)
      • [2.4 ABA问题](#2.4 ABA问题)
      • [2.5 CLH](#2.5 CLH)
    • 3.AQS
      • [3.1 结构](#3.1 结构)
      • [3.2 实现逻辑](#3.2 实现逻辑)
        • [3.2.1 独占锁模版](#3.2.1 独占锁模版)
        • [3.2.2 共享锁模版](#3.2.2 共享锁模版)
      • [3.3 ReentrantLock实现独占锁](#3.3 ReentrantLock实现独占锁)
      • [3.4 Semaphore实现共享锁](#3.4 Semaphore实现共享锁)
    • 4.悲观锁
      • [4.1 synchronized](#4.1 synchronized)
      • [4.2 同步方法](#4.2 同步方法)
      • [4.3 ReentrantLock](#4.3 ReentrantLock)
    • 5.等待唤醒机制
      • [5.1 Object](#5.1 Object)
        • [5.1.1 wait/notifyAll方法](#5.1.1 wait/notifyAll方法)
        • [5.1.2 面试题:实现生产者消费者模型](#5.1.2 面试题:实现生产者消费者模型)
      • [5.2 Condition](#5.2 Condition)
        • [5.2.1 await/signal方法](#5.2.1 await/signal方法)
        • [5.2.2 面试题:多线程轮流打印元素](#5.2.2 面试题:多线程轮流打印元素)
      • [5.3 LockSupport](#5.3 LockSupport)
        • [5.3.1 park/unpark方法](#5.3.1 park/unpark方法)
    • 6.共享锁
      • [6.1 Semaphore](#6.1 Semaphore)
        • [6.1.1 acquire/release方法](#6.1.1 acquire/release方法)
        • [6.1.2 面试题:同一时刻有且仅有两个线程同时进入临界区](#6.1.2 面试题:同一时刻有且仅有两个线程同时进入临界区)
      • [6.2 CountDownLatch](#6.2 CountDownLatch)
        • [6.2.1 countDown/await方法](#6.2.1 countDown/await方法)
        • [6.2.2 面试题:某一线程运行前需等待n个线程执行完毕](#6.2.2 面试题:某一线程运行前需等待n个线程执行完毕)
        • [6.2.3 CAS实现CountDownLatch](#6.2.3 CAS实现CountDownLatch)
        • [6.2.4 AQS实现CountDownLatch](#6.2.4 AQS实现CountDownLatch)
      • [6.3 CyclicBarrier](#6.3 CyclicBarrier)
        • [6.3.1 await方法](#6.3.1 await方法)
        • [6.3.2 面试题:多个线程须同时执行](#6.3.2 面试题:多个线程须同时执行)
        • [6.3.3 CAS实现CyclicBarrier](#6.3.3 CAS实现CyclicBarrier)
        • [6.3.4 AQS实现CyclicBarrier](#6.3.4 AQS实现CyclicBarrier)
      • [6.4 Exchanger](#6.4 Exchanger)
    • 7.死锁
      • [7.1 死锁4个必要条件](#7.1 死锁4个必要条件)
      • [7.2 预防死锁](#7.2 预防死锁)
      • [7.3 避免死锁](#7.3 避免死锁)
      • [7.4 面试题:实现死锁](#7.4 面试题:实现死锁)
    • 8.使用场景

一、Java 内存模型

1.定义

JMM(Java Memory Model)是Java虚拟机规范中所定义的一种内存模型,描述了Java程序中变量的访问规则

  • 对象的实例变量和静态变量 位于堆内存空间中,JMM中称之为主内存
  • 每一个线程执行时都需要处理器,每个处理器都有寄存器和Cache来存储局部变量 以及主内存中共享变量的副本 ,JMM中称之为工作内存。注意,现在CPU都是多物理核,每个物理核又可以模拟出多个逻辑核,即处理器,处理器只能操作自己的工作内存,无法访问其他处理器的工作内存。

工作内存的意义:工作内存实际上是处理器的寄存器 ,处理器运算必然需要操作数,直接从内存读操作数然后运算速度较慢,所以先将内存中的数据读到处理器中的寄存器。每个处理器有自己的寄存器。

2.并发安全问题

线程访问主存中的数据时,首先要从主内存读取数据到工作内存寄存器,然后处理器执行运算将运算结果存到寄存器,最后将结果写回主内存,三步操作是独立的,所以有并发安全问题

Java中的多线程并发问题指的是多线程访问共享数据时的执行结果不符合预期,包括多线程执行同一method访问共享数据和多线程执行不同method访问共享数据。

3.原子性&可见性&有序性

Java中并发安全问题包括原子性、可见性、有序性问题:

  1. 原子性:关注的是多条关联命令执行期间,其他线程不能修改主内存中相关的共享数据 ,本质是指令执行顺序与预期不一致 。(而数据库的原子性强调是否同时执行)
    单步 操作的原子性:i++涉及读主内存read、修改工作内存assign、写回主内存store多步 操作的原子性:i++print(i)。以上两种情况都涉及多步关条关联命令,执行期间其他线程如果修改了主内存中的数据,就会出现结果不一致的情况。
  2. 可见性:关注的是工作内存与主内存数据强一致 ,本质是更新工作内存和主内存不是原子性、更新主内存后其他线程无感知
    一是线程修改工作内存后,没有及时更新主内存,导致主内存数据为旧值。二是线程更新主内存后,其他线程没有感知,其工作内存的数据还是旧值。
  3. 有序性:关注的是CPU指令重排序 ,本质是为避免CPU空转,在当前指令等待从内外存获取数据时,利用CPU这段空转时间先执行后续指令
    当两条指令之间无数据依赖关系时,为了充分利用CPU资源,a=xb=1+2,由于第一条指令会访问内存读x,此时CPU空转,所以空转期间会指令第二条指令,然后赋值a,此时就出现了指令重排序。

举个例子,两个线程同时修改共享数据ticket,就会涉及原子性和可见性问题:

java 复制代码
class MyService implements Runnable {
    int ticket = 10;
    public void run() {
        while (ticket > 0){
            ticket--;
            System.out.print(ticket +" ");
        }
    }
}

public static void main(String args[]){// main线程
    MyService service = new MyService();
    Thread thread1 = new Thread(service);
    Thread thread2 = new Thread(service);
    thread1.start();
    thread2.start();
	// 执行结果:8 7 8 5 4 3 2 1 0 6
}

二、线程安全

1.volatile

volatile基于OS的内存屏障指令保证可见性 。还能防止指令重排序,保证有序性

volatile用于修饰共享变量,共享变量更新工作内存后会触发写内存屏障 ,立即将工作内存数据写回主存(虽然从CPU层面仍存在极小的ns级延迟,但是实际工程可完全忽略),并通过硬件的缓存一致性协议标记其他线程工作空间的共享数据失效。load 读指令加读内存屏障在读之前先处理失效队列使得应该失效的缓存失效,检测到缓存失效后重新读主内存,保证可见性。

2.乐观锁

2.1 CAS

CAS基于带lock_cmpxchg指令,因为单条CPU指令不可分割,所以是一个原子操作 ,保证原子性。维护三个字段:V(主内存值),A(工作空间值),B(要修改的结果)。

  1. 工作空间中共享变量的值为A
  2. 读取共享变量在主内存的值V
  3. 比较V==A判断上次读主内存到现在这段时间其他线程是否修改过数据。
  4. 如果相等,则将更新主内存的值为B
  5. 如果不相等,则说明上次读主内存到现在,有其他线程修改了该共享变量。

2.2 Unsafe类

Unsafe类的getAndAddInt()方法是CAS的实现类,在CAS的基础上加入了自旋操作处理更新失败的情况。

  1. do...while自旋 ,读取工作空间中共享变量的值A,如果缓存失效就重新读主内存并赋值给A
  2. 执行CAS指令,传入VAB,如果执行成功结束循环,返回旧值A
  3. 如果CAS失败即V!=A,自旋到第一步重新获取A
java 复制代码
public final class Unsafe {
    // 将共享变量的值加delta
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        // do...while就是自旋操作,当CAS成功以后,循环结束
        do {
            // A
            v = getIntVolatile(o, offset); 
        // 调用本类的weakCompareAndSetInt方法实现比较在交换; v: 相当于旧值A, v + delta:新值B
        } while (!compareAndSetInt(o, offset, v, v + delta));
        return v;
    }
    
    // 本地方法,调用CPU指令实现CAS
    public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
}

2.3 原子类

java.util.concurrent.atomic包中提供了原子操作类,类中的方法基于Unsafe类的CAS方法修改对象的值,所以每方法都是原子操作

CAS能保证数据能立即刷回主内存,但不保证其他线程工作内存的数据失效,因此原子类中使用volatile保证可见性

2.3.1 AtomicInteger
java 复制代码
public class AtomicInteger extends Number implements java.io.Serializable {
    // CAS算法的实现类
    private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
    // 表示变量值在内存中的偏移量地址,也就是V
    private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
    // 修改的值,volatile保证可见性
    private volatile int value;
    
    //原子操作,getAndAddInt()返回的是旧值,所以返回的是自增1后的值就要+1
    public final int incrementAndGet() {
        // this表示当前AtomicInteger对象,1表示要增加的值
        return U.getAndAddInt(this, VALUE, 1) + 1;// 调用Unsafe类中的getAndAddInt方法  
    }
	public final int get() //获取当前的值
	public final int getAndSet(int newValue)//获取当前的值,并设置新的值
	public final int getAndIncrement()//获取当前的值,并自增
	public final int getAndDecrement() //获取当前的值,并自减
	public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
	boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
	public final void lazySet(int newValue)//最终设置为newValue, lazySet 提供了一种比 set 方法更弱的语义,可能导致其他线程在之后的一小段时间内还是可以读到旧的值,但可能更高效。
}
2.3.2 AtomicReference

基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。

java 复制代码
class Person {
    private String name;
    private int age;
}

// 创建 AtomicReference 对象并设置初始值
AtomicReference<Person> ar = new AtomicReference<>(new Person("SnailClimb", 22));

// 更新值
Person updatePerson = new Person("Daisy", 20);
ar.compareAndSet(ar.get(), updatePerson);
2.3.3 AtomicIntegerFieldUpdater

如果需要原子更新类里的单个字段时 ,需要用到对象的属性修改类型原子类,要更新的字段必须使用volatile保证可见性,因为CAS本身不保证可见性。

java 复制代码
class Person {
    private String name;
    volatile int age;
}

// 创建 AtomicIntegerFieldUpdater 对象
AtomicIntegerFieldUpdater<Person> ageUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");

// 创建 Person 对象
Person person = new Person("SnailClimb", 22);

// 更新 age 字段
ageUpdater.incrementAndGet(person); // 自增
System.out.println("After Increment: " + person);

2.4 ABA问题

如果一个变量V初次读取的时候是A值,并且CAS时仍然是A值,那就能说明共享变量没有被其他线程修改过了吗?

不能,因为在这段时间共享变量的值可能经过多次修改,最后又改回A,CAS就会误认为它从来没有被修改过。这就是ABA问题。

ABA 问题的解决思路是在变量前面追加上版本号或时间戳

2.5 CLH

CAS锁会自旋,当一个线程一直被其他线程抢占 而一直获取不到锁时,会有饥饿 问题,所以CLH是在CAS的基础上设计的一种公平锁

为所有竞争共享变量的线程维护一个等待队列 ,当线程要获取锁时,会CAS tail指针 ,使tail指向该线程对应的结点<Thread,locked>,然后入队,成为新的队尾结点。只有队头线程会自旋尝试获取锁,释放锁后修改自己结点的locked状态为false。其他线程自旋+CAS访问前驱结点中存储的locked状态,当locked状态为false才CAS获取锁。

  • CLH 锁初始化时Tail会指向一个状态为false的空节点,如图1所示。
  • 当Thread1请求获取锁时,Tail节点指向Thread1对应的节点。Thread1检查到上一个节点locked状态为false,就CAS成功获取到锁,可以执行相应的逻辑了,如图2所示。
  • 当Thread2请求获取锁时,Tail节点指向Thread2对应的节点,Thread2检查到上一个节点状态locked为True,无法获取到锁,于是开始自旋上一个节点的状态,如图3所示。
  • 当Thread1释放锁时,会将状态变量置locked为False,如图4所示。
  • Thread2自旋检查到上一个节点locked状态变为False,则CAS获取锁成功,如图5所示。

3.AQS

CLH锁的所有结点都会自旋检查前驱结点的执行状态,因此存在大量无意义的CPU开销,所以AQS是在CLH的基础上设计的一种高性能 锁模版,封装了CAS获取锁失败后 的等待逻辑以及释放锁后 的同步逻辑,而获取锁、释放锁的条件和过程则由子类自定义。

3.1 结构

java 复制代码
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;

	abstract static class Node {
		// 前驱后继结点
        volatile Node prev;      
        volatile Node next;
        // 结点对应的线程      
        Thread waiter;      
    }
}

AQS维护双向等待队列 记录等待的线程,其中head指针指向持有锁的线程、tail指针指向队尾用于插入新的线程。volatile state字段用来记录锁的使用情况,具体意义由实现类自定义,例如ReentrantLock中表示锁的重入次数、Semaphore中表示锁的数量。

3.2 实现逻辑

AQS是一个抽象类 ,定义了获取锁和释放锁后线程与双向队列交互的所有操作,并定义了共享锁和独占锁两种模版,仅仅没有定义获取锁和释放锁时state的逻辑 ,需要子类根据不同的业务实现。

3.2.1 独占锁模版

AQS的独占锁入口是acquire()、release()方法,并对外暴露tryAcquire()、tryRelease()模板方法用于编写state的逻辑:

java 复制代码
// 获取锁
public final void acquire(int arg) {
    // 先尝试抢锁
    if (!tryAcquire(arg) &&
        // 抢锁失败,执行addWaiter入队并判断是否在队首,是的话再抢锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// 线程进入等待队列
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 前驱结点
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        // CAS tail指针入队
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 修改prev指针指向队尾
    node.prev = pred;
    // 修改前驱结点的next指针指向自己
    pred.next = node;
    return node;
}

// 线程判断是否有资格抢锁,有的话就抢锁
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    for (;;) {
    	// 当前结点的前驱结点
        final Node p = node.prev;
        // 只有head指向前驱结点,当前结点才有资格tryAcquire(arg)尝试抢锁
        if (p == head && tryAcquire(arg)) {
            // 抢到锁,head指向自己
            head = node;
            // 断开旧head,方便GC
            p.next = null; 
            return interrupted;
        }
        // head.next在非公平条件下抢锁失败或当前结点不是head.next,则park()进入等待队列
        if (shouldParkAfterFailedAcquire(p, node) &&
            LockSupport.park(this))
            interrupted = Thread.interrupted();
    }
}

// 释放锁
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0){
			int ws = h.waitStatus;
		    if (ws < 0)
		        compareAndSetWaitStatus(h, ws, 0);
		    // 获取head.next结点 
		    Node s = h.next;
		    if (s == null || s.waitStatus > 0) {
		        s = null;
		        for (Node t = tail; t != null && t != h; t = t.prev)
		            if (t.waitStatus <= 0)
		                s = t;
		    }
		    if (s != null)
		    	// 唤醒head.next结点
		        LockSupport.unpark(s.thread);
		}
        return true;
    }
    return false;
}

tryAcquire() 抢锁 :AQS有公平锁和非公平锁两种实现,非公平锁直接CAS state尝试获取锁,非公平锁会首先检查等待队列是否为空,为空才尝试获取锁。

addWaiter() 添加线程到等待队列 :获取锁失败后,创建NodeCAS修改tail指向自己,然后修改prev及前驱节点的next加入等待队列。

acquireQueued() 判断抢锁资格并抢锁head指向队列中持有锁的结点,AQS抛弃了无意义的自旋,队列中只有head.next结点才能CAS state尝试获取锁,获取成功将prev置空,便于垃圾回收器回收前驱结点,并修改head。没有获取锁的资格或获取锁失败则调用LockSupport.park()放弃CPU进入WATTING状态。

release() 释放锁 :根据next获取后继结点的线程ID调用LockSupport.unpark(threadId)唤醒后继结点对应的线程。后继结点被唤醒后检查head.next是不是自己,转到acquireQueued()

3.2.2 共享锁模版

AQS的共享锁入口是acquireShared()、releaseShared()方法,并对外暴露tryAcquireShared()、tryReleaseShared()模板方法用于编写state的逻辑:

java 复制代码
// 获取锁
public final void acquireShared(int arg) {
    // 先尝试获取资源,tryAcquireShared<0代表获取资源失败,doAcquireShared入队
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

// 线程进入等待队列
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 前驱结点
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        // CAS tail指针入队
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 修改prev指针指向队尾
    node.prev = pred;
    // 修改前驱结点的next指针指向自己
    pred.next = node;
    return node;
}

// 线程判断是否有资格抢锁,有的话就抢锁
private void doAcquireShared(int arg) {
	boolean interrupted = false;
    // 线程进入等待队列,和独占锁共用addWaiter方法
    final Node node = addWaiter(Node.SHARED);
    for (;;) {
    	// 当前结点的前驱结点
        final Node p = node.prev;
        // 只有head指向前驱结点,当前结点才有资格tryAcquireShared(arg)尝试抢锁
        if (p == head) {
            int r = tryAcquireShared(arg);
            // r >= 0 获取资源成功
            if (r >= 0) {
                // 设置头结点 + 传播唤醒后继
                setHeadAndPropagate(node, r);
                // 抢到锁,head指向自己
            	head = node;
                // 断开旧head,方便GC
                p.next = null;
                // 还有剩余资源,需要继续唤醒后继结点
                if(r>0 && node.next != null){
                	doReleaseShared();
                }
                return;
            }
        }
        // head.next在非公平条件下抢锁失败或当前结点不是head.next,则park()进入等待队列
        if (shouldParkAfterFailedAcquire(p, node) &&
            LockSupport.park(this))
            interrupted = Thread.interrupted();
    }
}



// 释放锁
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
// 唤醒后继结点
private void doReleaseShared() {
    Node h = head;
    if (h != null && h != tail) {
        int ws = h.waitStatus;
        if (ws == Node.SIGNAL) {
            if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                continue;
            // 唤醒head.next结点    
            LockSupport.unpark(h);
        }
        else if (ws == 0 &&
                 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
            continue;
    }
    if (h == head)
        break;
}

tryAcquireShared() 抢锁 :AQS有公平锁和非公平锁两种实现,非公平锁直接CAS state尝试获取锁,非公平锁会首先检查等待队列是否为空,为空才尝试获取锁。

addWaiter() 添加线程到等待队列 :获取锁失败后,创建NodeCAS修改tail指向自己,然后修改prev及前驱节点的next加入等待队列。

doAcquireShared() 判断抢锁资格并抢锁head指向队列中持有锁的结点,AQS抛弃了无意义的自旋,队列中只有head.next结点才能CAS state尝试获取锁,获取成功将prev置空,便于垃圾回收器回收前驱结点,并修改head检查锁是否还有剩余,有的话唤醒后继结点(唯一与独占锁的不同) 。没有获取锁的资格或获取锁失败则调用LockSupport.park()放弃CPU进入WATTING状态。

releaseShared() 释放锁 :根据next获取后继结点的线程ID调用LockSupport.unpark(threadId)唤醒后继结点对应的线程。后继结点被唤醒后检查head.next是不是自己,转到doAcquireShared()

3.3 ReentrantLock实现独占锁

ReentrantLock是AQS的独占锁实现类,定义state字段含义为锁的重入次数 ,仅当state==0时表示当前没有线程持有锁,state>0表示锁被占用,同时基于state实现了可重入机制,持有锁的线程可state++重复获取锁,仅当state--至0时释放锁。

  • tryAcquire() 模版方法实现:定义获取锁的逻辑为CAS(state,state+1),条件为state==0,成功返回true表示获取锁成功,失败返回false表示获取锁失败。
  • tryRelease() 模版方法实现:定义释放锁的逻辑为state-1,条件为state==0,成功返回true表示释放锁成功,失败返回false表示锁重入次数-1。
java 复制代码
protected boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 1、获取 AQS 中的 state 状态
    int c = getState();
    // 2、如果 state 为 0,证明锁没有被其他线程占用
    if (c == 0) {
        // 2.1、通过 CAS 对 state 进行更新
        if (compareAndSetState(0, acquires)) {
            // 2.2、如果 CAS 更新成功,就将锁的持有者设置为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 3、如果当前线程和锁的持有线程相同,说明发生了「锁的重入」
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 3.1、将锁的重入次数加 1
        setState(nextc);
        return true;
    }
    // 4、如果锁被其他线程占用,就返回 false,表示获取锁失败
    return false;
}

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 1、判断持有锁的线程是否为当前线程
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 2、如果 state 为 0,则表明当前线程已经没有重入次数。因此将 free 更新为 true,表明该线程会释放锁。
    if (c == 0) {
        free = true;
        // 3、更新持有资源的线程为 null
        setExclusiveOwnerThread(null);
    }
    // 4、更新 state 值
    setState(c);
    return free;
}

3.4 Semaphore实现共享锁

Semaphore是AQS的共享锁实现类,可实现多线程同时持有锁,定义state字段含义为锁的剩余容量 ,仅当state==0时表示当前锁已没有余量。

  • tryAcquireShared()模版方法实现:首先获取state判断state>0,那么表示锁还有剩余,CAS修改state=state-1获取锁。
  • tryReleaseShared():CAS更新state++释放锁。
java 复制代码
final int tryAcquireShared(int acquires) {
    for (;;) {
        // 1、获取可用资源数量。
        int available = getState();
        // 2、计算剩余资源数量。
        int remaining = available - acquires;
        // 3、如果剩余资源数量 < 0,则说明资源不足,直接返回;如果 CAS 更新 state 成功,则说明当前线程获取到了共享资源,直接返回。
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

4.悲观锁

我们将访问共享数据的那段代码称为临界区 。悲观锁认为访问共享数据的过程中其他线程一定会修改 共享数据,所以访问共享数据前先加互斥锁 ,并在临界区代码执行过程中始终持有锁,避免其他线程同时进入临界区,直到退出临界区才释放锁。

简单理解,如果要对某个共享变量用悲观锁,所有更新 该共享数据的代码块执行前都要获取锁,保证了原子性。

同时,悲观锁的成员变量又有volatile修饰,保证了锁的可见性、有序性。

4.1 synchronized

synchronized是由JVM实现的管程,获取锁和释放锁的逻辑都由JVM封装并自动执行。

  1. 偏向锁:当只有一个线程反复获取锁时,直接修改堆中对象头锁记录字段为当前线程ID。当修改锁记录字段时发现与当前线程ID不一致则升级为轻量级锁。
  2. 轻量级锁:当少量线程竞争锁时,CAS修改堆中对象头锁记录 字段上锁。获取锁失败时自旋,如果自旋超时,为避免大量线程自旋消耗CPU,升级为重量级锁。
  3. 升级为重量级锁后,JVM会为锁对象实例化一个Monitor对象,线程在获取重量级锁时,首先尝试获取信号量Monitor中的Owner字段 设置为当前线程ID。如果设置成功,则获取锁;如果失败,JVM将线程记录到Monitor对象的阻塞队列中,然后调用OS命令将线程阻塞并加入OS阻塞队列。
  4. 当持有锁的线程执行完临界区 后,会释放锁。JVM首先CAS 将Monitor中的Owner字段设置为null,然后遍历Monitor的阻塞队列获取线程ID,调用OS命令从OS的阻塞队列中唤醒一个或多个线程,被唤醒的线程进入就绪态准备重新竞争锁。整个获取锁-阻塞-释放锁-唤醒阻塞线程的过程完全由JVM控制,业务中没有手动的获取锁和释放锁的代码,所以整个过程从线程层面来说是被动的
  5. 当持有锁的线程因某些资源无法获得时,需要手动 调用wait()主动释放锁 ,记录到Monitor的等待队列中,然后调用OS命令将线程加入OS等待队列。直到持有资源的线程释放资源后发出notify(),线程进入就绪态准备重新竞争锁。

synchronized是JVM实现的,依赖JVM堆内存中的Monitor对象,多服务器 下Java进程相互独立,无法共享堆内存,所以要用分布式锁

java 复制代码
class MyService implements Runnable {
    int ticket = 10;
    static String LOCK = "LOCK";
    public void run() {
        while (true){
            synchronized (LOCK){
                if(ticket > 1){
	                ticket--;
	                System.out.print(ticket+" "+Thread.currentThread().getName()+", ");
				}else{
					break;
				}
            }
        }
    }
}

4.2 同步方法

同步方法是在方法上加synchronized修饰符,锁对象默认:

  • 对于非静态方法:锁对象是堆中的实例化对象this
  • 对于静态方法:锁对象是方法区中的Class字节码文件对象,同一个类在方法区只有一个字节码文件对象。

4.3 ReentrantLock

Lock接口下的ReentrantLock基于信号量 实现,需要手动lock()、tryLock()上锁和unlock()释放锁。

ReentrantLock还实现了可重入,即持有锁的线程可以在持有锁期间多次获取锁,使用count属性来记录持有锁的次数。

java 复制代码
class MyService implements Runnable {
    ReentrantLock lock = new ReentrantLock();
    
    int ticket = 10;
    static String LOCK = "LOCK";
    public void run() {
        while (ticket > 1){
            lock.lock();
            ticket--;
            System.out.print(ticket+" "+Thread.currentThread().getName()+", ");
            lock.unlock();
        }
    }
}

5.等待唤醒机制

等待唤醒机制式用来解决多线程间的协作问题。是进程同步机制的一种实际应用场景。

5.1 Object

5.1.1 wait/notifyAll方法

Object类中的wait()、notify()依赖于synchronized锁,实现了线程间的等待和唤醒机制,必须在synchronized代码块内调用。

  • wait()RUNNING → WAITING,线程获取synchronized锁后由于其他资源无法取得,主动调用wait(),JVM会将线程放入锁对象对应的Monitor的等待队列中,并调用OS指令将线程状态修改为WAITING,进入OS等待队列,释放synchronized锁。
  • notify()、notifyAll()WAITING → BLOCKED,持有资源的线程用完资源后,会调用notifyAll(),JVM会遍历Monitor的等待队列获取线程ID,并依次调用OS指令将线程从OS的等待队列移出,并放入阻塞队列。当有多个资源时,必须用notifyAll()。

思考:

  1. 为什么推荐用notifyAll():队列是先进先出,所以notify()唤醒的是队首元素,又因为需要不同资源的线程都在同一个Monitor等待队列中,因此notify()唤醒的可能不是需要当前资源的线程,多对生产消费者下,生产者和消费者对应关系就会出错,导致部分线程永远留在等待队列。
  2. 为什么notifyAll()后线程状态是阻塞态:因为负责执行notifyAll()的线程此时仍持有锁,所以直接修改为就绪态没有意义。要等线程执行完synchronized代码块后释放锁,此时从阻塞队列唤醒一个线程才能竞争锁。
5.1.2 面试题:实现生产者消费者模型
java 复制代码
class Desk {
    public static int foodFlag = 0;
    public static Object LOCK = new Object();
}

class Cooker extends Thread{
    @Override
    public void run() {
        while (true){
            synchronized (Desk.LOCK){// 保证多个生产者之间串行访问
                if (Desk.foodFlag == 0){
                    Desk.LOCK.wait();// 保证消费者和生产者之间的关系
                }else {
                    Desk.foodFlag++;
                    Desk.LOCK.notifyAll();
                }
            }
        }
    }
}

class Customer extends Thread{
    @Override
    public void run() {
        while (true){
            synchronized (Desk.LOCK){// 保证多个消费者之间串行访问
                if (Desk.foodFlag == 0){
                    Desk.LOCK.wait();// 保证消费者和生产者之间的关系
                }else {
                    Desk.foodFlag--;
                    Desk.LOCK.notifyAll();
                }
            }
        }
    }
}

5.2 Condition

5.2.1 await/signal方法

Condition类中的await()、signal()依赖于ReentrantLock锁,实现了线程间的等待和唤醒机制,必须在获取ReentrantLock锁后调用。与Object不同,ReentrantLock可以实例化多个Condition,Condition与资源可以是一对一关系 ,因此唤醒时只需要唤醒特定Condition等待队列的一个线程就行了,避免了无意义的唤醒,效率大大提高

  • await()RUNNING → WAITING,线程获取ReentrantLock锁后由于其他资源无法取得,主动调用await(),JVM会将线程放入Condition对象的等待队列中,并调用OS指令将线程状态修改为WAITING,进入OS等待队列,释放ReentrantLock锁。
  • signal()WAITING → BLOCKED,持有资源的线程用完资源后,会调用signal(),JVM会获取Condition等待队列队首的线程ID,调用OS指令将线程从OS的等待队列移出,并放入阻塞队列。
5.2.2 面试题:多线程轮流打印元素
java 复制代码
public class Main {
    static class MyThread extends Thread {
        int tid;
        static int id = 0;
        volatile static int i = 0;
        static ReentrantLock lock = new ReentrantLock();
        static HashMap<Integer, Condition> map = new HashMap<>();

        public MyThread(int tid){
            this.tid = tid;
            lock.lock();
            if(map.size()==0){
                map.put(0,lock.newCondition());
                map.put(1,lock.newCondition());
                map.put(2,lock.newCondition());
            }
            lock.unlock();
        }

        @Override
        public void run() {
            while (true){
                // 读i在锁外面就要volatile,如果这段代码移到lock里面就不需要volatile了
                if(i==100) break;
                try {
                    lock.lock();
                    if (id==tid){
                        i++;
                        System.out.println(tid+" "+i);
                        id = (id+1)%3;
                    }else{
                        map.get(tid).await();
                    }
                }catch (Exception e){}finally {
                    map.get((tid+1)%3).signal();
                    lock.unlock();
                }
            }
        }
    }

    public static void main(String[] args) {
        new MyThread(0).start();
        new MyThread(1).start();
        new MyThread(2).start();
    }
}

5.3 LockSupport

5.3.1 park/unpark方法

Condition底层调用的就是LockSupport,park()、unpark(threadId)底层就是调用OS的命令操作OS的等待队列,因此不需要任何前置条件就能使用。由于LockSupport的成员变量中用于记录 线程ID的等待队列,因此唤醒时要额外传一个threadId,这样才知道唤醒哪个线程。

  • park()RUNNING → WAITING,直接调用OS指令将线程状态修改为WAITING,进入OS等待队列。
  • unpark(threadId)WAITING → RUNNABLE,持有资源的线程用完资源后,调用unpark()并指定线程id,直接调用OS指令将线程从OS的等待队列移出,变为就绪态。

思考:

  1. 为什么unpark()会将线程变为就绪态,而执行signal()后会变为阻塞态:unpark()直接调用的OS的唤醒命令,就是直接变为就绪态,而signal()依靠锁,考虑到此时锁并未释放,就绪态肯定抢不到锁,所以内部调用unpark()后会再将线程阻塞。

6.共享锁

6.1 Semaphore

SemaphoreAQS共享锁的实现,用于控制访问特定资源的线程数 ,可以理解成锁可以被多个线程同时持有,可以通过构造方法设置是否是公平锁,默认非公平锁

6.1.1 acquire/release方法

Semaphore默认构造AQSstate值为permitsacquire()方法尝试使用CAS操作去修改state的值state=state-1,如果获取锁失败则会创建一个Node节点加入等待队列,挂起当前线程。

java 复制代码
// permits 表示许可获取锁的最大线程数量
public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

public void acquire() throws InterruptedException// 获取锁
public void release()// 释放锁
6.1.2 面试题:同一时刻有且仅有两个线程同时进入临界区
java 复制代码
public class CarThreadRunnable implements Runnable {

    // 创建一个Semaphore对象,限制只允许2个线程获取到许可证
    private Semaphore semaphore = new Semaphore(2) ;

    @Override
    public void run() {// 这个run只允许2个线程同时执行
        try {
            // 获取许可证
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + "----->>正在经过十字路口");

            // 模拟车辆经过十字路口所需要的时间
            Random random = new Random();
            int nextInt = random.nextInt(7);
            TimeUnit.SECONDS.sleep(nextInt);

            System.out.println(Thread.currentThread().getName() + "----->>驶出十字路口");

            // 释放许可证
            semaphore.release();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

6.2 CountDownLatch

6.2.1 countDown/await方法

CountDownLatch是AQS共享锁 的实现,会将AQS的volatile state值为count,当线程调用countDown()时,其实使用了CAS操作修改state的值state=state-1,直至最后一个线程修改state=0,那么该线程会唤醒调用await()进入等待队列的所有线程并继续执行。

java 复制代码
public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象
public void await() throws InterruptedException// 让当前线程等待
public void countDown()// 计数器进行减1

应用场景 :1.某一线程在开始运行前等待n个线程执行完毕 。2.实现多个线程并行执行任务(多个线程调用coundownlatch.await(),当计数器变为0时,多个线程会被同时唤醒)

6.2.2 面试题:某一线程运行前需等待n个线程执行完毕
java 复制代码
public class CountDownLatchDemo01 {

    public static void main(String[] args) {

        //  1. 创建一个CountDownLatch对象
        CountDownLatch countDownLatch = new CountDownLatch(2) ;// CountDownLatch中的计数器的默认值就是2

        //  2. 创建任务类对象,并且把这个CountDownLatch对象作为构造方法的参数进行传递(人物类实现了runnable接口,有一个成员变量CountDownLatch,并重写了run()方法)
        CountDownLatchThread01 countDownLatchThread01 = new CountDownLatchThread01(countDownLatch) ;
        CountDownLatchThread02 countDownLatchThread02 = new CountDownLatchThread02(countDownLatch) ;

        //  3. 创建线程对象,并启动线程
        Thread t1 = new Thread(countDownLatchThread01);
        Thread t2 = new Thread(countDownLatchThread02);
        t1.start();// run()输出:10秒以后执行了CountDownLatchThread01......
        t2.start();// run()输出:3秒以后执行了CountDownLatchThread02......

        //  4. 在主线程中调用 CountDownLatch中的await让主线程处于阻塞状态
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("主线程执行了.... 程序结束了......");
    }
}
6.2.3 CAS实现CountDownLatch

AtomicInteger实现CountDownLatch的效果,唯一的区别是CountDownLatch会主动进入等待队列等待而下述方式会一直轮询占用CPU:

java 复制代码
public class MyLock {
    static AtomicInteger state = new AtomicInteger(3);

    class Thread1 extends Thread{
        @Override
        public void run(){
            state.addAndGet(-1);
        }
    }

    class Thread2 extends Thread{
        @Override
        public void run(){
            while (true){
                if (state.get()==0) {
                    System.out.println("执行");
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3, true);
        new MyLock().new Thread2().start();
        new MyLock().new Thread1().start();
        new MyLock().new Thread1().start();
        Thread.sleep(3000);
        new MyLock().new Thread1().start();
    }
}
6.2.4 AQS实现CountDownLatch
java 复制代码
public class MyLock {
    static int state = 2;
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    class Thread1 extends Thread{
        @Override
        public void run() {
            lock.lock();
            state--;
            if(state==0) condition.signal();
            lock.unlock();
        }
    }

    class Thread2 extends Thread{
        @Override
        public void run() {
            lock.lock();
            while(state!=0) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("do");
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new MyLock().new Thread2().start();
        Thread.sleep(1000);
        new MyLock().new Thread1().start();
        new MyLock().new Thread1().start();
    }
}

6.3 CyclicBarrier

6.3.1 await方法

CyclicBarrier让一组线程到达一个屏障(也可以叫同步点)时调用await()被阻塞,count=count-1,直到最后一个线程到达屏障并阻塞后,count==0就会唤醒所被屏障拦截的线程继续执行。

java 复制代码
public CyclicBarrier(int parties, Runnable barrierAction)// 用于在线程到达屏障时,优先执行barrierAction
public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
6.3.2 面试题:多个线程须同时执行
java 复制代码
public class EmployeeThread extends Thread {

    // CyclicBarrier类型的成员变量
    private CyclicBarrier cyclicBarrier;
    public EmployeeThread(CyclicBarrier cyclicBarrier) {// 使用构造方法对CyclicBarrier进行初始化
        this.cyclicBarrier = cyclicBarrier ;
    }

    @Override
    public void run() {

        try {
            // 模拟开会人员的随机到场
            Thread.sleep((int) (Math.random() * 1000));
            System.out.println(Thread.currentThread().getName() + " 到了! ");
            cyclicBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class MettingThread extends Thread {

    @Override
    public void run() {
        System.out.println("好了,人都到了,开始开会......");
    }
}

public class CyclicBarrierDemo01 {
    public static void main(String[] args) {
        // 创建CyclicBarrier对象
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5 , new MettingThread()) ;

        // 创建5个EmployeeThread线程对象,把第一步创建的CyclicBarrier对象作为构造方法参数传递过来
        EmployeeThread thread1 = new EmployeeThread(cyclicBarrier) ;
        EmployeeThread thread2 = new EmployeeThread(cyclicBarrier) ;
        EmployeeThread thread3 = new EmployeeThread(cyclicBarrier) ;
        EmployeeThread thread4 = new EmployeeThread(cyclicBarrier) ;
        EmployeeThread thread5 = new EmployeeThread(cyclicBarrier) ;

        // 启动5个员工线程
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
    }
}
6.3.3 CAS实现CyclicBarrier

AtomicInteger实现CyclicBarrier的效果,唯一的区别是CyclicBarrier会主动进入等待队列等待而下述方式会一直轮询占用CPU:

java 复制代码
public class MyLock {
    static AtomicInteger state = new AtomicInteger(3);


    class Thread1 extends Thread{
        @Override
        public void run(){
            state.getAndAdd(-1);
            while (true){
                if (state.get()==0) {
                    System.out.println("执行");
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3, true);
        new MyLock().new Thread1().start();
        new MyLock().new Thread1().start();
        Thread.sleep(3000);
        new MyLock().new Thread1().start();
    }
}
6.3.4 AQS实现CyclicBarrier
java 复制代码
public class MyLock {
    static int state = 2;
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    class Thread1 extends Thread{
        @Override
        public void run() {
            lock.lock();
            state--;
            if(state==0) condition.signal();
            else {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("do");
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new MyLock().new Thread1().start();
        Thread.sleep(1000);
        new MyLock().new Thread1().start();
    }
}

6.4 Exchanger

Exchanger用于线程间的数据交换

java 复制代码
public Exchanger()// 构造方法
public V exchange(V x)// 进行交换数据的方法,参数x表示本方数据 ,返回值v表示对方数据

两个线程通过exchange方法交换数据时,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange()方法,当两个线程都执行exchange()方法后,这两个线程就可以交换数据。

java 复制代码
public class ManThread extends Thread {
    // 定义Exchanger类型的变量
    private Exchanger<String> exchanger ;
    private String name ;
    public ManThread(Exchange<String> exchanger , String name) {
        super(name);
        this.name = name ;
        this.exchanger = exchanger ;
    }

    @Override
    public void run() {
        try {
            String result = exchanger.exchange("钻戒");
            System.out.println(name + "---->>把钻戒给媳妇");
            System.out.println(name + "---->>得到媳妇给的" + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class WomanThread extends Thread {
    // 定义Exchanger类型的变量
    private Exchanger<String> exchanger ;
    private String name ;
    public WomanThread(Exchanger<String> exchanger , String name) {
        super(name) ;
        this.name = name ;
        this.exchanger = exchanger ;
    }

    @Override
    public void run() {
        try {
            String result = exchanger.exchange("铝戒");
            System.out.println(name + "---->>把铝戒给老公");
            System.out.println(name + "---->>得到老公给的" + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ExchangerDemo01 {
    public static void main(String[] args) {
        // 创建一个Exchanger对象
        Exchanger<String> exchanger = new Exchanger<String>() ;
        // 创建一个ManThread对象
        ManThread manThread = new ManThread(exchanger , "杨过") ;
        // 创建一个WomanThread对象
        WomanThread womanThread = new WomanThread(exchanger , "小龙女") ;
        // 启动线程
        manThread.start();
        womanThread.start();
    }
}

7.死锁

7.1 死锁4个必要条件

  1. 互斥条件:锁在同一时刻只能被一个线程占用。
  2. 请求与保持条件:用户线程角度 ,线程在持有 至少一个锁的同时,又请求 其他被占用 的锁,线程不会主动释放锁
  3. 不可剥夺条件:内核线程角度 ,已经分配给线程的锁只能主动释放,不允许被强制剥夺
  4. 循环等待条件:多个线程之间形成一种头尾相接的循环等待锁关系。

多线程以乱序同时申请多把互斥锁,锁申请失败后线程既不会主动释放已持有的锁,也不能被强制剥夺锁,等待关系形成了环结构。

7.2 预防死锁

  1. 破坏请求与保持条件:要求线程一次性申请所有所需锁 ,或者在没有获取全部锁时释放已持有的锁
  2. 破坏循环等待条件:给系统中的资源编号,按顺序申请,避免形成循环等待关系。

7.3 避免死锁

1.银行家算法+安全性算法:银行家算法维护每个资源的已分配数量和可用数量 ,每次都尝试将所需资源一次性分配给线程,然后执行安全性算法检查分配后是否存在安全序列使得每个进程都能顺利完成 ,如果存在安全序列那么才执行分配,否则让该线程等待。但是银行家算法必须预先知道整个进程集合的最大资源需求量,并且在运行过程中不能动态添加或删除进程

7.4 面试题:实现死锁

java 复制代码
public class MyLock {
    static ReentrantLock lock1 = new ReentrantLock();
    static ReentrantLock lock2 = new ReentrantLock();

    class Thread1 extends Thread{
        @Override
        public void run(){
            lock1.lock();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock2.lock();
            System.out.println(1);
            lock2.unlock();
            lock1.unlock();
        }
    }

    class Thread2 extends Thread{
        @Override
        public void run(){
            lock2.lock();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock1.lock();
            System.out.println(2);
            lock1.unlock();
            lock2.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new MyLock().new Thread1().start();
        new MyLock().new Thread2().start();
    }
}

8.使用场景

对于单步操作(当前操作与前后的操作没有很强的关联关系):

  • 读操作用volatile修饰共享变量,读操作就不用加锁,只要确保单步读操作是原子性的就行,因为有volatile所以读到的一定是最新的。(例如读共享变量ticket)
  • 写操作加乐观锁,保证自己的写操作不出错就行。(例如ticket++单步操作,前后的操作允许其他线程抢占)

对于多步操作 (多步之间紧密关联,中间如果被其他线程抢占自己执行结果会出问题

  • 悲观锁,将多步操作锁成一个原子操作,保证执行过程中不被其他线程抢占,自己的执行结果就不会出错。(例如ticket++和print(ticket),这两步操作之间如果被其他线程抢占那么输出结果会出问题,所以要合并为一个原子操作)
相关推荐
执笔画流年呀1 小时前
⾃动化测试常⽤函数
java·dubbo
我星期八休息1 小时前
扩展— TCP 全连接队列与 tcpdump 抓包
linux·服务器·开发语言·前端·网络·tcp/ip·tcpdump
疯狂打码的少年1 小时前
【面向对象】UML概述(基本构造块:事物/关系/图)
java
小小代码狗2 小时前
PHP 常用函数与变量参考文档
开发语言·安全·php
一水2 小时前
TTFT优化:一个方案的5次推倒重来
java·ai·状态模式
编程阿峰2 小时前
Python 环境配置(二)安装jupyter、matplotlib、numpy库
开发语言·python·jupyter·numpy·matplotlib
AC赳赳老秦2 小时前
CSDN 技术社区数据采集:OpenClaw 抓取公开技术热帖,生成领域技术热点周报
java·大数据·前端·数据库·python·php·openclaw
磁爆步兵2 小时前
内存分区:程序运行的核心秘密
java·开发语言·jvm