• Java并发编程实战(chapter_1)(原子性、可见性)


    混混噩噩看了很多多线程的书籍,一直认为自己还不够资格去阅读这本书。有种要高登大堂的感觉,被各种网络上、朋友、同事一顿外加一顿的宣传与传颂,多多少少再自我内心中产生了一种敬畏感。2月28好开始看了之后,发现,其实完全没这个必要。除了翻译的烂之外(一大段中文下来,有时候你就会骂娘:这tm想说的是个shen me gui),所有的,多线程所必须掌握的知识点,深入点,全部涵盖其中,只能说,一书在手,万线程不愁那种!当然,你必须要全部读懂,并融汇贯通之后,才能有的效果。我推荐,看这本书的中文版本,不要和哪一段的冗长的字句在那过多的纠缠,尽量一段一段的读,然后获取这一段中最重要的那句话,否则你会陷入中文阅读理解的怪圈,而怀疑你的高中语文老师是不是体育老师客串的!!我举个例子:13页第八段,我整段读了三遍硬是没想明白前面那么多的文字,是干什么用的,就是最后一句话才是核心:告诉你,线程安全性,最正规的定义应该是什么!(情允许我,向上交的几个翻译此书的,所谓的“教授”致敬,在你们的引领下,使我们的意志与忍受力更上了一个台阶,人生更加完美!)

    ## 一、多线程开发所要平衡的几个点

    看了很多次的目录,外加看了第一部分,发现,要想做好多线程的开发,无非就是平衡好以下的几点

    • 安全性
    • 活跃性
      • 无限循环问题
      • 死锁问题
      • 饥饿问题
      • 活锁问题(这个还没具体的了解到)
    • 性能要求
      • 吞吐量的问题
      • 可伸缩性的问题
    ## 二、多线程开发所要关注的开发点

    要想平衡好以上几点,书中循序渐进的将多线程开发最应该修炼的几个点,娓娓道来:

    • 原子性
      • 先检查后执行
      • 原子类
      • 加锁机制
    • 可见性
      • 重排
      • 非64位写入问题
      • 对象的发布
      • 对象的封闭
    • 不变性

    在一本国人自己写的,介绍线程工具api的书中,看到了这么一句话:外练原子,内练可见。感觉这几点如果在多线程中尤为重要。我在有赞,去年还记得上线多门店的那天凌晨,最后项目启动报一个类加载的错误,一堆人过来看问题,基德大神站在攀哥的后面,最后淡淡的说了句:已经很明显是可见性问题了,加上volatile,不行的话,我把代码吃了!!可以见得,多线程这几个点,在“居家旅行”,生活工作中是多么的常见与重要!不出问题不要紧,只要一出,就会是头痛的大问题,因为你根本不好排查根本原因在这。所以我们需要平时就练好功底,尽量避免多线程问题的出现!而不是一味的用框架啊用框架、摞代码啊摞代码!

    ## 三、原子性下面的安全问题
    **1. 下面代码有什么问题呢?**
    public class UnsafeConuntingFactorizer implements Servlet{
        private long count = 0;
        private long getCount(){
            return count;
        }
        public void service(ServletRequest req, ServletResponse resp){
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            ++count;
            encodeIntoResponse(resp,factor);
        }
    }
    

    思考:如何让一个普普通通的类变得线程安全呢?一个类什么叫做有状态,而什么又叫做无状态呢?

    **2. 上面代码分析**
    • 一个请求的方法,实例都是一个,所以每次请求都会访问同一个对象
    • 每个请求,使用一个线程,这就是典型的多线程模型
    • count是一个对象状态属性,被多个线程共享
    • ++count并非一次原子操作(分成:复制count->对复制体修改->使用复制体回写count,三个步奏)
    • 多个线程有可能多次修改count值,而结果却相同
    **3. 使用原子类解决上面代码问题**
    public class UnsafeConuntingFactorizer implements Servlet{
        private final AtomicLong count = new AtomicLong(0);
        private long getCount(){
            return count.get();
        }
        public void service(ServletRequest req, ServletResponse resp){
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            count.incrementAndGet();//使用了新的原子类的原子方法
            encodeIntoResponse(resp,factor);
        }
    }
    
    **4. 原子类也不是万能的**
    //在复杂的场景下,使用多个原子类的对象
    public class UnsafeConuntingFactorizer implements Servlet{
        private final AtomicReference<BigInteger> lastNumber 
            = new AtomicReference<BigInteger>();
        private final AtomicReference<BigInteger[]> lastFactors 
            = new AtomicReference<BigInteger[]>();
    
    
        public void service(ServletRequest req, ServletResponse resp){
            BigInteger i = extractFromRequest(req);
            if(i.equals(lastNumber.get())){//先判断再处理,并没有进行同步,not safe!
                encodeIntoResponse(resp,lastFactors.get());
            }else{
                BigInteger[] factors = factor(i);
                lastNumer.set(i);
                lastFactors.set(factors);
                encodeIntoResponse(resp, factors);
            }
        }
    }
    

    思考:什么叫做复合型操作?

    **5. 先列举一个我们常见的复合型操作**
    public class LazyInitRace {
        private ExpensiveObject instace = null;
        public ExpensiveObject getInstace(){
            if(instace == null){
                instace = new ExpensiveObject();
            }
            return instace;
        }
    }
    

    看好了,这就是我们深恶痛绝的一段代码!如果这段代码还分析不了的,对不起,出门左转~

    **6. 提高“先判断再处理”的警觉性**
    • 如果没有同步措施,直接对一个状态进行判断,然后设值的,都是不安全的
    • if操作和下面代码快中的代码,远远不是原子的
    • 如果if判断完之后,接下来线程挂起,其他线程进入判断流程,又是同样的状态,同样进入if语句块
    • 当然,只有一个线程执行的程序,请忽略(那还叫能用的程序吗?)
    **7. 性能的问题来了**
    //在复杂的场景下,使用多个原子类的对象
    public class UnsafeConuntingFactorizer implements Servlet{
        private final AtomicReference<BigInteger> lastNumber 
            = new AtomicReference<BigInteger>();
        private final AtomicReference<BigInteger[]> lastFactors 
            = new AtomicReference<BigInteger[]>();
    
        //这下子总算同步了!
        public synchronized void service(ServletRequest req, ServletResponse resp){
            BigInteger i = extractFromRequest(req);
            if(i.equals(lastNumber.get())){//先判断再处理,并没有进行同步,not safe!
                encodeIntoResponse(resp,lastFactors.get());
            }else{
                BigInteger[] factors = factor(i);
                lastNumer.set(i);
                lastFactors.set(factors);
                encodeIntoResponse(resp, factors);
            }
        }
    }
    

    思考:有没有种“关公挥大刀,一砍一大片”的感觉?

    **8. 上诉代码解析** - 加上了``synchronized``关键字的确解决了多线程访问,类安全性问题 - 可是每次都是一个线程进行计算,所有请求变成了串行 - 请求量低于100/s其实都还能接受,可是再高的话,这就完全有问题的代码了 - 性能问题,再网络里面,是永痕的心病~
    **9. 一段针对原子性、性能问题的解决方案**
    //在复杂的场景下,使用多个原子类的对象
    public class CacheFactorizer implements Servlet{
        private BigInteger lastNumber;
        private BigInteger[] lastFactors ;
        private long hits;
        private long cacheHits;
    
        public synchronized long getHits(){
            return hits;
        }
        public synchronized double getCacheHitRadio(){
            return (double) cacheHits / (double) hits;
        }
    
        public void service(ServletRequest req, ServletResponse resp){
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = null;
            synchronized (this){
                ++hits;
                if(i.equals(lastNumber)){
                    ++cacheHits;
                    factors = lastFactors.clone();
                }
            }
            if (factors == null){
                factors = factor(i);
                synchronized (this){
                    lastNumer = i;
                    lastFactors = factors.clone();
                }
            }
            encodeIntoResponse(resp, factors);
        }
    }
    

    在修改状态值的时候,才进行加锁,平时对状态值的读操作可以不用加锁,当然,最耗时的计算过程,也是要同步的,这种情况下,才会进一步提高性能。

    ## 四、可见性下面的对象共享

    可见性这个话题,在多线程的环境下,是相当棘手的。可能多年之后,你成为万人心中的老鸟,也同样会对这个问题,怅然若失!我自己总结了几点,可见性问题的难处:

    • 道理简单,真实场景代码错中复杂,你根本不知道是可见性导致的
    • 小概率事件,往往可能只有百分之一,甚至千分之一的出事概率,容易被我们“得过且过”
    • 容易直接扔到一个synchronized加锁块里面,进行“大刀”式的处理,而忽略了高效性
    • 可见性+原子性的综合考虑

    针对这些问题,我们只能先从基本功抓起,然后在日积月累的开发工作中,多多分析程序运行的场景,多多尝试,才能大有裨益。

    插曲:昨天看了《恋爱回旋》这部日本电影,其中有个场景让我记忆深刻:女主是小时候被魔鬼母亲常年训练的乒乓球少年运动员,后来总总原因,放弃了乒乓球,当起了OL,这一别就是15年。当再次碰到男主的时候,男主向女主发起乒乓球挑战,以为女主是个菜逼,然后赌一些必须要让女主完成的事情。(女主本人也是觉得乒乓球对自己是一种心理的负担,并且放弃这么久了,所以没啥子自信)没想到,女主一拿球拍,在接发球的那一刹那。。。。。大家应该都懂了。我当时就在影院中说出声来:基本功太重要了。

    **1. 可见性的发生的必要条件**

    可见性,无非就是再多线程环境下,对共享变量的读写导致的。可能一个线程修改了共享变量的值,而另一个线程读取的还是老的值,差不多就是这么大白话的解释了下来。其中发生的必要条件有:

    • 多线程环境访问同一个共享变量
    • 服务器模式下启动程序
    • 共享变量并没有做什么处理,代码块也没有同步

    当然,要分析为什么会有可见性的问题,要结合JVM虚拟机内存模型分析。以后会在《深入理解Java虚拟机》的学习中,做详细的分析,敬请期待。

    **2. 不多说上代码**
    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;
        }
    }
    

    思考:上面的打印number的值,有可能会有几种结果呢?什么情况下出现的这些个结果

    **3. 上诉代码分析**
    • number最终打印结果有可能出现42、0,或者根本就不会打印
    • 42:这种情况是运行正确的结果
    • 0:这种情况发生了指令重排(五星级的问题)
    • 不会打印:主线程对ReaderThread线程出现了共享变量不可见
    **4. “愚钝”的聊聊指令重排**

    之所以说是“愚钝”,原因是重排问题,是一个很底层很考验计算机基础能力的一个问题,小弟不才,当年分析计算机组成原理与指令结构的时候,枯燥极致,都睡过去了。现在回头,才知道其重要性。现阶段,对重排的分析,我只能举例个简单的例子,进行说明,更进一步的分析,同样是要结合JVM的机制(六大Happens-before)来分析,以后再做进一步,详尽的分析。下面就是那个简单的例子:

    //简单例子
    public class OrderConfuse{
        public static void main(String[] args){
            int a = 1;
            int b = 2;
            int c = a+b;
            int d = c+a;
            System.out.println(c);
            System.out.println(d);
        }
    }
    
    • 上面程序是正确的,也能正确输出
    • 对a和b的赋值操作,并非先赋值a再赋值b的
    • 原因是JVM底层会对指令进行优化,保证程序的快速执行,其实就是一种效率优化
    • 变量c会用到a和b变量,所以a和b的操作必须要发生在c之前(happens-before)
    • 有可能b进行了赋值,而a还是初始化的状态,就是值为0

    所以结合前面的代码段:

    public class NoVisibility{
        ......
        public static void main(String[] args){
            new ReaderThread().start();
            number = 42;
            ready = true;
        }
    }
    
    • number和ready之后,并没有使用它们的变量了
    • number和ready会被进行指令重排
    • 结果就是:ready已经赋值变成了true,可是number还是0

    这就是为啥会为零的原因所在!

    **5. 针对可见性,还是要上JVM的内存模型进行简单分析**
    • 每个线程都会有自己的线程虚拟机栈
    • 栈上面存储原始类型和对象类型的引用
    • 每次启动一个线程,都会在对共享数据进行一次复制,复制到每个线程的虚拟机栈中
    • 上面的number是在主线程中,同时在ReaderThread线程的虚拟机栈中有一个副本
    • 各个虚拟机栈最终要进行同步,才能保持一致
    • 所以每次修改一个共享变量(原始类型)其实是在本地线程空间里面修改
    • number在主线程里面修改了,可是在ReaderThread线程里面并没有修改,因为两个线程访问的空间并不一样,一个线程对另一个线程空间并不可见。
    **6. volatile关键字横空出世**

    volatile关键字的作用,主要有一下几点:

    • 能把对变量的修改,马上同步到主存中
    • 各个线程立马更新自己线程栈中的变量值
    • 防止指令重排
    • 无法保证原子性

    对于最底层如何做到这些个点的,具体还可以分析,例如什么内存屏障、状态过期等等,完全可以聊一个专题,今天再次先不聊,同样放到《深入理解JVM虚拟机》的学习中来详尽分析。所以,上面程序可以改成下面这个样子:

    public class Visibility{
        private static volatile boolean ready;//注意这个类型
        private static volatile 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;
        }
    }
    
    **7. synchronized关键字同样可以保证可见性**
    public class Visibility{
        private static boolean ready;
        private static int number;
    
        private static Object lock = new Object();
    
        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();
            synchronized(lock){//这里进行了加锁
                number = 42;
                ready = true;
            }
        }
    }
    
    • 加锁可以同时保证可见性与原子性
    • 加锁同样可以防止指令重排,内部代码都会照顺序执行
    **8. volatile不是万能的**
    public class VisibleNotAtomic{
        private static volatile int number = 1;
    
        private static class ReadThread extends Thread{
            public void run(){
                if(number == 2){
                    System.out.println("correct!");
                }else{
                    System.out.println("error!");
                }
            }
        }
        public static void main(String[] args){
            number++;
        }
    }
    
    • number是对主线程和ReadThread线程都可见的
    • 可是number++不是原子操作
    • 加加到了一半,主线程挂起,ReadThread线程运行,number的值还是1,输出error
    ## 五、第一部分总结

    我们主要讲了线程的原子性和可见性,结合代码,不知不觉就讲了一堆,而且感觉还可以在讲~~多线程的话题真的是太恐怖了!未来的可预见性的规划如下:

    • 对象的安全发布
    • 对象的不变性
    • 对象的合理加锁
    • 生产者消费者模型
    • 构建高效可伸缩的缓存

    恩,敬请期待~



    许多年前 你有一双清澈的双眼

    奔跑起来 像是一道春天的闪电

    想看遍这世界 去最遥远的远方

    感觉有双翅膀 能飞越高山和海洋




  • 相关阅读:
    [转载]使用消息队列实现分布式事务-公认较为理想的分布式事务解决方案
    【异常】Error: ERROR 1012 (42M03): Table undefined. (state=42M03,code=1012)
    hbase极度不稳定问题,经常的RIT问题
    ERROR: Version file does not exist in root dir hdfs://XXXXXXX:8020/tmp/hbase-hbase/hbase
    su无法切换一个普通用户hbase
    配置了ssh免密登录,仍然需要输入密码
    异常-Phoenix HBASE Last region should end with an empty key. You need to create a new region and regioninfo in HDFS to plug the hole
    Hbase Region in transition问题解决
    异常-Maxwell无法全量同步触发
    异常-No suppression parameter found for notification
  • 原文地址:https://www.cnblogs.com/1024Community/p/8542938.html
Copyright © 2020-2023  润新知