一.概述
线程安全性一章介绍了如何通过同步构建线程安全的对象,对象的共享将会从另外的角度描述如何保证线程安全性,比如从安全的发布角度,同时介绍了线程安全中其它几个重要概念,如可见性,同步除了能确保原子性之外,另一个就是可见性。
二.发布与逸出
1.发布发布一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。public class Test {
//将对象存储在一个共有变量中,所有线程都能访问,就是一种发布
//,除此之外,还有将对象动过get、set方法或构造函数等暴露出去也是发布
public static List strList = new ArrayList<>();
}
2.逸出某个不应该被发布的对象发布时,这种情况就是逸出。
下方代码展示了在构造函数中,造成了this引用的逸出。public class Test {
public Test(EventSource source) {
//造成了this引用在构造过程中逸出,此时其他线程获取到的this引用可能是一个未构造完成的对象
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
除了上方这种方式外,在构造函数中调用非私有的或者非终结的实例方法,也会导致this在构造过程中逸出,因为这些方法是可重写的,你不知道子类进行了什么操作。安全的对象构造过程:不要在构造过程中使this引用逸出。
只有当构造函数完成后,this引用才可以从线程中逸出。
三.可见性
1.定义当一个线程修改了对象状态后,其他线程能够立即看到发生的状态变化。
为什么会有不可见的问题,因为每个cpu核心是有自己的缓存的(L1,L2,L3),为了提高运行效率,cpu会先跟缓存的数据打交道,不会直接跟内存打交道。如果同一个内存地址保存的数据,在各个cpu内部都有缓存,每个线程使用各自的缓存进行操作。在没有正确同步机制的情况下,如果一个线程改变了该数据,该线程的缓存未必立即回写到内存,这样就会造成其他线程读取了失效数据,出现不可预知的问题。
2.相关概念失效数据:在没有正确的同步机制下,一个线程对变量进行修改,另一个线程访问改变量,获得到的未修改完的数据就是失效变量。(个人理解,非官方定义)
最低安全性:当线程在没有同步的情况下读取变量时,可能会读到一个失效值,但至少这个值是由之前某个线程设置的,而不是一个随机值。
重排序:在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整,这些调整通常是为了提高程序的运行效率。
3.保证可见性的方法为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
(1)加锁
内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果,加锁的含义不仅仅局限于互斥行为,还包括内存可见性。Synchronized、Lock均可。
(2)Volatile变量
这是Java提供的一种稍弱的同步机制,用于确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。修改volatile变量时,会将数据立即写回内存,然后导致缓存了该数据的其他cpu的缓存无效,这样其他线程在使用该数据的时候必须从内存重新读取数据到缓存,保证了可见性。加锁机制既可以保证可见性、有序性,又可以保证原子性,而volatile变量只能确保可见性和有序性。
volatile变量的使用条件(必须满足以下所有条件):对变量的写入操作不依赖变量的当前值(就是写入时不在乎失效数据),或者能确保只有单个线程更新变量的值。
该变量不会与其他变量一起纳入不变性条件中。
在访问变量时不需要加锁。(个人觉得这一条重复了,能使用volatile满足同步的话,自然不用加锁)
四.线程封闭
1.定义线程封闭就是不共享数据,仅在单线程内访问数据,这样就不需要同步。
2.实现线程封闭的方法
(1)Ad-hoc线程封闭定义:维护线程封闭的职责完全由程序实现来承担。(个人理解:就是自己实现同步,这样看起来就是线程封闭的)
(2)栈封闭在栈封闭中,只能通过局部变量才能访问对象。(个人理解:能用局部变量,就别用成员变量)
(3)ThreadLocal类这个类使线程中的某个值与保存值的对象关联起来,通常用于防止对可变的单实例变量或全局变量进行共享。
为每一个线程保存一份数据的副本,可以视为Map,但是这些特定于线程的值保存在Thread中。private static ThreadLocal connectionHolder = new ThreadLocal<>() {
protected Connection initialValue() {
return DriverManager.getConnection("DBUrl");
};
};
public static Connection getConnection() {
return connectionHolder.get();
}
五.不变性
1.定义如果某个对象在被创建之后其状态就不能被修改,那么这个对象就称为不可变对象,不可变对象一定是线程安全的。
满足以下条件时,对象才是不可变的:对象创建以后其状态就不能修改。
对象的所有域都是final类型
对象是正确创建的(在对象的创建期间,this引用没有逸出)。
2.事实不可变对象如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象”。
3.实现不变性的方法
(1)final域final类型的域是不可修改的,除非某个域是可变的,否则应将其声明为final域。
六.安全地发布
1.如何安全地发布
对象的发布取决于它的可变性:不可变对象可以通过任意机制来发布
事实不可变对象必须通过安全方式来发布
可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
最后,书中指出如下代码也是不安全的发布,虽然笔者没有实验出来不安全性,但是个人理解由于int型的n值由默认值0,在没有正确同步的情况下,Test在构造的时候存在指令重排序,导致没有创建完成变量就先将对象的内存地址赋予某一变量,导致其他线程使用该变量时,是一个正在构造中的对象,n可能还没有被赋值。当然本人可能理解的不正确,欢迎各位小伙伴来解答。~~~~public class Test {
private int n;
public Test(int n) {
this.n = n;
}
public int getN() {
return n;
}
}