学习本文内容,默认已经了解Java多线程基础。
本篇即为基础篇,那么主要讨论如何编写线程安全的代码,大概分为3个方向
- 如何避免多线程同时访问同一时刻访问相同数据
- 共享、发布对象,能够安全地由多个线程同时访问
- 根据现有线程安全组件构建线程安全的类
在学习各模块只是之前,先普及一些知识。
1.编写线程安全的代码核心?
在访问共享、可变的状态要进行正确的管理。
(可能现在还不太明白,看下去慢慢就懂了)
2.什么是线程安全性?
安全性是一个代码上使用的术语,但它只与状态相关,应用于封装其状态的整个代码,可能是一个线程安全的类,也可能是一个线程安全的程序。
(当多个线程访问某个类时,不管运行时环境采用何种调度方式或这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类能够表现出正确的行为,那么这个类就是线程安全的类,这段代码也就具有线程安全性)
3.一个无状态的对象一定是线程安全的
public class Test extends Thread {
@Override
public void run() {
AtomicInteger integer = new AtomicInteger(0);
int i = integer.incrementAndGet();
System.out.println(i);
}
}
这段代码计算过程的变量的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问。
一、使用同步避免多线程同一时刻访问相同数据
大部分多线程同一时刻访问相同数据,都会产生原子性问题。
1.什么是原子性?
原子性在一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
没有同步的情况下统计线程执行次数:
static int count = 0;
@Override
public void run() {
AtomicInteger integer = new AtomicInteger(0);
integer.incrementAndGet();
for (int i = 0; i < 10000; i++) {
++count;
}
}
相信这个问题,已经不止一次看到过了,很明显 ++count 这个并非一个原子性操作,这是一个“读取 - 修改 - 写入”的操作,在多线程的情况下就会出现下面的线程问题:
在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,叫做:竞态条件。
1.1 竞态条件
多个线程对同一个变量或资源进行读写的时候出现竞态条件,最终结果取决于多个线程的交替执行时序。
最常见的竞态条件类型是 “先检查后执行” 操作,通过一个可能失效的观测结果决定下一步的动作。
延迟初始化中的竞态条件(单例模式懒汉式加载):
public class Test {
private static Test instance = null;
public static Test getInstance() {
if (instance == null)
instance = new Test();
return instance;
}
}
可能会破坏一个类的正确性,存在下面的线程问题,如果 instance 初始化需要较长时间,那么就会增加程序运行负担。
具体问题以及解决方法参考:Java设计模式之单例模式
在这里提一下,在使用双重检查锁定(DCL)来解决延迟初始化的线程问题时,需要将实例定义为 volatile 变量。
这里涉及到了Java内存模型问题,该问题会在后面的Java并发编程实战(高级篇)讲述,可以移步查看。
2.2 复合操作(存在竞态条件的操作)
根据上面的竞态条件问题类型,为了确保线程安全性,“先检查后执行”(延迟初始化)、“读取-修改-写入”(递增运算)等操作必须是原子性的,那么如何保证复合操作的原子性呢?
- 使用并发包JUC提供的一些并发容器或原子类(后面进阶篇会讲解)
- 加锁
下节,讲解加锁,在JUC的atomic包下提供了原子类,这些原子类保证了一些复合操作为原子性。
使用 AtomicInteger 类型的变量来统计线程执行次数:
private static final AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
AtomicInteger integer = new AtomicInteger(0);
integer.incrementAndGet();
for (int i = 0; i < 10000; i++) {
count.incrementAndGet();
}
}
在实际情况中,应尽可能地使用现有的线程安全对象来管理类的状态。
2.用锁来保护状态
在接触多线程时就学到了使用 内置锁(synchronized) 来实现线程同步,还可以使用显式锁等等,这里就不一一列举,下面讲一下一个类的状态在什么情况下,需要使用锁来保护其状态:
- 对于可能被多个线程同时访问的可变状态变量,在访问这个变量的所有位置都需要同步,并且都需要持有同一个锁;
- 每个共享的和可变的变量都应该只有一个锁来保护,从而使维护人员知道哪一个锁;
- 对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。(不变性条件简单理解就是两个或多个操作结果之间存在着某种固定的关系,不能出现顺序错误问题,这也就说明了不变性条件必须同步)
下面代码希望提高Servlet的性能:将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上一次的计算结果,而无需重新计算:
public class UnsafeCachingFactorizer implements Servlet{
private final AtomicReference<Integer> lastNumber =
new AtomicReference<>();
private final AtomicReference<Integer[]> lastFactors =
new AtomicReference<>();
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber))
encodeIntoResponse(resp, lastFactors.get());
else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, lastFactors.get());
}
}
}
UnsafeCachingFactorizer 不变性条件是:在 lastFactors 中缓存的因数之积应该等于在 lastNumber 中缓存的数值。
很明显这段代码可能会破坏这个不变性条件,在线程A获取两个值的过程中,线程B可能修改了它们,这样线程A的不变性条件就被破坏了。(存在“先检查后执行”的竞态条件)
那么我们采用内置锁来解决:
public class SynchronizedFactorizer implements Servlet{
private AtomicReference<Integer> lastNumber;
private AtomicReference<Integer[]> lastFactors;
public synchronized void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber))
encodeIntoResponse(resp, lastFactors.get());
else {
BigInteger[] factors = factor(i);
lastNumber