Java Concurrency in Practice 读书笔记1

好记性不如烂笔头,所以做一下<Java Concurrency in Practice>的小抄和笔记,省得又忘记了。这本书目前 在豆瓣的评分是9.4分。推荐👍。

对于书里面显而易见的话,就不直接列出原文了;在HTML注释里可以看到原文。

Chapter 1 Introduction

一个进程的所有线程共享这个进程的地址空间,所以所有的线程都能看到heap上的变量和对象。每个线程都有自己的stack, stack上的变量和对象是互相隔离的。

JVM的垃圾回收器也是跑在另外的线程(一个或多个)里的。

操作系统调度的单位是线程。

多线程的好处:

  • 有效地利用多处理器的资源
  • 在某个线程等待I/O完成时,其它的线程可以利用CPU
  • 简化建模(modeling)。比如像servlets或者RMI这样的框架利用了多线程来简化建模。

对于非阻塞I/O,Java也提供了多个包(java.nio);类似于linux中的select和poll(有多类似则待确认)。 由于操作系统提升,在某些平台上即使是在client数目非常多的情况下,thread-per-client模型也是实际可行的。

1.3.2 Liveness hazards

thread safety侧重于“nothing bad ever happens”,程序不会出错。liveness侧重于“something good eventually happens”,程序不会卡住,能运行下去。

1.3.3 Performance hazards

线程对性能的影响:

  • 上下文切换(context switch)的成本:保留和恢复上下文,缓存的失效(loss of locality),话费在调度上的 CPU时间等。
  • 线程间同步(synchronization)带来的影响:失去编译器的优化,flush or invalidate memory caches(应该是 保证可见性带来的副作用)和create synchronization traffic on the shared memory bus。

JVM启动时会创建多个线程来执行JVM housekeeping tasks (garbage collection, finalization),以及一个 主线程来执行main方法。

Frameworks introduce concurrency into applications by calling application components from framework threads. Components invariably access application state, thus requiring that all code paths accessing that state be thread-safe.

在引入了多线程的框架里,你的代码一般会在框架控制的(多个)线程里被调用。如果你的代码会访问一个被多个线程共享 的state,那么需要保证对这个state的所有访问都是线程安全的。

书里举了一些引入了多线程的框架的例子,比如Timer/TimerTasks,Servlets和RMI。(见注释)

Chapter 2 Thread Safety

编写thread-safe的代码的核心是管理对(对象的)state的访问,特别是那些共享的、可变的state。
简单点说,state就是一个对象的data/变量。
“共享的”意为一个变量可以/可能被多个线程访问;“可变的”意为一个变量的值可能会被改变。

一个对象是不是需要成为一个thread-safe对象取决于这个对象会不会被多个线程访问。所以,这主要取决于程序怎么 使用一个对象,而不是这个对象的功能是什么。

当有多个线程访问一个state,且其中有一个线程可能会修改这个state时,那么这些线程就要使用同步(synchronization) 来协调好它们对这个state的访问。并发的读不需要同步。

Java里最简单的同步机制是synchronized关键字;它是一种互斥的锁机制。“同步”(synchronization)还 包括volatile变量的使用、explicit locks和原子(atomic)变量等机制。

如果多个线程没有使用合适的同步机制就去访问同一个可变的state,那么你的程序就是有问题的。三种解决方法:

  • 不要在线程间共享这个state
  • 把这个state对应的变量变成不可变的(immutable),或者
  • 不管什么时候,只要访问这个state就加上合适的同步操作

事先就把一个类设计成thread-safe比事后再把它修改成thread-safe要容易得多。

在设计thread-safe类时,封装(encapsulation),不变性(immutability),清晰的不变式(invariants)会有助于 设计。

2.1 What is thread safety?

线程安全的定义中最核心的是“正确性”(correctness)。“正确”的意思是一个类遵守它的规格说明(specification)。 好的规格说明定义了invariants和postconditions。invariants用来约束对象的state;postconditions用来 描述操作的效果(effects of its operations)。

如果一个类在被多个线程调用,且调用方不需要加额外的同步代码的情况下,能保持正确性,那么这个类就是thread-safe的。 Thread-safe类已经把任何需要的同步操作都封装在它的内部了,所以调用方不需要再加任何的同步代码。

没有state的对象(stateless objects)永远是线程安全的。

2.2.1 Race conditions

当程序的正确性取决于运行时多个线程的调用时机或者顺序(relative timing or interleaving of multiple threads),那么我们就说程序出现了race condition。换句话说,正确性全靠运气。

最常见的race condition类型是check-then-act,在这种情况下“check”可能会观察到一个过时的状态(stale state), 这会使得程序可能基于错误的条件做了“action”。

2.2.2 Example: race conditions in lazy initialization

一个不线程安全的check-then-act的例子。这里使用check-then-act做lazy initialization。

public class LazyInitRace {
     private ExpensiveObject instance = null;
     public ExpensiveObject getInstance() {
          if (instance == null)       // 非线程安全
          	instance = new ExpensiveObject();
          return instance;
     }
}

2.2.3 Compound actions

To avoid race conditions, there must be a way to prevent other threads from using a variable while we’re in the middle of modifying it,

为了避免race condition,如果一个操作使用到了一个变量,那么必须要防止其它线程在这个操作的中间时刻使用这个变量。

原子性(atomic)指的是在其它操作看来,一个操作要么所有步骤全部完成,要么所有步骤都没完成。(这些操作作用在 同一个state上。)

为了线程安全,check-then-act操作,(比如lazy initialization)和read-modify-write操作(比如 increment)都应该是原子操作。

我们可以用java.util.concurrent.atomic包中的一些类对numbers和对象引用做一些原子操作。 可以利用这些类来保证操作的原子性;当然也可以用锁

 private final AtomicLong count = new AtomicLong(0);
 count.incrementAndGet();

如果向stateless类里加一个thread-safe的对象,那么这个类仍然线程安全。

如果可能的话,用已经存在的thread-safe对象,比如AtomicLong,来维护一个类的state,会使得这个类在保持或者验证 thread-safey时变得简单一点。

如果一个对象的state的一致性涉及到多个state,那么所有相关的state变量都要在一个原子操作里更新。

2.3.1 Intrinsic locks

Java提供了内建的locking机制来保证原子性:synchronized block。
一个synchronized块有两个部分:一个对象引用,这个对象会被当成锁;已经被这个锁保护的一个代码块。
当synchronized块的范围是整个方法,且整个块的锁就是this对象时,可以简写成synchronized方法。
static的synchronized方法用这个类的Class对象当锁。

synchronized (lock) {
     // Access or modify shared state guarded by lock
}

public synchronized void service(ServletRequest req, ServletResponse resp) {

}

每个Java对象都可以当锁;这种锁也叫intrinsic locks或者monitor locks。
每个对象都可以当锁(或者说都有一个锁)只是为了方便,这样我们就不需要再显式地创建“锁对象”了。

一个线程进入synchronized块前需要得到锁;当这个线程退出这个synchronized块(正常退出或者抛异常退出)时, 释放这个锁。

Java里intrinsic lock和mutex一样,在一个时刻只有一个线程能得到它。

2.3.2 Reentrancy

intrinsic lock是可以重入的(reentrant);就是说,如果一个线程已经得到了锁,那么当它再去请求这个锁时, 请求会成功。
Reentrancy意味着获得锁是基于每个线程的,而不是基于每个调用。在实现时,锁会记住拥有它的线程以及一个计数 (acquisition count)。(每获得一次锁这个计数会加一,每释放一次锁这个技术减一?)
如果intrinsic lock不是重入的,那么在子类重载的synchronized方法里调用父类的方法就会死锁。

2.4 Guarding state with locks

如果使用同步来协调对一个变量的访问,那么所有访问到这个变量的地方都需要加上同步代码。比如,只在写操作地方加同步 代码是不够的,也要在所有读操作的地方加同步代码;要不读到的可能是过时的状态或者中间状态。

另外,如果用锁来协调对一个变量的访问,那么所有访问这个变量的地方都要用同一个锁。

 if (!vector.contains(element))
      vector.add(element);

上面的代码中,虽然containsadd都是原子操作,但是整个put-if-absent操作仍然不是原子的。

2.5 Liveness and performance

同步对liveness和性能的影响:
同步会影响liveness。
缩小synchronized块可以提高liveness。但是也不要把synchronized块分得太细;例如不要把一个原子操作分成 多个synchronized块。
synchronized块里最好不要放运行时间很长的操作;如果这个操作不会影响相关的state,可以吧它移到块外以提高 liveness。
得到和释放锁也是有成本的,所以synchronized块也不要分得太细。
synchronized块的大小需要在thread-safety,代码简洁和性能间做权衡。
在权衡代码简介和性能时,要避免过早地优化;先保持代码简洁,真有了性能上的需要再考虑优化。

Chapter 3 Sharing Objects

synchronized关键字不仅仅是关于atomicity或者标出“critical sections”; synchronized/synchronization也会影响内存可见性(memory visibility)。

3.1 Visibility

一般情况下(不加额外的同步代码),java不能保证一个线程可以及时地看到另一个线程对一个变量做的修改,甚至 有可能根本看不到(而不仅仅是不及时)。为了在线程之间保证内存写操作的可见性(visibility of memory writes), 必须要加同步代码。

public class NoVisibility {
    private static boolean ready;
    private static int number;
    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }
    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42; 
        ready = true;
    }
}

这里主线程对numberready变量做写操作;另外一个线程去读这两个变量。程序可能会打印出42(显而易见); 程序也可能会打印出0(就是说另一个线程看到主线程对ready的修改,却没看到对number的修改);程序也有可能 一直循环下去(另一个线程完全没看到主线程对ready的修改)。这是因为程序没有使用足够的同步代码,所以内存的可见性 在线程间没有什么保证。

上面的程序可能打印出0是因为ready变量的修改先于number变量被看见;即使在代码上number先于ready被 写。这个现象叫reordering。在没有同步的情况下,编译器、处理器和runtime都可以改变执行顺序。
为了防止上述的问题,最简单的方法是,无论何时当需要跨线程共享数据时都加上合适的同步代码。

3.1.1 Stale data

上面的NoVisibility例子展示了缺少必要同步的后果:过时的数据(stale data)。
糟糕的是,staleness不是要么修改全可见,要么全不可见。一个线程可能看到对某个变量的修改,却看不到对另外一个变量的 修改,即使另一个变量是先被修改的。

public class MutableInteger {
    private int value;
    public int get() { return value; }
    public void set(int value) { this.value = value; }
}

如果一个线程调用set,另一个线程可能看到,也可能看不到相应的修改。

public class SynchronizedInteger {
    private int value;
    public synchronized int get() { return value; }
    public synchronized void set(int value) { this.value = value; }
}

加上同步代码,可以解决问题。

3.1.2 Nonatomic 64-bit operations

如果不加同步代码,线程可能会看到stale value,但是java至少可以保证这个值是被某个线程在之前写入的,而不是 一个随机值。这种保证叫out-of-thin-air safety。
Out-of-thin-air safety适用于所有变量,除了64位的数字变量(numeric variables),比如没有被volatile 修饰的double和long。因为JVM是运行把64位的读写操作分成两个32位的操作(是不是只有32位处理器上的JVM才这样?待 确认);所以,64位数可能会只更新了一半。
所以,即使你不在乎值是否是stale,在线程间不加同步代码(volatile或者锁)就共享可变的long和double变量也是 不安全的。

3.1.3 Locking and visibility

锁和可见性(locking and visibility)
线程A在synchronized块里或者synchronized块前做的任何事情在线程B执行被同一个锁保护的synchronized块时都会 对B可见。在没有同步的情况下,是没有这种保证的。
所以锁不仅仅是互斥,锁也影响内存的可见性。为了保证所有线程都能看到共享变量的最新状态,线程需要在同一个锁上做 同步。

3.1.4 Volatile variables

volatile是一种“稍弱”一点的同步方式。
如果一个变量被声明成volatile,那么编译器和runtime会保证:

  • 不会把对这个变量的操作和其它内存操作做reorder
  • 不会把这个变量缓存在寄存器里,也不会把这个变量放在其它处理器看不到的cache里;线程总是能看到这个变量的最新值

The visibility effects of volatile variables extend beyond the value of the volatile variable itself. When thread A writes to a volatile variable and subsequently thread B reads that same variable, the values of all variables that were visible to A prior to writing to the volatile variable become visible to B after reading the volatile variable.

当线程A写一个volatile变量,之后线程B读到这个变量时,在A写volatile变量之前所有对A可见的变量在B读这个volatile 变量时也会对B可见。(所以为了使多个变量都可见,只要令最后/晚被写的那个变量为volatile就可以了,而不用全部声明成 volatile?待确认)

访问一个volatile不会阻塞线程,所以可以把它看成是一种比synchronized更轻量级的同步方式。

So from a memory visibility perspective, writing a volatile variable is like exiting a synchronized block and reading a volatile variable is like entering a synchronized block.

从内存可见性角度看,写一个volatile变量就好像(应该只是“好像”,不是“等于”)离开一个synchronized块,而读一个volatile 变量就好像进入一个synchronized块。

不要太依赖volatile变量来完成visibility;相比于用锁,太依赖volatile可能会使得代码更脆弱(不好写对),更难理解。只有当 用volatile可以简化同步的实现和验证时才去用它。
使用volatile变量的好的用例包括:

ensuring the visibility of their own state, that of the object they refer to, or indicating that an important lifecycle event (such as initialization or shutdown) has occurred

The most common use for volatile variables is as a completion, interruption, or status flag

一个用例,下面的asleep变量:

volatile boolean asleep;
...
while (!asleep)
    countSomeSheep();

volatile变量可以带来一些便利,也有一些限制。比如volatile变量不足以让自增操作(count++)变成原子操作,除非你能保证 只有一个线程会写这个volatile变量,其它线程都只做读操作。
锁能同时保证visibility和atomicity;volatile只能保证visibility。

3.2 Publication and escape

Publishing一个对象就是让这个对象在“外层scope”可见/可用。Publish就是发布、暴露的意思。
某些时候,我们希望暴露对象和对象的内部状态;有些时候,暴露又是必须的(为了线程安全,可能需要额外的同步)。
暴露对象可能会使得invariants更难维护。

不该暴露的对象却被暴露了,这叫escape。
escaped对象都可能导致线程安全问题;因为你不知道使用者是怎么使用escaped对象的。

有很多暴露对象的途径:

  • 把变量声明成public的
  • 在非private的方法里返回一个对象
  • 把一个对象传给其它类的方法
  • 等等

对象可能会被间接地暴露(只要一个对象可以被直接暴露的对象间接地引用到),比如下面的Secret对象。

 public static Set<Secret> knownSecrets;
 public void initialize() {
    knownSecrets = new HashSet<Secret>();
}

把一个对象传给一个alien方法也是一种publish。alien方法是指其它类的方法,或者overrideable的方法(那些非private非 final的方法)。因为alien方法怎么使用传入的对象是不可控的,所以可能会有线程安全的问题。

A final mechanism by which an object or its internal state can be published is to publish an inner class instance … because inner class instances contain a hidden reference to the enclosing instance.

还有一种暴露对象的方式是:如果一个对象的inner class instance被暴露出去了,那么这个对象也会被间接地暴露出去。 因为inner class的对象总是可以通过OutterClass.this来引用它的外层类的对象。

3.2.1 Safe construction practices

不要让this引用在对象构造期间escape出去,因为那个时候对象并完全被构造好,它还没处于一个consistent state。
即使publication发生在构造函数的最后一个语句也是不可以的;这个时候对象仍然是未构造完成的。
总之:“Do not allow the this reference to escape during construction.”

A common mistake that can let the this reference escape during construction is to start a thread from a constructor. When an object creates a thread from its constructor, it almost always shares its this reference with the new thread, either explicitly (by passing it to the constructor) or implicitly (because the Thread or Runnable is an inner class of the owning object).

There’s nothing wrong with creating a thread in a constructor, but it is best not to start the thread immediately. Instead, expose a start or initialize method that starts the owned thread.

不要在构造函数里启动(start)一个线程。因为对象创建线程时会把this共享给这个线程(TODO:how it possible?)
可以在构造函数里创建一个线程。

Calling an overrideable instance method (one that is neither private nor final) from the constructor can also allow the this reference to escape.

在构造函数里调用可重载的方法也会escape this引用。

this引用escape的例子,

public class ThisEscape {
  public ThisEscape(EventSource source) {
    source.registerListener ( 
      new EventListener() {
        public void onEvent(Event e) { 
          doSomething(e);
        }
      });
  }
}

If you are tempted to register an event listener or start a thread from a constructor, you can avoid the improper construction by using a private constructor and a public factory method

通过private的构造函数和public的工厂方法改造上面的例子(TODO:体会一下),

public class SafeListener {
  private final EventListener listener;
  
  private SafeListener() {
    listener = new EventListener() {
      public void onEvent(Event e) { 
        doSomething(e);
      }
    };
  }
  
  public static SafeListener newInstance(EventSource source) {
    SafeListener safe = new SafeListener(); 
    source.registerListener (safe.listener);
    return safe;
  }
}

3.3 Thread confinement

在线程间共享可变的data需要同步;为了避免同步,可以让data只能被一个线程访问。这种技术叫“thread confinement”。

一个这样的例子是:the use of pooled JDBC (Java Database Connectivity) Connection objects。
Connection对象不是线程安全的。但是在典型的应用场景下,一个线程(一个请求)从pool里得到一个Connection对象后, 只会在这个线程里使用;pool也不会把同一个Connection对象分配给另外的请求/线程。

虽然java提供了一些机制,比如local变量和ThreadLocal类,但是thread confinement更多的是一种程序上的design。 开发者需要保证thread-confined对象不从指定的线程里escape出去。

3.3.1 Ad-hoc thread confinement

Ad-hoc thread confinement describes when the responsibility for maintaining thread confinement falls entirely on the implementation.

Ad-hoc thread confinement是指把维护thread confinement的责任交给程序员;不依赖/使用特定的语言工具来实现thread confinement。

把一个特定的子系统,比如GUI,设计成单线程的,以此来保证thread confinement。虽然由于没使用任何额外的手段会 使程序变得脆弱(可能会不经意间打破thread confinement而不自知),但是简单的设计可能会使程序开发变得容易。

推荐少用ad-hoc thread confinement,因为代码会变得脆弱;尽量用更健壮的thread confinement手段,比如 stack confinement或者ThreadLocal。

3.3.2 Stack confinement

Stack confinement是指一个对象仅能通过(stack上的)local变量引用到。local变量存在于线程的stack上,而各个 线程的stack是互相不可见的。
如果local变量是primitive类型的,那么这个local变量是不会被其它线程引用到的。
如果local变量是引用类型,那么要保证这个引用不要escape。
如果一个对象是stack confined,那么即使这个对象不是线程安全的,程序仍然是线程安全的。

使用stack confinement时,最好有一个清晰的文档;否则可能不知不觉被维护者违法。

3.3.3 ThreadLocal

另一种更formal的维持thread confinement的方式是ThreadLocal;使用它可以associate a per-thread value with a value-holding object。

可变的Singleton或者global变量都不是线程安全,这时可以考虑使用thread-local变量。
在把单线程程序移植为多线程程序是,可以考虑用ThreadLocal来替代共享的全局变量以达到线程安全。但是在porting时,把application level的cache变成thread-local的cache可能会使得cache失去了原来的意义。所以要视具体情况而定。

private static ThreadLocal<Connection> connectionHolder 
    = new ThreadLocal<Connection>() {
        public Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
        }
    };
    
public static Connection getConnection() { 
    return connectionHolder.get();
}

如果某个方法需要一个buffer,且这个方法经常被调用,为了避免反复reallocate这个buffer,可以使用ThreadLocal。 在单线程的程序中,则可以用一个“全局”变量来做buffer。

线程终止时,ThreadLocal变量会被垃圾回收。

ThreadLocal常常用来实现application framework;比如一些J2EE的框架。

不要滥用ThreadLocal;不要把ThreadLocal当成可以随便增加global变量和某些“隐藏”变量的执照。这会减少reusability, 增加couplings。

3.4 Immutability

构造后就不变的对象叫immutable对象。
immutable对象的invariants在它们的构造函数里建立,之后就再也不会违反这些invariants了。
immutable对象永远是线程安全的。

简单把一个对象的所有field都声明成final的并不等于immutability。final的引用变量所指向的对象还是可能改变内容的。

当满足下面三个条件时,对象就是immutable的:

  • 构造之后对象的state都不能变;
  • 所有的field都是final的;
  • 构造期间,this引用没有escape。

可以通过immutable对象存储程序的state;想要改变程序的state,可以替换一个新的immutable对象。

3.4.1 Final fields

如果field不是可变就把它声明成final,这是一个good practice。

3.4.2 Example: Using volatile to publish immutable objects

immutable对象某些时候可以保证一种较弱的原子性(a weak form of atomicity)。
当一组相关的data需要以原子操作的方式被更新时,可以考虑把这些相关的data包在一个immutable对象里。
使用immutable对象可以消除访问和更新一组相关变量时潜在的race condition。
需要更新状态时,就用一个新的immutable对象来替换老的。虽然其它线程看到的有可能仍然是老的immutable对象,那么至少数据 都是一致的。为了保证其它线程看到最新的immutable对象,可以在声明这个immutable对象的引用时加上volatile关键字。
通过immutable对象和volatile,可以不用显式地locking就达到线程安全。

一个使用immutable对象和volatile的例子,

class OneValueCache {
    private final BigInteger lastNumber; 
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i, BigInteger[] factors) {
	     lastNumber = i;
		  lastFactors = Arrays.copyOf(factors, factors.length); 
    }
    
    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i))
            return null;
        else
            return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}

public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);

    public void service(ServletRequest req, ServletResponse resp) { 
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors); 
        }
        encodeIntoResponse(resp, factors); 
    }
}

3.5 Safe publication

3.5.1 Improper publication: when good objects go bad

一个improper publication的例子,

public class Holder {
   private int n;
   public Holder(int n) { this.n = n; }
   public void assertSanity() { 
        if (n != n)
            throw new AssertionError("This statement is false.");        }
}

// Unsafe publication
public Holder holder;
public void initialize() { 
    holder = new Holder(42);
}

一个线程A用上面的“unsafe publication”来publish一个Holder的引用;另外一个线程B使用这个published的Holder引用 来调用Holder的assertSanity()方法,这时有可能会抛AssertionError异常😱。

虽然在Holder的构造函数已经完全构建好了invariants,但是由于improper publication另外的线程仍然可能看到一个 partially constructed object。

上面的代码(因为没有加同步)可能会导致以后的问题:

  • 其它的线程可能会看到一个stale的holder引用,也就是说线程A虽然已经设置了holder,但是线程B可能还是会看到null。
  • 更糟的是,其它线程可能能看到holder的最新值,但是看到holder对应的对象的state却是stale的。
  • 另外,一个线程可能在某个时刻看到stale值,在下一个时刻却看到更新后的值;这就是上面代码有可能会抛异常的原因。对于 if (n != n),两次看到的n是不同的值。

3.5.2 Immutable objects and initialization safety

上面的Holder例子可以看到,多线程环境下对象的reference是visible的,不代表对象的state是visible的。
但是对于immutable对象(满足之前提到的immutable对象的三个要求),它可以不需要额外的同步代码就能被安全的访问。JVM能 保证initialization safety for sharing immutable objects。

另外,如果一个properly constructed object被标记成final,JVM也会作同样的保证;所以final field可以不加同步代码 就能安全的访问。
但是,如果final field指向的对象是可变的,那么访问这个对象的state还是需要同步的。

3.5.3 Safe publication idioms

可变对象必须要safely published;通常要在publishing线程和consuming线程上都加上同步代码。这里先关心consuming线程, 讨论如何让consuming线程看到对象的正确state(就是让consuming线程看到publishing线程想要publish的state)。

要safely publish对象,对象的引用和对象的state都要对另外的线程visible。一个properly constructed object可以 通过以下的方式来safely publish:

  • Initializing an object reference from a static initializer;
  • Storing a reference to it into a volatile field or AtomicReference;
  • Storing a reference to it into a final field of a properly constructed object; or
  • Storing a reference to it into a field that is properly guarded by a lock.

另外,也可以利用java自带的thread-safe collections来safely publish一个对象。publishing线程把对象放到一个 这样的collection里,consuming线程再去取这个对象;这种方式可以保证consuming线程得到的对象都是safely published。 因为这些collection内部加了同步代码。
Java自带的线程安全的collection有HashtablesynchronizedMapConcurrentMapVectorCopyOnWriteArrayListCopyOnWriteArraySetsynchronizedListsynchronizedSetBlockingQueueConcurrentLinkedQueue等。

如果对象可以被静态地构造,那么使用static是最简单的方式:

public static Holder holder = new Holder(42);

JVM会在类初始化的时候执行上面的这个static initializer;JVM内部会有一些同步来保证statically initialized的对象被 safely publish。

3.5.4 Effectively immutable objects

如果一个对象被safely published之后再也不会做任何修改,那么publish之后再访问它就不需要额外的同步代码了。
因为safe publication保证了其它线程都能看到对象引用和对象state的最新值,且后续也不会修改这个对象,所以没必要加同步。
我们把那些虽然可变,但是一旦publish之后就不会再改的对象叫做effectively immutable对象。
effectively immutable对象safely published之后就不用再同步了。
利用这一点可以简化程序的开发和性能。

书中讲了一个例子,

public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date>());

这里Date是可变的;通过上面提过的利用线程安全的collection来safely publish Date对象;如果publish之后,Date再也 不会被改变了,那么后续使用Date的时候就不用加额外的同步代码了。

如果对象是可变的,那么safe publication只保证让其它线程看到这个对象publish时的最新状态(only the visibility of the as-published state)。后续访问这个对象时也要加同步代码;要不如果这个对象被修改了,修改的可见性却不能保证。

对于可变性不同的对象,publication的要求也不相同:

  • Immutable objects can be published through any mechanism;
  • Effectively immutable objects must be safely published;
  • Mutable objects must be safely published, and must be either thread-safe or guarded by a lock.

3.5.6 Sharing objects safely

总结一下,在多线程环境中使用和共享对象的policy有:

  • Thread-confined;不会被分享,所以也不需要同步。
  • Shared read-only;对于immutable或者effectively immutable对象,访问时不需要加同步。
  • Shared thread-safe;A thread-safe object performs synchronization internally, so multiple threads can freely access it through its public interface without further synchronization.
  • Guarded;A guarded object can be accessed only with a specific lock held. Guarded objects include those that are encapsulated within other thread-safe objects and published objects that are known to be guarded by a specific lock.
2015-05-16 15:15
推荐到豆瓣

如果你觉得这篇文章对你有用,可以微信扫一扫表示🙏 / If you find this post is useful to you, buy me 🍶 via Wechat