- 什么是线程安全性
在线程安全性的定义中,最核心的概念就是正确性。正确性的含义是,某个类的行为与其规范完全一致。在良好的规范中会定义各种不变性条件来约束对象的状态,以及定义各种后验条件来描述对象操作的结果。
定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为。
举例:Servlet是无状态,即它不包含任何域,也不包含任何对其他类中域的引用。也就是线程之间的变量不存在依赖关系,或者说线程之间没有共享的变量,彼此互不干扰。那么无状态对象都是线程安全的。
原子性
竞态条件
最常见的竞态条件类型就是“先检查后执行”,通过一个可能失效的观测结果来决定下一步的动作。
书中举了一个计数的例子,那么计数的操作可以拆解成以下三个步骤:读取上一次的数值 => 修改值(++)=> 写入值。也就是值是依赖于上一次的值。那么当线程A将数值读取(检查)出来到修改(执行)这个过程中,可能线程B就已经把线程A读取出来的值改变了,那么线程A读取的数值就是失效了,这里就是一个竞态条件。
if(instance == null){ instance = new ExpensiveObject(); }
上面是一个经典的懒汉式单例(延迟加载),但是这种单例是线程不安全的,这里也存在竞态条件,在并发的环境下无法保证单例。
复合操作
要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量。
- 单个需要同步的变量
比如上面计数的例子,只有一个count是需要同步的变量,这样可以直接用java为我们提供的原子变量(Atomic Variable)来保证在某个线程修改该变量时,为该变量上锁(实际上是锁住了读取 => 修改 => 写入这个过程),之后再访问该变量的线程会被阻塞。
多个需要同步的变量
举个例子:
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、lastFactors都加上了原子变量,但是依旧存在竞态条件。先解释一下这个代码的含义,它是实现了某种简单的缓存,当用户重复对某个number进行两次的factor计算,可以直接从数组中取出来,这就要求上一次的number和上一次对应计算出来的factor应该一一对应的存储在数组中(具体可以看P19)。
那么这里的竞态条件的产生是因为多个变量引起的,比如说number=2,对应的factor=5,此时线程A将2存储到了lastNumber中,但是此时线程B突然将他的结果10存到了lastFactors中,这样导致2和10的错误对应。解决办法详见下文的内置锁。
- 单个需要同步的变量
加锁机制
- 内置锁
含义:静态的synchronized的方法以Class对象作为锁,每个Java对象都可以用作一个实现同步的锁。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法
获得锁时机:线程在进入同步代码块之前自动获取。
释放锁时机:正常的控制路径退出 / 从代码块中抛出异常退出。
缺点:虽然保证了线程安全但是效率很低,因为在事务设计中应该是短小精悍的,而这种对这个方法的加锁使得效率变低。
- 重入
内置锁是可以重入的,重入就意味着获取锁操作的粒度是“线程”,而不是“调用”。举个很简单的例子,比如一个方法是通过递归实现的,那么一个线程调用了该方法后,依旧可以不断递归完成而不会阻塞;也就是说一个线程获得了锁之后,可以反复对该同步代码调用。
- 内置锁
- 用锁来保护状态
这个其实在上文已经详细阐释了。那么此处我们将加锁的处理抽象化,在上文中,只对一个变量时,我们利用原子变量的方式加锁,实际上是锁住了那一个过程或者说操作。在多个变量时,我们利用内置锁解决,实际上也是锁住了一个“更宏观的操作”。所以加锁实际上是锁住了一系列需要原子性的操作。那么这个操作可以不断宏观,比如多个方法组成的一组操作,这样我们在这组操作之后再加锁。显然这样粗暴的加锁,可能会导致程序中出现过多的同步,从而导致性能下降。
活跃性和性能
上文中我们理解一下锁的作用范围(或者说同步代码块的作用范围),synchronized是加在方法上的,所以作用域是在整个方法上。若我们能缩小同步代码块的作用范围,我们很容易做到既确保并发性,又维护线程安全性。
@GuardedBy("this") private BigInteger lastNumber; //GuardedBy的含义就是该变量受内置锁保护 @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); }
为了使得同步机制的统一,所以都采用synchronized来实现了,而抛弃了原子变量的方式。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。