核心概念
原子性
原子性是指不可再分的最小操作指令,即单条机器指令,原子性操作任意时刻只能有一个线程,因此是线程安全的。
Java内存模型中通过read
、load
、assign
、use
、store
和write
这6个操作保证变量的原子性操作。
这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。 就是原子性说一个操作不可以被中途CPU暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行。
一个不正确的知识:“原子操作不需要进行同步控制”。
原子操作
是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在可能发生中断之前执行完毕。
原子性可以应用于基本数据类型(除了long
和double
),对于写入和读取,可以把它们当作原子操作来操作内存。但是,long
和double
这两个64位长度的数据类型Java虚拟机并没有强制规定他们的read
、load
、store
和write
操作的原子性,即所谓的非原子性协定,但是目前的各种商业Java虚拟机都把long
和double
数据类型的4中非原子性协定操作实现为原子性。所以Java中基本数据类型的访问读写是原子性操作。
对于大范围的原子性保证需要通过lock
和unlock
操作以及synchronized
同步块来保证。
例子
比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作技术——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。
可见性
可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。可见性问题是好多人忽略或者理解错误的一点。
CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。
这一点是操作系统或者说是硬件层面的机制,所以很多应用开发人员经常会忽略。
volatile、final和synchronized
Java中通过volatile
、final
和synchronized
这三个关键字保证可见性。
volatile
通过刷新变量值确保可见性。
synchronized
同步块通过变量lock
锁定前必须清空工作内存中变量值,重新从主内存中读取变量值,unlock
解锁前必须把变量值同步回主内存来确保可见性。
final
被final
修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this
引用传递进去,那么在其他线程中就能看见final
字段的值,无需同步就可以被其他线程正确访问。
顺序性
程序执行的顺序按照代码的先后顺序执行。1
2
3
4
5
6
7
8// 语句1
boolean started = false;
// 语句2
long counter = 0L;
// 语句3
counter = 1;
// 语句4
started = true;
从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们一定完全按照此顺序执行。
处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。
CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。
Java解决多线程并发问题
lock和synchronized
常用的保证Java操作原子性的工具是锁和同步方法。
无论使用锁还是synchronized
,本质都是一样,通过锁来实现资源的排它性,从而实际目标代码段同一时间只会被一个线程执行,进而保证了目标代码段的原子性。这是一种以牺牲性能为代价的方法。
synchronized
synchronized当作函数修饰符
1 | public synchronized void method(){ |
锁定的是调用这个同步方法对象。也就是说,当一个对象在不同的线程中执行这个同步方法时,会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象却能够任意调用这个被加了synchronized
关键字的方法。
上边的示例代码等同于如下代码。1
2
3
4
5
6
7public void method()
{
synchronized (this) // (1)
{
//…..
}
}
this
是调用这个方法的对象。可见,同步方法实质是将synchronized
作用于Object Reference
。那个拿到了对象锁的线程,才能够调用同步方法,而对另一个对象而言,这个锁对其没有影响,这个对象也可能在这种情形下摆脱同步机制的控制,造成数据混乱。
synchronized代码块
1 | public void method(SomeObject so) { |
零长度的byte[]
对象创建起来将比任何对象都经济。查看编译后的字节码:生成零长度的byte[]
对象只需3条操作码,而Object lock = new Object()
则需要7行操作码。1
2
3
4
5
6
7
8
9class Foo implements Runnable
{
private byte[] lock = new byte[0];
Public void method()
{
synchronized(lock) { //… }
}
//…..
}
synchronized作用于static函数
1 | Class Foo |
method2()
方法是把class
作为锁的情况,和同步的static
函数产生的效果是相同的,取得的锁很特别,是当前调用这个方法的对象所属的类。
CAS(compare and swap)
基础类型变量自增(i++
)是一种常被新手误以为是原子操作而实际不是的操作。Java中提供了对应的原子操作类来实现该操作,并保证原子性,其本质是利用了CPU级别的CAS指令。由于是CPU级别的指令,其开销比需要操作系统参与的锁的开销小。1
2
3
4
5
6
7
8AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
new Thread(() -> {
for(int a = 0; a < iteration; a++) {
atomicInteger.incrementAndGet();
}
}).start();
}
Java如何保持可见性
Java提供了volatile
关键字来保证可见性。当使用volatile
修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。volatile
适用于不需要保证原子性,但却需要保证可见性的场景。一种典型的使用场景是用它修饰用于停止线程的状态标记。1
2
3
4
5
6
7
8
9
10
11boolean isRunning = false;
public void start () {
new Thread( () -> {
while(isRunning) {
someOperation();
}
}).start();
}
public void stop () {
isRunning = false;
}
在这种实现方式下,即使其它线程通过调用stop()
方法将isRunning
设置为false,循环也不一定会立即结束。可以通过volatile
关键字,保证while
循环及时得到isRunning
最新的状态从而及时停止循环,结束线程。
Java如何保持顺序性
编译器和处理器对指令进行重新排序时,会保证重新排序后的执行结果和代码顺序执行的结果一致,所以重新排序过程并不会影响单线程程序的执行,却可能影响多线程程序并发执行的正确性。
Java中可通过volatile
在一定程序上保证顺序性,另外还可以通过synchronized
和锁来保证顺序性。synchronized
和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。
除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before
原则隐式的保证顺序性。两个操作的执行顺序只要可以通过happens-before
推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作任何保证,可对其进行任意必要的重新排序以获取高效率。