JMM(Java内存模型)区别于JVM
volatile相关
内存可见性问题:该问题是指当多个线程操作共享数据时,彼此不可见,都是在自己线程所分配的缓存中修改的。
volatile能保证内存可见性,不能保证原子性。因此将共享数据定义成AtomicInteger类型比较合适。这种类型的共享数据通过volatile保证了内存可见性,又通过CAS算法保证了原子性。通过CAS算法保证原子性时有两步,第一步是获取内存中的数据,第二步是原子性的,在修改之前再次获取内存中值,然后比较并替换。
CAS算法比加锁效率高的原因是修改不成功后,会不停再次比较,没有加锁时上下文切换导致的消耗,因此效率高,现在在JDK1.8后,concurrentHashMap也摒弃了之前分段锁的机制,采用了CAS算法。
保证内存可见性手段(即主内存和工作内存间的交互操作):
read(读取):从主内存中读取数据
load(载入):将主内存读取到的数据写入工作内存
use(使用):从工作内存读取数据来计算
assign(赋值):将计算好的值重新赋值到工作内存中
store(存储):将工作内存数据写入主内存
write(写入):将store过去的变量赋值给主内存中的变量
lock(锁定):将主内存变量加锁,标识为线程独占状态
unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
如果变量用volatile修饰了,就会在assign操作后,触发后面的store和write(即内存回写),并在回写之前,调用lock操作,将对应内存区域锁住,写完unlock。
保证内存可见性底层实现: 底层是通过缓存一致性协议和总线嗅探机制实现。一个线程在修改共享变量后,会立即将新值写回主内存。这个写回内存的操作会引起其他CPU里缓存了该内存地址的数据无效。这是因为在写回主内存过程中,数据会经过总线,其他CPU启用了总线嗅探机制,监听总线中数据的变化,若存在数据变化,立马让其他线程工作区中的共享数据失效,之后,需要再次获取新数据,这样确保了数据的一致性。
指令重排序规则:(as-if-serial语义和happens-before原则)
单例模式双重检测锁(存在问题,需要解决)
init:对对象的成员变量进行真正的赋值和调用构造方法,对象初始化完成
pubstatic:对静态变量赋值,把对象(即instance实例)赋值给静态变量
总结:上面两步操作被重排序之后,某一线程在给静态变量赋值之后,但是对象的初始化的方法还没有完成,这导致其他线程在判断对象是否为空时,判断不为空,因此可以拿到instance,但此时拿到的是一个半初始的对象
禁止指令重排原理:
内存屏障实现:JVM使用lock指令这样一个标识实现,CPU在识别这样一个标识后,不把前后代码进行排序,这就是volatile中禁止指令重排的原理,硬件有更复杂的实现。
并发修改异常
在对同一数据源同时做迭代并且修改其中数据操作时,即使是线程安全的容器,也会出现该异常。
解决办法:使用CopyOnWriteArrayList代替。注意:添加操作多时,效率低,因为每次添加时都会进行复制,开销非常的大。并发迭代操作多时可以选择。
CountDownLatch原理
用于闭锁
FutureTask
FutureTask也可用于闭锁,因为它也是等待分线程执行完成之后才去获取结果,行为与CountDownLatch类似。
虚假唤醒
为了避免虚假唤醒问题,obj.wait()应该总是使用在循环中
Condition
单个Lock可能与多个Condition对象关联。在Condition对象中,与wait、notify和notifyAll方法对应的分别是await、signal和signalAll。
Condition实例实质上被绑定到一个锁上。要为特定Lock实例获得Condition实例,可以使用newCondition()方法。
使用Condition对象实现线程按序交替
ReadWriteLock
比独占锁性能更优,这种锁是读时可以并发,写时独占。
线程八锁
关键:
①非静态方法的锁默认为this,静态方法的锁为对应的Class实例。
②某一个时刻内,只能有一个线程持有锁,无论几个方法。
线程池
Fork/Join框架
使⽤多线程可能带来什么问题?
并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序
运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下⽂切换、死锁还有受限于硬件和软件的资源闲置问题。