并发特性

三大特性

可见性

当一个线程修改了共享变量的值,其他线程能够看到修改的值。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。

有序性:即程序执行的顺序按照代码的先后顺序执行。JVM存在指令重排,所以存在有序性问题

什么是指令重排:在不影响程序结果/语义的情况下,JVM可以优化我们的指令顺序

  1. //在我们new一个Person类的时候,会进行3步
  2. //1:现在堆空间开辟一块内存
  3. //2:初始化person对象(调用构造函数)
  4. //3:把初始化好的person对象指向刚创建出来的堆空间地址
  5. //但是在2和3的步骤下,有可能会进行指令重排,
  6. //因为不管是先初始化后指向地址,还是先指向地址后初始化,都不影响结果,
  7. //所以这种情况下是有可能被指令重排的。
  8. Person person=new Person()

原子性:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行

例如:i++就不是原子操作

  1. //i++不是原子操作,他的操作是分成三步的
  2. //1:先获取i的值
  3. //2:i自增+1
  4. //3:把自增的值再赋值给i
  5. //在多线程的情况下,所以i++不是线程安全的,因为第2,3步有可能被打断。
  6. i++

JMM

Java内存模型,用于屏蔽掉各种硬件和操作系统的内存访问差异。JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
截屏2022-04-03 上午10.35.20.png

volatile

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量
  • 所以,volatile可以保证可见性和有序性,但不能保证原子性(i++场景)

java线程

进程和线程

  • 进程基本上相互独立的,而线程存在于进程内
  • 进程可以看作是一个个正在运行的程序,而线程是进程里面的一个个任务

线程的同步互斥

  • 同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,会先进行挂起等待,直到消息到达时才被唤醒
  • 互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源

    Java线程调度是抢占式调度

    抢占式线程调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定,线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。

Java线程的生命周期

  • NEW(初始化状态),就是刚使用new方法,new出来的线程
  • RUNNABLE(可运行状态+运行状态),调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行
  • BLOCKED(阻塞状态),比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态
  • WAITING(无时限等待)
  • TIMED_WAITING(有时限等待)
  • TERMINATED(终止状态),线程要被销毁,释放资源

    Java线程的中断机制

    Java没有直接的方法来停止某个线程,而是提供了中断机制。通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。也就是说被中断的线程可以选择立即停止,也可以选择一段时间后停止,也可以选择不停止。 04-03-1.mp4 (89.57MB)

CAS

比较并交换,针对一个变量,首先读取它当前内存中的实际值与你当前线程中的值是否相同,如果相同,就给它赋一个新值,如果不相同,则把最新内存中的值返回。
CAS 操作是由 Unsafe 类提供支持。

cas的自旋是怎样实现的

就是用个do while或者for(;;)循环,用当前线程取到的值和内存中真实的值做比较,如果不一致,则把新的内存值赋值给当前的线程值,然后继续比较,直到能把新值写入成功为止
截屏2022-03-14 23.09.51.png

CAS缺陷

  • 自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销(空转)
  • 只能保证一个共享变量原子操作
  • ABA 问题
    • 当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值A修改为B,又马上将其修改为A,此时其他线程不感知,还是会修改成功
    • 使用原子引用类AtomicStampedReference解决,AtomicStampedReference里面有reference和stamp,reference即我们实际存储的变量,stamp是版本,每次修改可以通过+1保证版本唯一性

synchronized

  • synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量)。
  • Java虚拟机通过一个同步结构支持方法和方法中的指令序列的同步:monitor。
  • 同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响
  • synchronized同步块方法是由monitorenter和monitorexit指令包括起来的。

    Monitor

    管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。synchronized就是java内置的管程。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。
    管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。

synchronized加锁加在对象上,锁对象是如何记录锁状态的?

对象的内存布局

Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

  • 对象头:
    • Mark Word ,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
    • Klass Pointer,对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
    • 数组长度,如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 4字节
  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

Mark Word是如何记录锁状态的

在Mark Word的最后的2位是锁的标志位,00表示轻量级锁,10表示重量级锁。01表示无锁或者偏向锁,所以会多占用一位(32bit的倒数第三个位置),001表示无锁,101表示偏向锁。

锁升级

  • 偏向锁升到到轻量级锁,轻量级锁升级到重量级锁
  • 轻量级锁不可以降级为偏向锁,只可能降级为无锁状态.

synchronized的锁变化

  • 偏向锁,可能在进入同步块中不存在竞争,那就会偏向某个线程,那这个线程重复进入当前同步块的时候,不会重复加锁解锁的操作,可以直接进入。
  • 轻量级锁,线程间存在轻微的竞争(线程交替执行,临界区逻辑简单的情况下)。一个线程刚开始在竞争锁的时候,没竞争上,会进行一次CAS,如果CAS成功则获得锁。CAS失败会膨胀变成重量级锁。
  • 重量级锁,存在线程竞争激烈场景,膨胀期间会创建一个monitor对象

synchronized锁优化

  • 偏向锁批量重偏向,当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象 的 Thread ID
  • 偏向锁批量撤销,当撤销偏向锁阈值超过 40 次后,jvm 会认为不该偏向,于是整个类的所有对象都会变为不可偏向的(轻量级锁),新建的对象也是不可偏向的
  • 自旋优化,重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程
  • 锁粗化,JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
  • 锁消除,Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁
  • 逃逸分析

AQS

AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器

具备的特性

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

AQS定义两种资源共享方式

  • Exclusive-独占,只有一个线程能执行,如ReentrantLock
  • Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

AQS定义两种队列

  • 同步等待队列:主要用于维护获取锁失败时入队的线程
    • 同步等待队列也称CLH队列,是一种带头节点双向链表数据结构的队列,是FIFO先进先出线程等待队列,线程由原自旋机制改为阻塞机制。
    • 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程
    • 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
    • 通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)
  • 条件等待队列:调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁。
    • 调用await方法阻塞线程;
    • 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)

ReentrantLock

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全,是一种独占锁。
ReentrantLock默认是非公平锁NonfairSync,公平锁与非公平锁的区别:非公平锁会先进行一次CAS。
ReentrantLock流程:

  • 用到lock方法时,线程然后进行compareAndSetState,如果是第一个线程到达的,此次cas能成功,把state修改为1,然后把独占线程修改为当前线程,这个独占线程主要是用作重入锁判断的。
  • 其他线程进入时,cas是失败的,然后会构建一个等待队列的节点Node(是独占模式的节点),然后入队到等待队列,入队后,会当前节点的前驱节点的waitState修改为-1(在等待队列里面,都是由头节点去唤醒下一个节点的,waitState=1表示下一个节点可以被唤醒)。如果再有其他的线程,也是一样的操作,继续入队
  • 当持有锁的线程,执行unLock,把state修改为0,把独占线程置为null,然后去unpark头节点,去唤醒头节点的下一个节点线程。

Semaphore

俗称信号量,就类似于互斥锁,通过设置信号量的资源来控制线程通过数量。Semaphore把state当作资源数,例如state=3,表示同时只允许3个线程通过。

Semaphore流程

  • 例如此时的资源数是3,也就是state是3,有线程调用Semaphore的acquire方法,会进行cas,把state-1,也就是取了一个资源,此时的state-1还是大于0,所以线程可以执行任务
  • 其他的线程过来,都会把stat-1,如果state-1后还是大于0,可以执行任务
  • 直到state=0,表示没有资源了,其他的线程过来,会把线程挂起,然后把线程放入等待队列里面。
  • 直到有线程调用release,release会把state+1,表示把资源放回去,然后去唤醒等待队列的头节点
  • Semaphore除了会唤醒头节点的下一个节点,如果头节点的下一个节点唤醒成功,那下一个节点会继续唤醒下一个节点

CountDownLatch

  • CountDownLatch是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作。
  • CountDownLatch是一种公平锁的实现
  • CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count)由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回

CountDownLatch实现原理

  • 底层基于 AQS 实现,CountDownLatch 构造函数中指定的count直接赋给AQS的state,每次countDown()则都是release(1)减1,当count还是大于0的时候,把线程park,然后入等待队列。最后减到0时unpark线程
  • 而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在await()方法中等待的线程。

CyclicBarrier

字面意思回环栅栏(循环屏障),通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
CyclicBarrier和CountDownLatch比较像,都可以用作等待多个线程到了再统一执行
CyclicBarrier是通过ReentrantLock的”独占锁”和Conditon来实现一组线程的阻塞唤醒的,而CountDownLatch则是通过AQS的“共享锁”实现

ReentrantReadWriteLock

读写锁ReentrantReadWriteLock,它内部,维护了一对相关的锁,一个用于只读操作,称为读锁readerLock;一个用于写入操作,称为写锁writerLock。
没有写锁的情况下,多个线程可以同时去读一个资源(读读共享)
如果有写锁的情况下,就不允许其他线程再去读或者写了(读写,写读,写写互斥)。但是如果是持有写锁的线程,可以再次获取读锁。(锁降级的场景,ReentrantReadWriteLock不支持锁升级)

在ReentrantReadWriteLock中,是使用state一个常量记录读写状态的,把int类型的state按位切割,高16位表示读,低16位表示写。高位表示读锁被持有的数量,低位表示写锁重入数

写锁的获取

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态

写锁的释放

就是把写状态位-1,因为可能重入多次,当写状态位为0时,则释放锁。

读锁的获取

判断当前有没有写锁,有写锁再判断当前线程是不是持有写锁的线程,都不是则跳过。否则获得读锁,设置读锁数+1,再把线程对应的读锁重入数修改

读锁的释放

当前线程的重入数-1,读锁数量-1,如果读锁数量==0,释放读锁成功。 2022.04.03-2.mp4 (115.56MB)

阻塞队列

阻塞队列BlockingQueue 继承了 Queue 接口,是队列的一种。在队列基础上又支持了两个附加操作的队列:

  • 支持阻塞的插入方法put: 队列没满的时候是正常的插入,如果队列已满,则阻塞,直至队列空出位置
  • 支持阻塞的移除方法take: 队列里有数据会正常取出数据并删除;但是如果队列里无数据,则阻塞,直到队列里有数据

    阻塞队列特性

    阻塞功能使得生产者和消费者两端的能力得以平衡,当有任何一端速度过快时,阻塞队列便会把过快的速度给降下来。实现阻塞最重要的两个方法是 take 方法和 put 方法。

    take 方法

    take 方法的功能是获取并移除队列的头结点,通常在队列里有数据的时候是可以正常移除的。可是一旦执行 take 方法的时候,队列里无数据,则阻塞,直到队列里有数据。一旦队列里有数据了,就会立刻解除阻塞状态,并且取到数据

    put 方法

    put 方法插入元素时,如果队列没有满,那就和普通的插入一样是正常的插入,但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间。如果后续队列有了空闲空间,比如消费者消费了一个元素,那么此时队列就会解除阻塞状态,并把需要添加的数据添加到队列中。

ArrayBlockingQueue

ArrayBlockingQueue是最典型的有界阻塞队列,其内部是用数组存储元素的,初始化时需要指定容量大小,利用 ReentrantLock 实现线程安全。
使用独占锁ReentrantLock实现线程安全,入队和出队操作使用同一个锁对象,也就是只能有一个线程可以进行入队或者出队操作;这也就意味着生产者和消费者无法并行操作,在高并发场景下会成为性能瓶颈。
利用了Lock锁的Condition通知机制进行阻塞控制

LinkedBlockingQueue

LinkedBlockingQueue是一个基于链表实现的阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,代表它几乎没有界限,队列可以随着元素的添加而动态增长。
LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素。LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞,添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。

  • takeLock , take锁,从队列取任务
  • Condition notEmpty = takeLock.newCondition(),当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒。
  • putLock ,put锁,往队列放任务
  • notFull = putLock.newCondition(),当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒

SynchronousQueue

SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take。
SynchronousQueue 最大的不同之处在于,它的容量为 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。
SynchronousQueue 的容量不是 1 而是 0,因为 SynchronousQueue 不需要去持有元素,它所做的就是直接传递(direct handoff)。由于每当需要传递的时候,SynchronousQueue 会把元素直接从生产者传给消费者,在此期间并不需要做存储,所以如果运用得当,它的效率是很高的。

DelayQueue

DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口。在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。延迟队列的特点是:不是先进先出,而是会按照延迟时间的长短来排序,下一个即将执行的任务会排到队列的最前面。 2022.04.03-3.mp4 (23.19MB)