(以下知识理解和代码来自《Java并发编程实战一书》)
- 一些基本概念:
- 1.进程:操作系统为各个独立的进程分配各种资源,包括内存、文件句柄以及安全证 书。不同进程之间通过一些粗粒度的通信机制来交换数据,包括:套接字、信号处理器、共享内存、信号量以及文件等。
- 2.线程:线程允许一个进程内同时存在多个程序控制流。线程共享进程内的资源,例如内存句柄和文件句柄,线程有自己的程序计数器、栈和局部变量等。所以一个进程内的线程能够访问相同的变量并在同一个堆上分配对象,这就需要一种比在进程共享数据粒度更细的数据共享机制。如果没有明确的同步机制来协同对共享数据的访问,当某一个线程正在使用某个变量时,另一个线程可能修改了这个变量。
- 3.同步:可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。
- 4异步:执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。
线程安全性
1.线程安全的核心
-
1.1**线程安全性:**当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类时线程安全的
-
1.2核心:对共享的和可变的状态变量访问操作进行管理。共享意味着变量可以由多个线程同时访问;可变则意味着变量的值在其生命周期内可以发生改变。
-
1.3Java同步机制:当多个线程同时访问可变状态变量并且至少有一个进行了写操作,则需要同步机制来协同对对象可变状态的访问。Java中主要同步机制是关键字synchronize,它提供一种独占的加锁方式,此外还包括volatile变量、显示锁和原子量。
-
1.4出现同步错误时,可以:
不在线程内共享该状态变量。
将状态变量修改为不可变的变量。
在访问状态变量时使用同步。 -
1.5当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明晰的不变性规范能起到一定的帮助作用。
-
1.6在线程安全类中封装了必要的同步机制,因此类使用者无须进一步采用同步措施。
-
1.7无状态对象一定是线程安全的:
@ThreadSafe public class StatelessFactorizer extends GenericServlet implements Servlet { public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); encodeIntoResponse(resp, factors); } }
与大多数Servlet相同,StatelessFactorizer是无状态的:不包含任何filed(属性),也不含任何对其他类中field的引用。其中i变量是局部变量,在栈中分配内存,线程有自己各自的栈,所以线程之间不会对彼此的i操作。
2.原子性
先展示一段代码:
@NotThreadSafe
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
private long count = 0;
pubic long getCount() { return count}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}
其中++count其实包含三个独立的操作:读取count的值,将值+1,将计算结果写入count。一个“读取”-“操作”-“写入”的操作序列,并且其结果依赖于之前的状态。所以当有多个线程同时调用service方法时,由于随机的不恰当执行序列可能出现不正确的结果是一种非常重要的情况,他有一个正式的名字:Race Condition—竞争条件。
2.1竞争条件
- 当某个计算的正确性取决于多个线程的交替执行时序时,会发生竞争条件。最常见的进程条件类型是Check-Then-Act—“先检查后执行”。例如:if(文件X不存在)然后根据这个观测结构采用相应的动作: Then(创建X)。但事实上从你观测到这个结果到开始创建文件之间,观测结果已经无效(例如线程B已经在此阶段创建了X),从而导致各种问题(未逾期异常、数据被覆盖、文件破坏等…)
- 实例:延迟初始化
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
class ExpensiveObject { }
2.2避免竞争条件
-
2.2.1原子操作:假设有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。
-
2.2.2 LazyInitRace 和 UnsafeCountingFactorizer 都包含一组需要一原子方式执行的操作。
-
2.2.3除了采用同步机制,在实际情况下应该尽可能的使用现有的线程安全对象(例如AcomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要容易很多,更容易维护和验证该线程安全性。e.g:
@ThreadSafe
public class CountingFactorizer extends GenericServlet implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() { return count.get(); }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}
当有多个类的状态需要管理,并且这些状态之间有约束存在约束时,这种方法并不正确。e.g:假设我们要提高Servlet性能,当两个连续的请求对相同数值进行因数分解时,可以直接用上一次结果3果:
@NotThreadSafe
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {
private final AtomicReference<BigInteger> lastNumber
= new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors
= new AtomicReference<BigInteger[]>();
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber.get()))
encodeIntoResponse(resp, lastFactors.get());
else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
上述类中对象有以下约束关系:lastNumber(缓存的因数之积)==lastFactor(缓存的数值),尽管每次调用set方法都是原子的,但是扔无法同时更新两个变量。
- 2.2.4 同步机制:synchronized,e.g:
@ThreadSafe
public class SynchronizedFactorizer extends GenericServlet implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
public synchronized void service(ServletRequest req,
ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber))
encodeIntoResponse(resp, lastFactors);
else {
BigInteger[] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(resp, factors);
}
}
此时虽然实现了线程安全,但这是一种粗暴的同步方式(只是将每个方法都作为同步方法),付出的代价很高,由于service是一个synchronize方法,每次只能有一个可以执行,这与Sevlet框架相背离了。我们可以缩小同步代码块的作用范围,同时解决这两个问题。e.g:
@ThreadSafe
public class CachedFactorizer extends GenericServlet implements Servlet {
@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();
}
}
encodeIntoResponse(resp, factors);
}
}
- 2.2.4.1 通常,在简单性和性能之间存在着相互制约关系。当实现某个同步策略时,不要盲目为了性能而牺牲简单性。
- 2.2.5 当执行较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。