一、并发问题的产生和解决
1、多线程的作用
CPU、内存、I/O 设备的速度是有极大差异的,
为了合理利用 CPU 的高性能,平衡这三者的速度差异,
计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
(1)CPU 增加了缓存,以均衡与内存的速度差异-------导致 可见性
问题
(2)操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
------导致 原子性
问题
(3)编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。------导致 有序性
问题
2、Java解决并发问题---JMM Java内存模型
(1)JMM作用:定义程序中各种共享变量的访问规则
(2)基本原则:只要不改变程序执行结果,编译器和处理器可任意优化
(3)规定所有变量都存储在主内存,每条线程有自己的工作内存,线程通信必须经过主内存
下面详述
此部分来源:Java 并发 - 理论基础 | Java 全栈知识体系
二、并发的三大特性
1、原子性---任何时刻只能有一个线程,同步----lock,synchronized
lock-----JMM的lock和unlock
synchronized-----monitorenter monitorexit
2、可见性---一个线程修改了共享变量值,其他线程能立即得知---volatile, synchronized,final
- volatile---保证新值能立即同步到主内存,
每次使用前立即从主内存刷新---保证当前值都是修改后的新值
- synchronized----lock前清空变量值,从主内存中重获取----锁前取新值
unlock前必须将变量同步回主存-----解锁前更新新值
- final----构造方法初始化且构造方法没有把this引用传递出去----其他线程可看见final字段值
3、有序性----所有操作有序执行----volatile,synchronized
volatile---禁止指令重排
synchronized---保证一个变量在同一时刻只允许一条线程对其lock操作
----确保持有同一个锁的两个同步块只能串行进入
三、并发理论
1、CAS---比较并交换
V--现在内存位置的值---新值
A--线程操作前复制到自己工作空间的值----原始值
B--本线程操作后的新值----修改值
当V=A时-----说明没有其他线程对此值修改----B更新V的值
2、JMM---java内存模型
(1)所有的变量(实例字段,静态字段,构成数组对象的 元素,不包括局部变量和方法参数)都存储在主内存中,
每个线程有自己的工作内存,线程的工作内存保存被线程使用到变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的变量。
不同线程之间也不能直接访问对方工作内存中的变量,线程间变量值的传递通过主内存来完成。
(2) 八大原子操作:
主内存中读数据lock---->主存中数据---read-->工作内存---load-->在工作内存中复制一个副本----use--->执行引擎(线程)
unlock <------ 写入主内存<---write---主内存<---store---工作内存 <--assign--线程中操作得到的值
3、as-if-serial---保证单线程执行结果不改变
(1)原理:保证编译器和处理器不会对存在数据依赖关系的操作 进行重排序
(2)重排序存在数据依赖关系的操作---改变执行结果
不存在数据依赖关系的操作---可能会被重排序----只要保证执行结果不变就行
4、happens-before----保证同步的多线程执行结果不变
(1)背景:
JVM会对代码进行编译优化,会出现指令重排序情况,
为了避免编译优化对并发编程安全性的影响,需要一些规则。
(2)目的:
定义一些禁止编译优化的场景,保证并发编程的正确性
(3)原理:
指定某些特定情况的执行顺序规则----保证并发的正确性
(4)happens-before规则
- 程序次序规则:在一个线程内一段代码的执行结果是有序的。【按代码顺序执行】
就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的,不会变。
- 管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!
【前线程unlock先于后一个线程lock】
管程是一种通用的同步原语------synchronized实现
- volatile变量规则:就是如果一个线程先去写一个volatile变量,然后另一个线程去读这个变量,那么线程写操作的结果一定对读的线程可见。 【写操作先于读操作】
- 线程启动规则:在主线程A执行过程中,启动子线程B, 【线程start先于此线程其他操作】
那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
- 线程终止规则:在主线程A执行过程中,子线程B终止,【终止操作后于此线程其他操作】
那么线程B在终止之前对共享变量的修改结果在线程A中可见。
也称线程join()规则。
- 线程中断规则:对线程interrupt()方法的调用先行发生于 被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到线程是否发生中断。
【interrupt先于InterruptedException异常的触发】
- 传递性规则:这个简单的,就是happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。
- 对象终结规则:一个对象的初始化的完成,即构造函数执行的结束一定先发生于它的finalize()方法执行。 【对象初始化先于回收】
【注】
1、interrupt()---不能直接中断线程执行,只能改变中断状态,主要用来唤醒阻塞中线程
详见: Java多线程1---线程基础:创建线程三种方式、线程常用方法、线程的生命周期、线程通信等_@snow'的博客-CSDN博客
2、线程中断规则理解: 如果在线程t1上面调用 interrupted(), t1.interrupted(). 线程一就会停止,不会触发InterruptedException.
5、指令重排
编译器重排序----重排语句的执行顺序
处理器重排序----重拍机器指令的执行顺序
内存重排序----内存系统重排序
6、并发关键字:volatile、synchronized,final
(1)volatile---保证可见性和有序性
- 保证了新值能立即存储到主内存,每次使用前立即从主内存中刷新----立即刷新---可见性
- 禁止指令重排序优化----禁止重排----有序性
(2)synchronized--保证原子性、可见性和有序性
(3)final---保证可见性
四、并发工具类---JUC原子类
1、更新变量---atomic
原子操作类能够用一种用法简单、性能高效、线程安全的方式更新一个变量----Unsafe实现
(1)一般数据类型
AtomicInteger 原子更新整型
AtomicLong 原子更新长整型
AtomicBoolean 原子更新布尔型
(2)数组类型
AtomicIntegerArray 原子更新整型数组中元素
AtomicLongArray 原子更新长整型数组中元素
AtomicReferenceArray 原子更新引用类型数组中元素
(3)引用类型
AtomicReference 原子更新引用类型
AtomicMarkableReference 原子更新带标记位引用类型
AtomicStampedReference 原子更新带版本号引用类型----解决CAS中ABA问题
2、计数器---CountDwnLatch
一个基于执行时间的同步类,允许一个或多个线程等待其他线程完成操作
3、循环屏障---CyclicBarrier
- 一种基于同步到达某个点的信号量触发机制
- 作用:让一组线程到达一个屏障时被阻塞,知道最后一个线程到达屏障才会解除
4、信号量----Semaphore
- 用来控制同时访问特定资源的线程数量---流量控制
- 构造方法中设置一个int值---最大并发数
5、数据交换---Exchnger
- 提供一个同步点,用于线程间进行数据交换-----两个线程在同步点进行数据交换
- 实现方式---exchange()
第一个线程执行exchange()方法后,会阻塞第二个线程执行该方法,
当两个线程都到达同步点时,两线程即可交换数据
五、并发容器
(一)HashMap----ConcuurentHashMap
1、hashMap----非线程安全
(1)底层结构=数组+链表----一维数组的元素是链表
链表----解决key的hash碰撞问题
(2)当链表长度>8时,转成红黑树----几率很小,千万分之一
原因:数组扩容----超过数组的0.75就扩容----扩容2倍
(3)数组初始长度16----0--15
15二进制----1111
每个key会对应一个hashCode-----多位的二进制数
(4)哈希算法:
key的hashcode和15的二进制数进行与操作---得到一个哈希值-----数组下标
下标相同---则用链表形式存在同一个数组下标下
[注]1、key和15进行与操作,得到的值范围[0-15]
2、数组扩容一倍,则key和31进行与操作----值范围[0-31]
2、ConcuurentHashMap---线程安全
线程安全的实现:
(1)JDK1.7 数组+Segment+分段锁
在数组基础上增加了一层Segment,一个Segment对应数组的一段,这样对某段进行的操作只需要锁住对应段,不影响其他段的操作
Segment继承了ReentrantLock并实现了序列化(Seralizable)接口,说明Segment的锁是可重入的。
(2) JDK1.8 数组+链表+红黑树-----与HashMap保持一致,
取消了Segment分段锁的数据结构,取而代之的是Node,Node的value和next都是由volatile关键字进行修饰,可以保证可见性
线程安全实现上,采用CAS+Synchronized替代Segment分段锁
CAS:比较并交换,详见JUC面试题
(二)ArrayList---CopyOnWriteArrayList
1、作用
读写分离,保证读的数据安全
读不阻塞,写阻塞---多线程写排队
2、原理
读---不加锁
写----采用复制新集合的形式---加volatile和lock锁,控制同步,避免多线程copy多个副本
初始化的时候只有一个容器,很长一段时间,这个容器数据、数量等没有发生变化的时候,大家(多个线程),
都是读取(假设这段时间里只发生读取的操作)同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,
但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList 底层实现添加的原理是
先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,
但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
3、适合场景:读多,写少
(三)Set-----CopyOnWriteArraySet---不重复的集合
CopyOnWriteArraySet的“线程安全”机制,和CopyOnWriteArrayList一样,是通过volatile和互斥锁来实现的
内部是用CopyOnWriteArrayList实现的,实现不重复的特性也是直接调用CopyOnWriteArrayList的方法实现的
(四)Queue---ConcuurentLinkedQueue
1>入列出列线程安全,遍历不安全
2>不允许添加null元素
3>底层使用列表与cas算法包装入列出列安全