第二章 线程安全性

什么是线程安全性

  要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。“共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内可以发生变化

  一个对象是否需要是线程安全的,取决于它是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。要使得对象是线程安全的,需要采用同步机制来协同对对象可变状态的访问。如果无法实现协同,那么可能会导致数据破坏以及其他不该出现的结果

  Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括volatile类型的变量,显式锁(Explicit Lock)以及原子变量

原子性

  在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件(Race Condition)

  在UnsafeCountingFactorizer中存在多个竞态条件,从而使结果变得不可靠。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件

  使用“先检查后执行”的一种常见情况就是延迟初始化。延迟初始化的目的是将对象的初始化操作推迟到实际被使用时才进行,同时要确保只被初始化一次

@NotThreadSafe
public class LazyInitRace{
 
   private ExpensiveObject instance=nullpublic ExpensiveObject getInstance(){
        if(instance==null)
        instance=new ExpensiveObject();
        return instance;
    }
}        

  LazyInitRace说明了这种延迟初始化情况。getInstance方法首先判断ExpensiveObject是否已经被初始化,如果已经初始化则返回现有的实例,否则,它将创建一个新的实例,并返回一个引用,从而在后来的调用中就无须再执行这段高开销的代码路径

  复合操作,LazyInitRace包含一组需要以原子方式执行(或者说不可分割)的操作。要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中

  在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的

 

加锁机制

  在使用原子引用的情况下,尽管对set方法的每次调用都是原子的,但仍然无法同时更新lastNumber和lastFactors。如果只修改了其中一个变量,那么在这两次修改操作之间,其他线程将发现不变性条件被破坏了。要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量

  内置锁,Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁

  每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法

  Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A也将永远地等下去

  重入,当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放


用锁来保护状态

   因为锁使得线程能够串行访问它所保护的代码路径,所以我们可以用锁来创建相关的协议,以保证线程对共享状态的独占访问。只要始终如一地遵循这些协议,就能够确保状态的一致性。操作共享状态的复合操作必须是原子的,以避免竞争条件

 

活跃性与性能

   简单和性能互相制约,不要为了性能牺牲简单性

posted on 2018-10-21 17:26  胡子就不刮  阅读(93)  评论(0编辑  收藏  举报

导航