并发编程的线程安全的两个方面:保证同步访问共享可变数据的一致性,避免死锁
多线程引起的问题(Thread)
修改不可见——通信不可靠
JSL(Java Language Specification) 只保证非long\double类型变量的简单读\写操作具有原子性,但不保证所有读写操作都具有原子性。因此读取字段时,线程不会看到任意值,但并不会保证一个线程的修改对其他线程可见——不保证线程之间的可靠通信。由于JVM对代码的提升机制(会对代码进行重构,如指令重排序、命令替换),也可能导致修改不可见(如下例中的while)。因此,需要同步机制,保证多线程系统资源的可靠通信。
代码块的执行没有规定的顺序——代码块不互斥
由于多线程间代码的执行顺序不同而导致多次执行结果不同(或long等类型的数据状态不一致)的现象(类似于数据库串行化现象)。因此,需要同步机制,保证资源在某个代码块的各线程中的互斥。
原子性操作
不可中断的一个或一系列操作具有原子性。原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断(线程切换不会中断原子操作)。
Java中具有原子性的操作:
- 非long\double的基本类型数据的简单读\写。i++ 非原子操作。
- volatile:使用volatile修饰的变量的简单读\写,包括long\double。
- 原子类(例如AtomicInteger、AtomicBoolean)的读\入。注意incrementAndGet自增操作具有原子性。
- 并发锁:使用synchronize或者Lock进行限定的并发锁,其中的代码都具有原子性。
同步机制的作用(Synchronization)
防止某个对象在被另一个线程修改时被一个线程看到处于不一致状态,确保进入同步方法或块的每个线程都能看到由同一个锁保护的所有先前修改的效果。
使代码块或方法具有原子性,执行不可中断
何时需要进行同步
对于需要被多线程共享访问的可变对象 的非原子性操作,需要利用同步机制进行控制。使得对象资源处于一致的状态,避免操作发生串行化错误。
1、当需要保证线程间资源互斥
需要保证被锁定资源只能同时被一个线程获取
2、当需要保证线程之间的可靠通信
需要保证对共享可变数据的访问具有原子性
需要保证long\double类型变量的简单读写操作的原子性
如何实现同步:如何保证数据一致性
为了性能,应该避免过度同步,对能保证一致性的最小模块加锁。所以按下列方法中从前到后的顺序,选择保证数据一致性的方法。
1、保证可变共享变量的访问具有原子性
1.1、volatile修饰变量——只能保证可靠通信,不保证互斥
1.2、对读\写操作加锁——加synchronized锁或Lock锁
1.3、对基本类型变量,类型替换为原子类型——如AtomicInteger
2、对包含共享变量的代码块加锁
3、使共享变量为不可变对象——不可变对象不存在数据不一致的状态,所以不需要同步
3.1、用 final 修饰共享变量
3.2、声明为静态变量,只在类初始化时修改值(可以利用静态方法初始化,后面更改初始化逻辑时更简单)
class public ClassA{
boolean a = false;
void synchronized methodOne(){
Thread backgroundThread = new Thread(() -> {
//while(!a) 代码会被提升为如下代码,导致修改不可见。可利用上述两种方法避免代码提升,使修改可见
while(!a){
//do sth
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
a=true;
}
}
//
while(!a){
//do sth
}
//上述while会被JVM提升为如下if-while代码
if(!a){
while(true){
//do sth
}
}
//
//保证防止JVM对while进行代码提升的方法
//方法一:2.2、volatile修饰变量
volatile boolean a = false;
//方法二:2.1、对读\写操作加锁
synchronized boolean getA(){
return false;
}
synchronized void setA(){
this.a = true;
}
//方法三:
AtomicBoolean a = false;