先引入两个概念:发布和逸出
解释:
1.发布:使对象可以在当前作用域外使用
**
* 发布:
* 1.使被发布对象的引用保存在其他代码可以访问的地方 如static
* 2.非私有方法返回私有成员引用 getter
* 3.引用传递到其他类
*
* 不正确发布:
*
public class Publish {
private String[] statusCode=new String[]{
"status1","status2"
};
*
*大家都知道线程之间为了处理效率 会缓存一份数据 这样的statusCode发布后
* 无法保障statusCode的可见性 会导致并发安全问题 具体参考我的上篇文章
*栈持有堆副本导致成员变量不可见,可能触发数据竞争
* 而数据竞争可能导致并发问题
*
public String[] getStatusCode() {
return statusCode;
}
}
**
2. 逸出:发布了不该发布的对象
1.内部状态的暴露,但没有保证可见性。如上述不正确发布例子
2.对象没构造完成就被发布了。
public class ThisEscape{
public ThisEscape(EventSource source){
new EventListener(){
public void onEvent(Event e){
this.dosomething(e);
}
}
initsomeDependValue();
}
public void dosomething(Event e){
}
}
此时的this仍未构造完成,
可能dosomething依赖了一些状态
未构造完成的this还没赋予dosomething需要的值
构造过程有另外的线程触发监听事件,
执行的是一个半成品“this”的方法
构造是个耗时的操作
(某main线程)
------
时间线:0-10ms ThisEscape example=new ThisEscape();//假设需要耗时100ms
(异步 不在main) 10-20ms e.onEvent()//耗时
20-40ms example的dosomething被监听事件触发
。。。100ms还没过 对象还没构造完 以来的数据还没初始化就开始do
------
所以说过程中this可能会逃逸/逸出
那么就会涉及:避免发布和安全发布
解释:
1.避免发布:
1.1 栈封闭 :局部变量的特性保证了栈封闭。将对象的引用封闭在栈内部,限制了引用的使用范围。比如单例Servlet(Servlet容器默认是采用单实例多线程),其service是并发执行的。如果访问同个Servlet,可能会产生并发问题。但是平时我们只要不在Servlet中存在发布的成员对象,只使用栈封闭式编码,就可以保证线程安全性。可能你会说,Spring中@Service之类的组件不就是发布了吗,那可以想下Spring的原理,那个发布的@Service组件是否是单例的(下面会讲安全的发布:发布不可变对象)
1.2 线程封闭:使用ThrealLocal,保证每个线程持有自己的发布对象副本,正如上面所说,Spring的@Service@Dao组件是单例的,但是Dao中的对象必须含一个数据库的连接Connection,而这个Connection不是线程安全的,所以每个DAO都要包含一个不同的Connection对象实例。那DAO单例,怎么确保Connection对象的线程安全呢。答案听说就是ThreadLocal
2.安全发布:
2.1 发布不可变对象
2.1.1 不可变的对象:①创建后状态不可变 ②其所有域都是final ③对象创建正确,没有this逸出
例子:可变对象造成地问题
这其实是个事实不可变对象,但需要通过正确发布来保障
/**
有2个线程 A,B
A做的操作:Holder holder = new Holder(42);
B做的操作:
if(holder != null) {
holder.assertSantiy();
}
对于线程A的操作,jvm执行时的步骤:1.栈里生成holder引用,2.执行构造函数,在堆里生成Holder的内存空间,并且给n赋值为42,3.把holder指向堆里生成的内存空间
问题是:上面的1,2,3步骤不是按照1,2,3的顺序执行的,执行引擎对指令重排序后,可能会按照1,3,2的顺序执行,也可能是别的顺序
结果:这样就导致当holder指向了堆里的内存空间时(这时holder不是null了),但是构造函数执行尚未完成,n还没有被赋值为42。
*
* */
public class Holder {
private int n;
public Holder(int n){
this.n=n;
}
public void assertSanity() throws Exception {
if(n!=n)
throw new Exception("Xxx");
}
}
引用了部分观点https://www.cnblogs.com/simiie/p/6053780.html
2.1.2 浅谈单例:结合这里谈及的发布问题,参考别人写的很好的单例文章→传送门https://www.cnblogs.com/dongyu666/p/6971783.html 大致摘抄两个结合线程安全讲讲👇
懒汉单例:(个人分析的!)问题1:成员后续状态会修改,不是final。可见性不保证,不安全发布对象。(饿汉只要构造器不要依赖其他成员,就没这个问题,发布保障了2.1.1所述不可变对象的条件。具体参考传送门提及的饿汉模式优化)
问题2:操作先验后操作,没有原子性
public class Single1 {
private static Single1 instance;
private Single1() {}
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
懒汉安全:也稍微解决了纯synchronized的效率问题,不用次次上锁 。解决了原子性的问题 。
public class Single3 {
private static Single3 instance;
private Single3() {}
public static Single3 getInstance() {
if (instance == null) {
synchronized (Single3.class) {
if (instance == null) {
instance = new Single3();
}
}
}
return instance;
}
}
进阶懒汉:原子性 可见性 以及稍微完善的性能
public class Single4 {
private static volatile Single4 instance;
private Single4() {}
public static Single4 getInstance() {
if (instance == null) {
synchronized (Single4.class) {
if (instance == null) {
instance = new Single4();
}
}
}
return instance;
}
}
饿汉模式就不多说了,看引用资料
2.2 事实不可变对象:技术上看可变,但是状态发布后其实是不可变的,就要采取安全发布策略。一、保证线程安全,对象的变化可见 最简单用static初始化对象 二、 其次用volatile(懒汉单例的优化用到了)三、或者保存到正确构造对象的final中(饿汉模式里的优化用到了)四、保存到锁住的区域中
2.3 可变对象:一是要确保像不可变对象一样安全发布。并且还要是线程安全地(上锁),确保状态的改变可见 参考懒汉单例
总结:
所以:确保对象是线程安全的可以采取如下基本思路👇
(个人理解)
对象的线程安全
1.避免发布,状态线程间或者栈间不可见。(所有操作在限定区域内,不存在线程安全问题,对象状态都是安全的)
2.安全发布不可变对象和事实不可变对象,使之状态不可变,安全地允许并发读取(对象都不可变了,怎么访问也无所谓了,避免数据竞争)这就够了。但这里指的是这个对象线程安全不安全而不是说讨论包裹该对象的方法是否线程安全。如果是讨论方法级别的线程安全,比如还要考虑可能出现竞态条件。
3.上锁确保状态变更的可见性(避免数据竞争)
方法的线程安全:其实就是多个对象,状态。
1.确保方法内部没有任何发布的对象,参考上述1
2.确保所有状态对象都是线程安全的(或者不可变保障不出现数据竞争),其次确保执行逻辑没有出现竞态条件(最简单上锁)
题外话:有个比较特殊的,for循环里删除list元素,此时是否出现了竞态条件?尽管方法看起来是线程的安全的,但是不当的编码导致了ConcurrentModificationException。严格意义上讲不算并发安全问题把 =-=
那么,如果面试官问你线程安全是否绝对线程安全,你可以以不同的线程安全角度去剖析了。一个是调用语义上看方法是否线程安全,另一个则是从对象是否线程安全来考虑。
《JAVA并发编程实战》PDF P59 END
理解了基本并发问题所在,可以尝试接着了解解决并发问题同时带来的性能损耗问题。了解更多并发的奥秘。一起看书吧~博客的知识点零零碎碎,书本才能带来系统化结构化的知识体系。(锁的实现,性能的损耗,分布式问题,JDK5并发包的奥秘)