文章目录
线程安全性
定义
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
线程安全类的特点
- 原子性
提供互斥访问,同一时刻只能有一个线程对它进行操作。比如Synchronized
、Lock
、Atomic
(包下的类)、CAS
算法
- 可见性
一个线程对主内存的修改可以及时被其它线程观察到。比如:Synchronized
、Volatile
- 有序性
一个线程观察其它线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。而符合有序性的类有:Volatile
、Synchronized
、Lock
,除此之外,在某些情况下,其线程的执行也可能是有序的,比如Java
内存模型具备一些先天的有序性,而该有序性要求其符合Happens-Before
原则
线程安全类举例
原子性
- Synchronized
- Lock
- Atomic(包下的类)
- CAS算法
Synchronized
依赖于JVM
,其特点:
- 修饰代码块
大括号括起来的代码,作用于调用的对象
- 修饰方法
整个方法,作用于调用的对象
- 修饰静态方法
整个静态方法,作用于所有对象
- 修饰类
括号括起来的部分,作用于所有对象
各种原子性锁的对比
名称 | 特点 |
---|---|
Synchronized | 不可中断锁,适合竞争不激烈,可读性好 |
Lock | 可中断锁,多样化同步,竞争激烈时能维持常态 |
Atomic (包下的类) | 竞争激烈时能维持常态,比Lock 性能好;只能同步一个值 |
可见性
导致共享变量在线程间不可见的原因
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主存间及时更新
Synchronized
JMM
关于Synchronized
的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值
Volatile
通过加入内存屏障和禁止重排序优化来实现
- 对
Volatile
变量写操作时,会在写操作后加入一条Store
屏障指令,将本地内存中的共享变量值刷新到主内存 - 对
Volatile
变量读操作时,会在读操作前加入一条Load
屏障指令,从主内存中读取共享变量
有序性
Java
内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
具有有序性的类有:Volatile
、Synchronized
、Lock
其中Synchronized
、Lock
通过保证某一时刻只有一个线程在执行,相当于单线程,因而可以保证有序性,而Volatile
则是通过禁止编译器和处理器对指令重排序进而保证有序性。
除了这些之外,Java
内存模型具备一些先天的有序性。即不需要通过任何手段就能够得到保证的有序性,这个通常称为Happens-Before
原则。如果两个操作的执行次序无法从Happens-Before
原则推导出来,那么它们就不能保证它们的有序性。虚拟机可以随意的对它们进行重排序。
Happens-Before原则
Happens-Before
原则,就是先行发生原则,总共涉及到8条原则:
- 程序次序规则
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则
一个Unlock
操作先行发生于后面对同一个锁的Lock
操作
Volatile
变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则
如果操作A
先行发生于操作B
,而操作B
又先行发生于操作C
,则可以得出操作A
先行发生于操作C
- 线程启动规则
Thread
对象的start()
方法先行发生于此线程的每一个动作
- 线程中断规则
对线程interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则
线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()
方法结束,Thread.isAlive()
的返回值手段检测到线程已经终止执行
- 对象终结规则
一个对象的初始化完成先行发生于它的finalize()
方法的开始
其它保证线程安全的方法
堆栈封闭
从JMM
中我们得知,所谓堆栈,指的是线程的堆栈。而堆栈封闭,指的就是线程在执行过程中,动态的创建,使用本地变量或对象、在使用结束后,直接销毁该变量或对象,换言之,该变量与对象自始至终都是在一个线程内运作,永远不会在多个线程之间所共享。
堆栈封闭,表面上感觉高大上,其实际上使用起来异常的简单。
如下所示:
public void someMethod(){
long threadSafeInt = 0;
threadSafeInt++;
}
本地对象引用
对对象的本地引用略有不同。引用本身不共享。但是,所引用的对象并不存储在每个线程的本地堆栈中。所有对象都存储在共享堆中。
如果在本地创建的对象从未转义它在其中创建的方法,那么它就是线程安全的。因而我们可以将它传递给其他方法和对象,只要这些方法或对象中没有一个使传递的对象对其他线程可用。
如下所示:
public void someMethod(){
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localObject);
}
public void method2(LocalObject localObject){
localObject.setValue("value");
}
本例中的LocalObject
实例不从方法返回,也不传递给任何可以从someMethod()
方法外部访问的其他对象。执行someMethod()
方法的每个线程将创建自己的LocalObject
实例,并将其分配给LocalObject
引用。因此,这里LocalObject
的使用是线程安全的。
事实上,整个方法someMethod()
是线程安全的。即使LocalObject
实例作为参数传递给同一类或其他类中的其他方法,它的使用也是线程安全的。
当然,唯一的例外是,如果以LocalObject
作为参数调用的方法之一以允许从其他线程访问LocalObject
实例的方式存储LocalObject
实例。