【读书笔记】《Java并发编程实战》第三章 对象的共享

可见性

 在多线程编程中,通常,我们无法确保读操作的线程能实时的看到其他线程写入的值。
 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

 并发编程过程中可能会发生“重排序”等问题,如例1

//例1
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;
	}
}

 Novisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。另一种情况,NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值。这种现象被称为“重排序”。

失效数据

 在缺乏同步的程序中可能产生错误的一种情况:失效数据。当读线程查看ready变量时,可能会得到一个已经失效的值。除非每次访问变量时都使用同步,否则很可能得该变量的一个失效值。

非原子的64位操作

 非volatile类型的64位数值变量(doublelong)。Java内存模型要求,变量得到读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同线程中执行,那么很可能会读取到某个值得高32位和另一个值得低32位。

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

Volatile变量

 这里还需要说明下volatile关键字的作用,可以说有2个作用,其一是,用volatile修饰的变量的读取和写入都是直接操作内存,以保证被其它线程读取到值都是最新的,或者称之为确保内存的可见性;其二是,保证变量的读取和写入操作都是原子操作,就是上面long和double的读取所遇到的问题,注意这里提到的原子性只是针对变量的读取和写入,并不包括对变量的复杂操作,比如i++就无法使用volatile来确保这个操作是原子操作。

使用volatile的需要满足的所有条件:

a、对变量的写入操作不依赖于变量当前的值,或者你能确保只有单个线程更新变量的值

b、该变量不会与其他状态变量一起纳入不变性条件中

c、在访问变量时不需要加锁

发布与逸出

 “发布”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。
 “逸出”是指某个不应该发布的对象被发布。

 当“发布”一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。

以下示范一个错误案例:
 当ThisEscape发布EventListener时,也隐含地发布了ThisEscape实例本身,因为在这个内部类的实例中包含了对ThisEscape实例的隐含引用。

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

不要在构过程中使this引用逸出。

 在构造过程中使this引用逸出的一个常见错误是,在构造函数中启动一个线程。在构造函数中创建线程并没有错误,但最好不要立即启动它。
 在构造函数中调用一个可改写的实例方法时,同样也会导致this引用在构造过程中逸出。
 如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公有的工厂方法,从而避免不正确的构造过程。

线程封闭

 当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭

ThreadLocal类

 维持线程封闭性的一种规范方法是使用ThreadLocal,这个类能使线程中某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本。

ThreadLocal对象通常用于防止可变的单实例变量或全局变量进行共享。

不变性

不可变对象举例:

//不可变对象
public final class ThreeStooges {
	private final Set<String> stooges = new HashSet<>();

	public ThreeStooges() {
		stooges.add("Moe");
		stooges.add("Larry");
		stooges.add("Curly");
	}

	public boolean isStooge(String name) {
		return stooges.contains(name);
	}
}

不可变对象一定是线程安全的。

 当满足一下条件时,对象才是不可变的:
 a、对象创建以后其状态就不能修改。
 b、对象的所有域都是final类型。
 c、对象时正确创建的(在对象创建期间,this引用没有逸出)。

安全发布

 发布不可变对象的引用时没有使用同步,也仍然可以访问该对象。
可变对象必须通过安全的方式来发布。这意味着发布和使用该对象的线程时都必须使用同步。可变对象必须通过安全的方式来发布,并且必须是线程安全的或者由某个锁保护起来。

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。
一个正确构造的对象可以通过以下方式来安全地发布:
a、在静态初始化函数中初始化一个对象引用。
b、将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
c、将对象的引用保存到某个正确构造对象的final类型域中。
d、将对象的引用保存到一个由锁保护的域中。

思维导图总结

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Handsome_Le_le/article/details/107233887