注:本文为笔者阅读《JAVA并发编程实战》(Brian Goetz等注)一书的学习笔记,如有错漏,敬请指出。
重要概念摘录:
概述
-
线程安全的界定:当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步、在调用代码方无须作其他协调,这个类的行为依然是正确的,称这个类是线程安全的。(我的理解:找不出并行执行与串行执行结果相异的情况)
-
构建并发程序要正确使用线程和锁。编写线程安全的代码,本质上是管理对状态的访问,而且通常是共享、可变的状态。
-
一般而言,一个对象的状态就是它的数据(存储在状态变量中),还包括其他附属对象的域,包含任何会对它外部可见行为产生影响的数据。
-
线程安全的性质取决于程序如何使用对象,而不是对象完成了什么。
原子性
- 自增操作(++count)并不是原子操作,它实际上是三个离散操作的简写形式:获取当前值,加1,返回新值 (read-modify-write)。若两个线程缺乏同步,会引发问题。
- 计数上的轻微错误在基于Web的服务中是可接受的,但若计数器用于生成序列或对象唯一的标识符,多重调用返回相同的结果会导致严重的数据完整性问题。
- 在一些偶发时段里,出现错误结果的可能性对于并发程序而言非常重要,这称为竞争条件。
一些问题的解决方案
在没有正确同步的情况下,若多个线程访问同一个变量,如何修复程序隐患?
- 不要跨线程共享变量
- 使状态变量为不可变的(我的理解:final)
- 在任何访问状态变量的时候使用同步
(2和3选一个)
注意:
- 一开始就将一个类设计成是线程安全的,比在后期重新修复它更容易。
- 访问特定变量的代码越少,越容易确保使用恰当的同步,越容易推断出访问一个变量所需的条件。
- 对程序的状态封装得越好,程序越容易实现线程安全,也有助于维护者保持这种线程安全性。
一些示例代码:
线程安全示例:无状态对象
以下是利用Servlet进行简单的因数分解操作的程序。
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp){
BigInteger i=extractFromRequest(req);
BigInteger[] factors=factor(i);
encodeIntResponse(resp,factors);
}
}
(注:这段代码似乎要自己编写extractFromRequest()和encodeIntResponse()方法)
- StatelessFactorizer像大多数Servlet一样是无状态的;它不包含域也没有引用其他类的域。一次特定计算的瞬时状态,会唯一地存在本地变量中,这些本地变量存储在栈中,只有执行线程才会被访问。
- 一个访问StatelessFactorizer的线程不会影响访问同一个Servlet的其他线程的计算结果。因为两个线程不共享状态,它们如同在访问不同的实例。
充分考虑活跃度和性能
缓存最新请求和结果的servlet
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import net.jcip.annotations.GuardedBy;
import java.math.BigInteger;
public class CachedFactorizer {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
@GuardedBy("this") private long hits;
@GuardedBy("this") private long cacheHits;
public synchronized long getHits(){return hits;}
public synchronized double getCacheHitRatio(){
return (double) cacheHits/(double)hits;
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i=extractFromRequest(req);
BigInteger [] factors=null;
synchronized (this){
++hits;
if(i.equals(lastNumber)){
++cacheHits;
factors=lastFactors.clone();
}
}
if(factors==null){
factors=factor(i);
synchronized (this){
lastNumber=i;
lastFactors=factors.clone();
}
}
encodeIntResponse(resp,factors);
}
}
- 特点:平衡了简单(同步整个方法)与并发(同步尽可能短的代码路径)
- 考虑因素:
- 请求与释放锁需要开销,故不要讲锁块分解得过于琐碎。
- 不要过早为了性能牺牲简单性(可能引发安全问题)
- 耗时的计算或操作,比如网络或控制台IO,执行期间不要占有锁。