并发编程(基础)
线程
线程是参与系统调度的最小单位,一个进程中可以并发执行多个线程。
线程的特征
- 异步
- 并行(CPU核数)
JAVA线程使用
- 继承Thread
- 实现Runnable、Callable
- Future、CompleteFuture
- 线程池
JAVA线程状态
如何停止线程
- 线程自行运行结束
- interrupt方法中断线程
- 调用interrupt() 发送一个中断信号给某个线程。
- 在线程的run方法中通过isInterrupted()判断是否被中断,自定义代码处理。
- 如果run方法中有调用阻塞式的方法(比如sleep),当收到interrupt会抛出InterruptedException异常,在catch中自定代码处理中断逻辑。
- 通过定义状态变量,判断变量的值中断线程,在外部调用方法修改某个变量的值,run方法中判断变量的值是否需要停止执行
class MyThread extends Thread{
private volatile boolean isStop = false;
@Override
public void run() {
int i = 0;
while(!isStop){
i++;
}
}
public void setStop(boolean stop){
this.isStop = stop;
}
}
线程安全
-
原子性
Synchronized, Lock, AtomicXXX(CAS)
-
可见性
Synchronized, Lock, Volatile
-
有序性
Synchronized, Lock, Volatile
synchronized
- 修饰方法(实例方法、静态方法)
- 修饰代码块
抢占锁的本质是什么
如何实现互斥?
- 共享资源
- 可以是一个标记,0无锁 1有锁
synchronized锁升级,线程安全和性能之间的平衡设计
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
CAS
-
CAS, compare and swap的缩写,中文翻译成比较并交换。
-
CAS是原子性的操作:如果内存位置的值与预期原值相匹配,那么会将该位置的值更新为新值 。否则,不做任何操作。
/*
@param o 要修改的字段所属的对象
@param offset 字段在对象内的内存位置偏移量
@param expected 期望值(旧的值)
@param update 更新值(新的值)
@return true 更新成功 | false 更新失败
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt( Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong( Object o, long offset, long expected, long update);
线程安全之可见性
- 两个线程有一个共享变量,一个线程修改了该变量的值后,另一个线程是否能实时读取到该变量的修改后的值
可见性的本质
- CPU资源的利用问题
- CPU增加高速缓存
- 操作系统中,多线程通过CPU时间片切换,提升CPU利用率
- 编译器(JVM的深度优化)
CPU高速缓存
-
缓存行
- 缓存行 (Cache Line) 是CPU Cache中的最小单位,CPU Cache由若干缓存行组成,一个缓存行的大小通常是 64 字节(这取决于 CPU)
-
伪共享
- 伪共享是指多个线程同时读写同一个缓存行中的不同变量时导致的CPU缓存失效。
- 如何避免伪共享:对齐填充,java可以使用Contended注解
CPU缓存一致性问题
- 总线锁
- 缓存锁
- 缓存一致性协议(MESI)
- MESI表示缓存的四种状态:
- Modify(修改)
- Exclusive(独占)
- Shared(共享)
- Invalid(无效)
内存屏障
-
CPU层面不知道什么时候允许优化,什么时候不允许不优化,需要人为用指令告诉它
- 读屏障
- 写屏障
- 全屏障
-
防止指令重排
CPU优化之路
JMM(Java Memory Model)
Volatile
- Volatile解决了可见性、有序性问题,用到CPU的缓存锁+内存屏障
final(内存屏障指令)
- JMM内存模型禁止编译器把final域的写重排序到构造函数之外
Happens-Before
- JVM会对代码进行编译优化,会出现指令重排序情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。
-
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生与书写在后面的操作。【保证单线程的有序】
-
锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
-
volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。【先写后读】
-
传递规则:A 先于 B 且 B 先于 C 则 A 先于 C
-
线程启动规则:Thread对象的start方法先行发生于此线程中的每一个动作。
-
线程中断规则:对线程 interrupt 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。【先中断,后检测】
-
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 等待方法结束,Thread.isAlive() 的返回值手段检测线程已经终止执行。
-
对象终结规则:一个对象的初始化完成先行发生于它的 finalize 方法的开始。
DCL(Double Check Lock)
- DCL是一种单例模式写法的简称,全称是Double Check Lock,翻译过来叫双重检查锁。
public class Singleton {
//加上volatile,禁止new对象时的指令重排
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
//第1次检查时为了保证只有首次并发的情况下才阻塞,提高性能
if (null == instance) {
//加锁,是为了保证线程安全
synchronized(Singleton.class) {
//第2次检查时为了保证,避免重复创建对象
if (null == instance) {
instance = new Singleton();
}
}
}
return singleton;
}
}
-
new对象的过程并不是一个原子操作,有以下3步:
分配内存 -> 初始化对象(构造方法) -> 变量的引用指向对象的内存地址
该过程可能会指令重排序为:
分配内存 -> 变量的引用指向对象的内存地址 -> 初始化对象(构造方法)
-
如果不加volatile,多线程并发情况下可能会得到一个不完整的对象:
线程1获取到同步锁进行对象的单例创建,当执行完分配内存、变量的引用指向对象的内存地址之后,还没来得及完成对象的初始化,此时线程2执行到判断变量不为null,于是返回了该变量,但拿到的是一个数据未初始化完整的对象。