1. Java线程同步机制的幕后助手是内存屏障。不同同步机制的功能强弱不同,相应的开销以及可能导致的问题也不同。


volatile

CAS

final

static

原子性保障

具备

具备

具备

不涉及

不涉及

可见性保障

具备

具备

不具备

不具备

具备

有序性保障

具备

具备

不涉及

具备

具备

上下文切换

可能

不会

不会

不会

可能

  1. 锁是Java平台中功能最强大的一种线程同步机制,同时其开销也最大,可能导致的问题也最多。被争用的锁会导致上下文切换,锁还可能导致死锁、锁死等线程活性故障。锁适用于存在多个线程对多个共享数据进行更新、check-then-act操作或者read-modity-write操作这样的场景。
  2. 锁的排他性以及Java虚拟机在临界区前后插入的内存屏障使得临界区中的操作具有原子性。由此,锁还保障了写线程在临界区中执行的操作在读线程看来是有序的,即保障了有序性。Java虚拟机在MonitorExit对应的机器码后插入的内存屏障则保障了可见性。锁能够保障线程安全的前提是访问同一组共享数据的多个线程必须同步在同一个锁之上,否则原子性、可见性和有序性均无法得以保障。在满足貌似串行语义的前提下,临界区内以及临界区外的操作可以在各自范围内重排序。临界区外的操作可能会被JIT编译器重排到临界区内,但临界区内的操作不会被编译器、处理器重排到临界区之外。
  3. Java中的所有锁都是可重入的。内部锁(synchronized)仅支持非公平锁,因此它可能导致饥饿。而显式锁(ReentrantLock)既支持非公平锁又支持公平锁,显示锁可能导致锁泄露。内部锁和显式锁各有所长,各有所短。读写锁(ReadWriteLock)由于其内部实现的复杂性,仅适用于只读操作比更新操作要频繁得多且读线程持有锁的时间比较长得场景。读写锁(ReadWriteLock)中的读锁和写锁是一个锁实例所充当的两个角色,并不是两个独立的锁。
  4. 线程转储中可以包含锁的相关信息——线程在等待哪些锁,这些锁又是被哪些线程持有的。
  5. volatile相当于轻量级锁。在线程安全保障方面与锁相同的是,volatile能够保障可见性、有序性;与锁不同的是volatile不具有排他性,也不会导致上下文切换。与锁类似,Java虚拟机实现volatile对有序性和可见性的保障也是借助于内存屏障。从这个角度来看,volatile变量写操作相当于释放锁,volatile变量读操作相当于获得锁——Java虚拟机通过在volatile变量写操作之前插入一个释放屏障,在volatile变量读操作之后插入一个获取屏障这种成对的释放屏障和获取屏障的使用实现了volatile对有序性的保障。类似,Java虚拟机在volatile变量写操作之后插入一个存储屏障,在volatile变量读操作之前插入一个加载屏障这种成对的存储屏障与加载屏障的使用实现了对可见性的保障。
  6. 在原子性方面,volatile仅能保障long/double型变量写操作的原子性。如果要保障对volatile变量的赋值操作的线程安全,那么赋值操作右边的表达式不能涉及任何共享变量(包括被赋值的变量本身)。volatile关键字在可见性、有序性和原子性方面的保障并不会对其修饰的数组元素的读、写操作起作用。
  7. volatile变量写操作的成本介于普通变量的写操作和在临界区内进行的写操作之间。读取一个volatile变量总是意味着(通过高速缓存进行的)读内存操作,而不是从寄存器中读取。因此,volatile变量读操作的成本比读取普通变量要略高一点,但比在临界区中读取变量要低。
  8. volatile的典型运用场景包括:一,使用volatile变量作为状态标志;二,使用volatile保障可见性;三,使用volatile变量代替锁;四,使用volatile实现简易版读写锁。
  9. CAS使得我们可以在不借助锁的情况下保障read-modify-write操作、check-then-act操作的原子性,但是它并不保障可见性。原子变量类相当于基于CAS实现的增强型volatile变量(保障volatile无法保障的那一部分操作的原子性)。常用的原子变量类包括AtomicInteger、 AtomicLong、AtomicBoolean等。AtomicStampedReference则可以用于规避CAS的ABA问题。
boolean compareAndSwap(Variable V,Object A, Object B){
	if(A == V.get()){ // check:检查变量值是否被其他线程修改过
		V.set(B);	// 更新变量值
		return true;	// 更新成功
	}
	return false;	// 变量值已被其他线程修改,更新失败
}

// 具体使用
do{
	oldValue = count;	// 读取共享变量的当前值
	newValue = oldValue + 1; 	// 计算共享变量的新值
} while (!compareAndSwap(oldValue,newValue));
  1. static关键字能够保证一个线程即使在未使用其他同步机制的情况下也总是可以读取到一个类的静态变量的初始值(而不是默认值)。对于引用型静态变量,static还确保了该变量引用的对象已经初始化完毕。但是,static的这种可见性和有序性保障仅在一个线程初次读取静态变量的时候起作用。
  2. final关键字在多线程环境下也有其特殊作用:当一个对象被发布到其他线程的时候,该对象的所有final字段(实例变量)都是初始化完毕的。而非final字段没有这种保障,即这些线程读取该对象的非final字段时所读取到的值可能仍然是相应字段的默认值。对于引用型final字段,final关键字还进一步确保该字段所引用的对象已经初始化完毕。
  3. 实现对象的安全发布,通常可以依照以下顺序选择适用且开销最小的线程同步机制。
  • 使用static关键字修饰引用该对象的变量。
  • 使用final关键字修饰引用该对象的变量。
  • 使用volatile关键字修饰引用该对象的变量。
  • 使用AtomicReference关键字修饰引用该对象的变量。
  • 对访问该对象的代码进行加锁。
    为避免将this代表的当前对象逸出到其他线程,我们应该避免在构造器中启动工作者线程。通常我们可以定义一个init方法,在该方法中启动工作者线程。在此基础上,定义一个工厂方法来创建(并返回)相应的实例,并在该方法中调用该实例的init方法。