(并发二)保障线程安全的底层思路?


先引入两个概念:发布和逸出

解释:

    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并发包的奥秘)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,621评论 18 399
  • 在一个方法内部定义的变量都存储在栈中,当这个函数运行结束后,其对应的栈就会被回收,此时,在其方法体中定义的变量将不...
    Y了个J阅读 4,416评论 1 14
  • 坐公交车的诗人 躲在嘈杂的游戏中 悄悄浏览各平台的诗歌 把自己藏进一个个意象 随跌宕的情感潮起潮落 “坐车看诗歌,...
    致远_8493阅读 152评论 0 1
  • 如果可以,我真想做路边的一盏灯,照亮别人的时候还可以温暖自己~需要的时候,把我点亮,不需要的时候放我静静呆...
    Mei_Xu阅读 129评论 0 0
  • 4当下最珍贵了吧。无论对错,眼下的是最真实的,也是最让人可以看到的,看明白的。
    vitamin喂喂喂阅读 207评论 0 0