Java Concurrency [1]

PREFACE

这是Java concurrency in practice的读书笔记和感♂悟。免得忘了又要去翻书,又要去翻书,又要去翻书。按书的章节大概分为五个部分吧!Fundamentals、Structuring Concurrent Application、Liveness、Performance and Testing、Advance Topic。。。虽然目前还并没有看完。其中混杂这一些不知从哪看来的东西。还有有些词汇感觉不好翻译的就直接用英文了,倒时候Google找资料知道英文关键字来找也方便。

ATOMICITY

操作的原子性类似于数据库中的原子性,即该操作是不可分割的。

Read-Modify-Write

可能初学者会认为单条语句往往是不可分割的是原子操作,其实并不是这样。比如++count,这是一种典型的读-修改-写的操作,是由三个分离的操作组成:读count的值,将读得的值+1,将结果写到count。如果不加以同步,在计数器程序的多线程中就可能出现,a、b线程读到count的值x,然后分别+1得到结果x+1,然后分别再将结果写入count,导致最终结果是x+1而不是x+2

解决:可以用AtomicLongcount的类型,执行count.incrementAndGet()实现count增加的操作。

Check-Then-Act

在平时写程序当中经常会根据某些条件来决定接下来要做的动作。比如在单例模式中,我们会更据instance是否已经创建来决定是否新建instance

1
2
3
4
5
6
7
8
9
10
public SingleInstance{
private SingleInstance instance = null;

private SingleInstance(){}

public SingleInstance getInstance(){
if(instance == null) instance = new SingleInstance();
return instance;
}
}

上面这一段单例模式代码在多线程中就是不安全的,CHECKinstance == null,如果instance == null为真,那么ACT:新赋予instance一个SingleInstance对象。当线程a,b同时执行getInstance()时,a,b分别检查instance是否等于null,发现等于null。可能在线程的调度下,线程a,执行instance = new SingleInstance(),并且返回instance。接着线程b因为之前检查instance等于null,所以也执行new SingleInstance()。最终线程a,b分别得到了不同的SingleInstance。

注意:即使在a执行instance = new SingleInstance()return instance之间,b执行instance = new SingleInstance(),最终的结果也不能保证a,b得到同样的SingleInstance。具体原因可参考Happens-before Order

LOCKING

Intrinsic Locks

Java提供一种内建的锁机制,用synchronized修饰的代码块。其中修饰的代码块可以为类的方法或者仅仅一段代码。当线程a进入synchronized代码块之前隐式的获得监视器的锁,离开该代码块时隐式释放锁。Intrinsic Locks是互斥锁,当线程a持有锁的时候,若线程b要获得锁就必须等待线程a释放锁。

1
public synchronized void someMethod () { ... }

上面这段代码是用synchronized来修饰方法,其中该方法属于的对象this隐式作为监视器。相当于下面这段代码。

1
2
3
public void someMethod () {
synchronized (this){ ... }
}

1
synchronized (lock) { ... }

上面这段代码显示的指定一个对lock象作为synchronized代码块的监视器。

值得注意的是Intrinsic Locks是可重入的(reentrancy),即当线程a已经持有lock的时候,当线程a试图再次获得之前相同的lock,会成功获得lock。可重入性的实现机制是纪录锁的请求数count和锁现在被哪个线程所持有,当线程a获得一个当前线程a没有持有的锁的时候,JVM纪录锁的持有者为线程a,并把count设为1,当线程a再次请求这个锁,count+1,当线程a离开被这个锁守护的synchronized的代码块,count-1,当count减为0的时候,锁被释放。比如下面这段代码,当线程a调用someMethod_2时不会引起死锁。

1
2
3
4
class SynchronizedClass{
public synchronized void someMethod_1 () { ... }
public synchronized void someMethod_2 () { someMethod_1 () }
}

VISIBILITY && HAPPENS-BEFORE ORDER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
}

对于上面这段代码,我们期望的是当ready = true时,ReaderThread线程退出循环,然后打印出number的值42。然而不幸的是程序可能不会如我们所愿的运行。有其他两种情况的发生:1)ReaderThread线程读到的ready一直都是false,以致于ReaderThread陷入循环无法自拔。这是因为缺少同步,不能保证ready的值在main线程中的改变对ReaderThread是可见的。2)第二种情况可能会超出预料,ReaderThread读到readytrue,跳出循环,但是输出的number值是0。这是因为缺少同步,Java Compile可能认为number的赋值并不需要在ready的赋值之前执行,因此将代码的执行顺序进行了reorder。

在多线程中对一个共同的变量a进行读写的时候,其中一个线程1对a进行的写对另一个线程2对a变量的读是可见的Visibility(即线程2读到的a的值是线程1修改后的值),当且仅当线程1的写操作Happens-Before线程2的读操作。

注意:与单线程不同的是,即使线程1对变量a的写发生在线程2对变量的a的读之前,并不能保证 线程2读到的值是线程1写入变量a的值。因此认为对共享数据进行写的时候才需要同步是错误的想法,因为没有同步保证的读操作可能读取到的是stale data。

有一下几种情况是可以构成Happens-Before关系的:

  • 在同一线程中任何在代码顺序在前面的代码Happens-Before后面的代码

  • 对监视器Monitor的解锁Happens-Before所有之后对相同监视器的加锁。因为Happens-Before具有传递性,所以所有位于解锁前面执行属于同一线程的代码Happens-Before所有之后对相同监视器加锁的线程后面执行的代码

  • volatile关键字修饰的变量的写Happens-Before之后对该变量的读。对volatile关键字修饰的变量的读和写于对监视器的加锁解锁有相似的内存一致性效果,不同的是前者不需要互斥锁

  • 对线程start方法的调用Happens-Before该线程start方法中执行的代码

  • 线程a中有调用其他线程的join方法,所有线程a中在调用join之前执行的代码Happens-Before调用join

注意:之后 是指在不同线程中先后执行的代码。比如线程a执行x = x+1,在线程a执行完这条语句之后,线程b在某个时刻执行println(x)。此时b执行的println(x)就是在x = x+1之后执行;用前面、后面 是指同一线程中先后执行的代码

java.util.concurrent还有它的子包中的类的方法满足以下同步条件:

  • Actions in a thread prior to placing an object into any concurrent collection happen-before actions subsequent to the access or removal of that element from the collection in another thread.

  • Actions in a thread prior to the submission of a Runnable to an Executor happen-before its execution begins. Similarly for Callable submitted to an ExecutorService.

  • Actions taken by the asynchronous computation represented by a Future happen-before actions subsequent to the retrieval of the result via Future.get() in another thread.

  • Actions prior to “releasing” synchronizer methods such as Lock.unlock, Semaphore.release, and CountDownLatch.countDown happen-before actions subsequent to a successful “acquiring” method such as Lock.lock, Semaphore.acquire, Condition.await, and CountDownLatch.await on the same synchronizer object in another thread.

  • For each pair of threads that successfully exchange objects via an Exchanger, actions prior to the exchange() in each thread happen-before those subsequent to the corresponding exchange() in another thread.

  • Actions prior to calling CyclicBarrier.await and Phaser.awaitAdvance (as well as its variants) happen-before actions performed by the barrier action, and actions performed by the barrier action happen-before actions subsequent to a successful return from the corresponding await in other threads.

参考:

  • Memory Consistency Properties

  • Chapter 17 of the Java Language Specification

SHARING OBJECT SAFELY

The most useful policies for using and sharing objects in a concurrent program are:

  • Thread-confined. A thread confined object is owned exclusively by and confined to one thread, and can be modified by its owning thread.

  • Shared readonly. A shared readonly object can be accessed concurrently by multiple threads without additional synchronization, but cannot be modifiedbyany thread.Shared read only objects include immutable and effectively immutable objects.

  • 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 with in other thread-safe objects and published objects that are known to be guarded by a specific lock.

Safe Publication

要共享创建出来的对象,需要将对象的引用公布(publish)出来。从前面的变量的Visibility可知,并不是简单的将对象的引用赋予一个public变量就行。为了安全的将一个对象publish,该对象的引用和和对象自身的状态同时对其他的线程visible。一个正确构造的对象可以安全的通过以下方法publish,来满足对该对象引用的visibility(只是保证当publish的时候,对象的状态立即对其他的线程可见,并未保证对象自身状态的visibility):

  • 用static initializer初始化对象的引用(个人理解static initializer是指,变量用static修饰,直接在定义变量的同时初始化对象的引用或者通过static{}代码块来初始化对象的引用)

  • 将对象引用存储在用volatile修饰的变量或者AtomicReference中

  • 将对象引用存储在用finial修饰的变量中

  • 将对象引用存储在guarded by a lock的变量中

[个人理解]关于正确构造的对象(proper constructed object),难道还有不正确构造的对象?正确构造的对象是指构造函数运行完成的对象,那不正确构造的对象就指构造函数还未运行完的对象,例如当构造对象A的构造函数运行到一半的时候,该对象A的this就publish给了别的对象B,这时候对象B获得的对象A的引用指向的就是不正确构造的对象。

Thread Confinement

既然多线程访问共享的数据需要使用同步。那么只要不共享数据即可避免同步。换句话说,只要数据只被单一的线程所访问,那就不需要同步。

Ad-hoc Thread Confinement

暂时不理解

Stack Confinement

将变量限制在局部变量之中。Java的基本类型能保证作为局部变量时不被其他的线程所访问,不会破坏封闭性。对于对象引用的局部变量,要注意被引用的对象不会escape。

ThreadLocal

使用ThreadLocal为每个线程分配自己专有独一无二的变量。ThreadLocal表面上理解有点类似于Map<Thread, T>的形势,将Thread与所对应的值一一映射,且每个T都是归属于自己的Thread互不相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);

// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};

// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}

参考:

Immutable object

Immutable object需要满足:

  • 在构造后状态不会被修改

  • 所有变量都是final

  • 正确的构造(proper construction),this在构造函数中不会被其他的对象所获取。

被final关键字修饰的成员变量通常可以在没有多于的同步限制下进行访问,但是当该成员变量是对一个可变对象的引用的时候,我们仍然需要在访问该可变对象的时候进行相应的同步

Effectively Immutable object

在publish后状态不会被修改的对象,对象逻辑上保持不变