什么是线程安全
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。
进程间交换数据
套接字,信号处理器,共享内存,信号量以及文件等。
竞态条件
在多线程环境下,getNext是否会返回唯一的值,要取决于运行时对线程中操作的交替执行方式。
读取-修改-写入
public class RaceCondition {
private int value;
public int getNext() {
return value++;
}
}
先检查后执行
public class LazyInitRace{
private ExpensiveObject instance = null;
public ExpensiveObject getInstance(){
if(instance == null){
instance = new ExpensiveObject();
}
return instance;
}
}
注:由于运行时可能将多个线程之间的操作交替执行,因此这两个线程可能同时执行读操作,从而使得他们得到相同的值
复合操作
为了确保线程安全,“先检查后执行”(例如延迟初始化)和"读取-修改-写入"(例如递增运算)等操作必须是原子的。我们将”先检查后执行“以及”读取-修改-写入“等操作统称为符合操作:包含了一组必须以原子方式执行的操作以确保线程安全。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量
//非线程安全
public class UnsafeCachingFactoryizer impelemtns Servlet{
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger> lastFactors = new AtomicReference<BigInteger>();
public void service(ServletRequest req, ServletResponse res) {
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get()) {
encodeIntoResponse(res,lastFactors.get());
}else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(res,Factors);
}
}
}
注:在某种执行时序中,可能无法同时更新lastNumber和lastFactors。同样也无法保证会同时获取两个值。
线程切换
在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁的出现上下文 切换操作,这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度、而不是线程运行上。
Timer
Timer类的作用是使任务在稍后的时刻运行,或者运行一次,或者周期性地运行。TimerTask将在Timer管理的线程中执行,而不是由应用程序来管理。如果某个TimerTask访问了应用程序中其他线程访问的数据,那么TimerTask需要以线程安全的方式来访问数据,其他类也必须采用线程安全的方式来访问数据。通常,要实现这个目标,最简单的方式是确保TimerTask访问的对象本身是线程安全的,从而就能把线程安全性封装在共享对象内部。
JAVA同步机制
Java中主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括volatile类型的变量,显示锁 (Explict Lock)以及原子变量。
加锁的含义
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
重入
内置锁时可以重入的,因此某个线程试图获得一个已经由他自己持有的锁,那么这个请求就会成功。
重排序
假设线程A需要看到线程B更新了number,ready,但实际上却只看到了其中一个值更新。
关于锁住哪个对象的问题
无论List使用哪个锁来保护它的状态,可以肯定的是,这个锁并不是ListHelper上的锁。ListHelper只是带来了同步的假象,尽管所有的链表操作都被声明为synchronized,但却使用了不同的锁,这意味着putIfAbsent相对于List的其他操作来说并不是原子的,因此就无法确保当putIfAbsent执行时另一个线程不会修改链表。
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E e) {
boolean absent = !list.contains(e);
if(absent) {
list.add(e);
}
return absent;
}
}
- 正确的实现方式
要想使这个方法能正确的执行,必须使list在实现客户端加锁或外部加锁时使用同一个锁。客户端加锁是指,对于使用某个对象X的客户端端代码,使用X本身用于保护其状态的锁来保护这段代码。要使用客户端加锁,你必须知道对象X使用的是哪一个锁。
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E e) {
synchronized (list) {
boolean absent = !list.contains(e);
if (absent) {
list.add(e);
}
return absent;
}
}
}
- synchronized
java1.6之前重量级锁机制
java1.6之后synchronized锁升级过程
并发编程三大特性:可见性,有序性,原子性
volatile
volatile可以保证可见性,有序性,但是无法保证原子性
- 可见性
- 有序性
计算机为了提高代码的执行效率,会对机器指令重排优化,volatile修饰的变量禁止指令重排。
内存屏障技术:lock前缀指令禁止重排序
指令重排遵循as-if-serial,happens-before语义
-
as-if-serial
-
happens-before
public class VolatileSerialTest {
volatile static int x = 0;
volatile static int y = 0;
public static void main(String[] args) throws InterruptedException {
Set<String> resultSet = new HashSet<>();
HashMap<String, Integer>