并发基础
- 三大特性
- 原子性,一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行
- 可见性,当一个线程修改了共享变量的值,其他线程能够立刻看到修改的值。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
- 有序性,就是程序的执行的顺序按照代码的顺序执行。为什么有有序性这个问题呢?是因为在java中存在指令重排,指令重拍指的是在不影响程序结果/语义的情况下,JVM可以优化我们的指令顺序。
- JMM,java内存模型,定义了线程和主内存之间的关系,规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量
- Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

- volatile,保证可见行和有序性,但不保证原子性
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量
- CAS
- 比较并交换,针对一个变量,首先读取它当前内存中的实际值与你当前线程中的值是否相同,如果相同,就给它赋一个新值,如果不相同,则把最新内存中的值返回
- 自旋,就是用个do while或者for(;;)循环,用当前线程取到的值和内存中真实的值做比较,如果不一致,则把新的内存值赋值给当前的线程值,然后继续比较,直到能把新值写入成功为止
- CAS的缺陷
- 自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销(空转)
- 只能保证一个共享变量原子操作
- ABA 问题
synchronized
解决线程的并发安全问题,synchronized和lock
- synchronized是JVM内置锁,基于Monitor机制实现。
- Monitor,一般称为“监视器”或者“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发
- Java虚拟机通过一个同步结构支持方法和方法中的指令序列的同步:monitor。
- 同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响
-
synchronized加锁加在对象上,锁对象是如何记录锁状态的?
对象的内存布局
对象在内存中存储的布局可以分为三块区域:对象头(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对象
锁升级过程
偏向锁升到到轻量级锁,轻量级锁升级到重量级锁
- 轻量级锁不可以降级为偏向锁,只可能降级为无锁状态.
AQS,是一个抽象同步框架
主要定义了同步器共同的基础行为:等待队列、条件队列、独占获取、共享获取
AQS定义两种资源共享方式
- Exclusive-独占,只有一个线程能执行,如ReentrantLock
- Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
AQS定义两种队列
- 同步等待队列:主要用于维护获取锁失败时入队的线程
条件等待队列:调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁。
ReentrantLock是一种独占锁,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。
ReentrantLock默认是非公平锁NonfairSync,公平锁与非公平锁的区别:非公平锁会先进行一次CAS。
ReentrantLock流程:- 用到lock方法时,线程然后进行compareAndSetState,如果是第一个线程到达的,此次cas能成功,把state修改为1,然后把独占线程修改为当前线程,这个独占线程主要是用作重入锁判断的。
- 其他线程进入时,cas是失败的,然后会构建一个等待队列的节点Node(是独占模式的节点),然后入队到等待队列,入队后,会当前节点的前驱节点的waitState修改为-1(在等待队列里面,都是由头节点去唤醒下一个节点的,waitState=1表示下一个节点可以被唤醒)。如果再有其他的线程,也是一样的操作,继续入队
- 当持有锁的线程,执行unLock,把state修改为0,把独占线程置为null,然后去unpark头节点,去唤醒头节点的下一个节点线程。
Semaphore,信号量,可用作限流
CountDownLatch,允许一个或多个线程等待,直到其他线程完成操作。
CyclicBarrier,循环屏障,通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行
ReentrantReadWriteLock,读写锁
它内部,维护了一对相关的锁,一个用于只读操作,称为读锁readerLock;一个用于写入操作,称为写锁writerLock。
没有写锁的情况下,多个线程可以同时去读一个资源(读读共享)
如果有写锁的情况下,就不允许其他线程再去读或者写了(读写,写读,写写互斥)。但是如果是持有写锁的线程,可以再次获取读锁。(锁降级的场景,ReentrantReadWriteLock不支持锁升级)
在ReentrantReadWriteLock中,是使用state一个常量记录读写状态的,把int类型的state按位切割,高16位表示读,低16位表示写。高位表示读锁被持有的数量,低位表示写锁重入数
阻塞队列
阻塞队列BlockingQueue 继承了 Queue 接口,是队列的一种。在队列基础上又支持了两个附加操作的队列:
- 支持阻塞的插入方法put: 队列没满的时候是正常的插入,如果队列已满,则阻塞,直至队列空出位置
- 支持阻塞的移除方法take: 队列里有数据会正常取出数据并删除;但是如果队列里无数据,则阻塞,直到队列里有数据
阻塞队列特性
阻塞功能使得生产者和消费者两端的能力得以平衡,当有任何一端速度过快时,阻塞队列便会把过快的速度给降下来。实现阻塞最重要的两个方法是 take 方法和 put 方法。
ArrayBlockingQueue
有界阻塞队列,其内部是用数组存储元素的,初始化时需要指定容量大小,利用 ReentrantLock 实现线程安全。
使用独占锁ReentrantLock实现线程安全,入队和出队操作使用同一个锁对象,也就是只能有一个线程可以进行入队或者出队操作;这也就意味着生产者和消费者无法并行操作,在高并发场景下会成为性能瓶颈。
LinkedBlockingQueue
LinkedBlockingQueue是一个基于链表实现的阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,代表它几乎没有界限
LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素。LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞,添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。
SynchronousQueue
SynchronousQueue是一个没有数据缓冲的BlockingQueue,它的容量为 0,所以没有一个地方来暂存元素,导致每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。
DelayQueue
DelayQueue 是一个支持延时获取元素的阻塞队列,在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。延迟队列的特点是:不是先进先出,而是会按照延迟时间的长短来排序,下一个即将执行的任务会排到队列的最前面。
ForkJoin
是一种基于分治算法,主要包含两部分,一部分是分治任务的线程池 ForkJoinPool,另一部分是分治任务 ForkJoinTask。ForkJoinPool允许其他线程向它提交任务,并根据设定将这些任务拆分为粒度更细的子任务,这些子任务将由ForkJoinPool内部的工作线程来并行执行,并且工作线程之间可以窃取彼此之间的任务
ForkJoinPool
ForkJoinPool 是用于执行 ForkJoinTask 任务的执行池,不再是传统执行池 Worker+Queue 的组合式,而是维护了一个队列数组 WorkQueue(数组)
ForkJoinPool提交任务方式
- 提交异步执行execute
- 等待并获取结果invoke
- 提交执行获取Future结果(submit)
工作窃取
工作窃取,就是允许空闲线程从繁忙线程的双端队列中窃取任务。默认情况下,工作线程从它自己的双端队列的头部获取任务。但是,当自己的任务为空时,线程会从其他繁忙线程双端队列的尾部中获取任务。这种方法,最大限度地减少了线程竞争任务的可能性。

