• Java并发编程实战(chapter_2)(对象发布、不变性、设计线程安全类)


    又是三星期的生活。感觉自从有了这个分享之后,会无形多了一份动力,逼着自己不能落后,必须要去不停的学习,这其实是我想要的,各位少年团中的成员也都是有共鸣的,在此很感动,省去一万字。。。。。这一次会总结对象的安全发布、不变性,这几点,在我们工程实践中,同样也是非常具有参考与思考价值的基础知识点。看书枯燥,理解生涩,可是当你看过,理解一点,再平时业务代码中就会比别人多思考一分,就会比别人在更“恶劣”的网络环境中,更稳定一分。这几天想起《三傻》中,那句很经典的话:追求卓越,成功将会悄悄的靠近你。

    ## 一、发布与溢出 “发布(Publish)”一个对象的意思是指,使对象能够在当前作用于之外的代码中使用。这个“之外”,尤为关键,各种出问题的地方,都是因为这个“之外”所引起的。例如,如果在对象构造完成之前就发布该对象,就会破坏线程安全性。当某个不应该发布的对象被发布时,这种情况就被称为“溢出”。下面使用简单的例子进行说明:
    ### 1. 日常非常不注意的行为
    class Status {
        private String[] states = new String[]{"AA","BB","CC"};
    
        public String[] getStates(){
            return states;
        }
    }
    

    思考:很多人会不服的来争吵:这特么哪里有问题,跑了这么久的线上了,一直没出问题啊!好,那么问题了来:是不是线上一直没问题的代码,就是好代码?就是正确的代码?

    类似的代码还有:

    class Cache {
        private static HashMap<String,Object> cache = new HashMap<>();
    
        public static Object getCacheValue(String key){
            return return cache.get(key);
        }
    
        public static HashMap<String,Object> getCache(String key){
            return cache;
        }
    
        public static void addCache(String key, Object object){
            cache.push(key, object);
        }
    }
    

    P.S.:以上代码是我去年年底,再项目工程中看到的代码,而且在线上运行着,千真万确!

    ### 2. 分析问题所在

    你问我:这错在哪?如果我要回答,我会说:没错,你都没错。个人原因,我不喜欢程序员当面怼,因为我知道,大家都不容易,并且还知道:真的有问题那天,你知道痛了,你会主动改的,根本不用我说啥。当然,更严重的是,代码中(恩,线上代码),有人将states命名成了s,cache命名成了c,这我也说不了啥,什么叫做“追求卓越”,可能每人心中都会有自己的诠释吧。如果是下面代码出现在一个神不知鬼不觉的地方,请看:

    class Controller {
        public void cache(){//1
            Status status = new Status();
            String[] allStatus = status.getStates();
            Cache.addCache("ALL_STATUS",allStatus);
        }
    
        public void modify(){//2
            String[] allStatus = (String[])Cache.getCacheValue("ALL_STATUS");
            allStatus[0] = null;// 也许变成了其他值,null是一种比较极端的情况
        }
    
    
        public String getFirstUpcaseStatus(){//3
            String[] allStatus = (String[])Cache.getCacheValue("ALL_STATUS");
            return allStatus[0].toUpperCase(); // oh no! NPE!
        }
    
        public void remove(){//4
            Cache.getCache().remove("ALL_STATUS");
        }
    }
    
    • 1、2、3、4四个方法我们并不知道是什么时候触发的
    • 就是说时间顺序上,有可能是4号方法首先被触发,那1、2、3都将有问题
    • 即使4不被触发,先1、2,后3,也是出问题的
    • 也许我们代码写的很复杂,例如在2号程序中调用了非常多的service,用了非常多的设计模式,最终我们将修改数组中的值
    • 也许我们知道问题所在不去修改数组中的状态值,可是你能保证你能维护这个代码一辈子吗?
    • 以后交给两个人维护,两个人由于没啥子追求,别人代码不看,一个人在一边修改了数组,而另一个人在另一边使用了数组中的状态值,后果不堪设想
    ### 3. 更加隐蔽式的危险发布

    下面这种,新学到的一种危险性行为发布:

    public class ThisEscape{
        public ThisEscape(EventSource source){
            source.registerListener(e->doSomething(e));
        }
    }
    

    心得:请大家尽量使用Java8语法,整洁、大方、可撸(这是什么鬼!)
    思考:注意doSomething方法,会有什么问题呢?

    ### 4. 构造器与构造者
    • 作为构造者不要在构造器里面添加过多的逻辑,出错之后,这个锅你背不起!
    • 即使在一个构造器的最后一行,这个对象也是没有没初始化完成的!
    • this指针被发布出去,后果不堪设想,对象没初始化完成,而使用this指针。
    • 上面代码,可以在doSomething方法内部使用ThisEscape.this来访问父类
    • 如果父类没有初始化完,而访问父类,那将报错,这就是问题所在
    ### 5.针对这种隐蔽式的情况,我们怎么做
    public class SafeListener {
        private final EventListener listener;
    
        private SafeListener() {
            listener = e->doSomething(e);
        }
    
        public static SafeListener newInstance(EventSource source){
            SafeListener safe = new SafeListener();
            source.registerListener(safe.listener);
            return source;
        }
    }
    
    ### 6. 再举例一些不安全发布的例子
    
    class Holder{
        // 曝露类属性,大忌~
        public Holder holder;
    
        public void initialize(){
            holder = new Holder();
        }
    }
    
    //由于未被正确发布,因此这个类可能出现故障
    public class Holder{
        private int n;
    
        public Holder(int n){
            this.n = n;
        }
    
        public void assertSanity(){
            if(n != n){
                throw new Exception("initial erroe");
            }
        }
    }
    

    说明:抛出异常这个类是很玄乎的,因为线程可见性的原因,线程初次读取n的时候是老的值,可是这之后n值被其他线程更新,这个线程再次读取的时候,读取到一个失效的值,这就是抛出异常的原因。可以见得普普通通的自身与自身的比较,在多线程的环境下,都是很有问题的!!

    ### 6-线程封闭
    • 常见的封闭模式:栈封闭。就是在局部方法中使用一个变量,而不把他暴露出去。另外我自己的理解,每次方法返回一个新对象,也是一种使用方式。
    public int loadThe Ark(Collection<Animal> candidates){
        // 将animals封闭在方法内部
        SortedSet<Animal> animals;
        int numberParies = 0;
        Animal candidate = null;
    
        //针对animals容器进行各种统计
    
        return numberParise;
    
    }
    
    class Status {
        public String[] getStates(){
            //每次都返回新的对象数组
            return new String[]{"AA","BB","CC"};
        }
    }
    
    • 另一种封闭模式:ThreadLocal模式。这种模式也比较常用,每次在web项目中保存session的时候,常常使用这种模式,来标记当前访问线程的登陆情况。不过这个要注意的是,再web中使用TreadLocal容易导致溢出,具体的分析,请期待到springMVC系列。
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
        public Connection initialValue(){
            return DriverManager.getConnection(DB_URL);
        }
    };
    public static Connection getConnection(){
        return connectionHolder.get();
    }
    
    ### 7-给出写安全发布的模式
    • 在静态初始化函数中初始化一个对象引用
    • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中
    • 将对象的引用保存到某个正确构造对象的final类型域中
    • 将对象的引用保存到一个由锁保护的域中
    ## 二、不变性

    满足线程安全的另外一种方式,就是使用不可变对象。如果想要创建不可变对象的话,要满足以下条件:

    • 对象创建以后其状态就不能修改
    • 对象的所有域都是final类型
    • 对象是正确创建的(在对象的创建期间,this引用没有溢出)
    ### 1. 基础的不可能变模型

    这种方式,有点像我在《CC》观后感那篇文章中,讲到的一个观点:尽量对原始工具包中的类进行封装,有节制的使用其中的功能。下面代码就展示了,再可变对象的基础上构建不可变类

    
    public final class ThreeStooges{
        //注意,这个stooges变量是可变的!
        private final Set<String> stooges = new HashSet<>();
    
        public ThreeStooges(){
            stooges.add("1");
            stooges.add("2");
            stooges.add("3");
        }
    
        public boolean isStooges(String name){
            return stooges.contains(name);
        }
    }
    
    
    ### 2. 有点高端的货:使用不可变对象与volatile保证线程同步

    这里使用了三个内在的基本功点:对象不可变、对象读写分离、对象可见性。上代码:

    class Value{
        private final BigInteger lastNumber;
        private final BigInteger[] lastFactors;
    
        public Value(BigInteger lastNumber, BigInteger[] factors){
            this.lastNumber = lastNumber;
            this.lastFactors = Arrays.copyOf(factors,factors.length);//这里进行写复制
        }
        public BigInteger[] getFactors(BigInteger i){
            if(lastNumber = null || !lastNumber.equals(i))
                return null;
            else
                return Arrays.copyOf(lastFactors,lastFactors.length);//这里进行读复制
        }
    }
    
    

    说明:由于每次初始化时候都进行类属性的初始化,并与外界分离,因为factors数组每次都是复制一个副本进行初始化的!并且每次读的时候,也是讲数组对象进行复制分离。这样,只要一初始化对象之后,实际上,类对象里面的两个类属性都是不可变的了,因为全部与外界隔离了

    下面我们看看怎么使用:

    public class VolatileCacheFactorizer implements Servlet{
        private volatile Value cache = new Value(null,new BigInteger[0]);
    
        public void service(ServletRequest req, ServletResponse resp){
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = cache.getFactors(i);
            if(factors == null){
                factors = factor(i);
                cache = new Value(i,factors);
            }
            encodeIntoResponse(resp,factors);
        }
    }
    

    说明:这里cache类属性使用volatile,保证多线程写入的时候,都能够同步到主内存中,在这种情况下,多线程即是可见的,而通过Value对象的不变性又保证了对cache对象访问的安全性,那这样,整个service就是线程安全的了!

    ## 三、设计线程安全的类

    这一部分,我看书中涉及到很多名词,需要上网搜搜资料看看解释,否则读这一部分会很懵逼。我下面从一些名词解释入手来说说这一章。

    ### 1. 什么叫做监视器模式

    乍看之下还以为这是一种设计模式,的确是一种设计模式!不过还想不起来是什么样子的。我一google才发现是非常简单的,其实就是一段互斥访问的代码段(管程):

    class SynClass{
        private long value = 0;
    
        public synchronized long getValue(){
            return value;
        }
    
        public synchronized long increment(){
            if(value == Long.MAX_VALUE)
                throw new IllegalStateException("counter overflow");
            return ++value;
        }
    }
    

    说明:加了synchronized关键字的代码段,就相当一个屋子,每次只允许一个线程访问,如果访问有需求了,还可能进行挂起工作,那监视器是谁能?监视器就是对象本身,synchronized是加锁操作,这个锁也是这个对象持有的一个内部锁,如果要挂起代码,可是使用对象本身就天然继承自Object的wait方法,这就是监视器的作用。我看网上解释说:监视器(其实就是每个对象自己,因为每个对象都继承了Object)就像一个屋子的管理者,然后把对象这个“屋子”分成了三个地方:互斥访问区域、准备访问的区域和等待区域。

    ### 2. 什么叫做先验条件和后验条件
    • 先验条件(precondition):针对方法(method),它规定了在调用该方法之前必须为真的条件。
    • 后验条件(postcondition):也是针对方法,它规定了方法顺利执行完毕之后必须为真的条件。
    ### 3. 设计线程安全的类的三要素
    • 找出构成对象状态的所有变量
    • 找出约束状态变量的不变性条件
    • 建立对象状态的并发访问管理策略
    ### 4. 什么叫做不变性条件

    这个也是要做一定解释:程序在一系列的操作之后,还能够满足自己的先验条件和后验条件的,就叫做不变性条件(这个理解有点困难,大致我自己的想法是这样)

    ### 5. 收集同步的需求
    class SafeClass{
        private long value = 0;
    
        public synchronized long increment(){
            if(value == Long.MAX_VALUE)
                throw new IllegalStateException("counter overflow");
            return ++value;
        }
    }
    
    • 我们要做的,确定本类中的那些状态,会再多线程的操作下影响对象的不变性
    • 如果一个状态转变是依赖于前一个状态的话,那就会复合操作,需要同步机制
    • 当然,有些状态转变不依赖之前,例如温度
    • 上例中increment加上了synchronized就是一种保护程序不变性与后验条件的机制
    ### 6. 注意状态的所有权

    举个简单的例子

    class Owner{
        private SunChild sub;
    }
    

    其实这个sub对象就是Owner所拥有的一个子对象,所有权归Owner。但是如果加上如下代码

    class Owner{
        private SunChild sub;
    
        SubChild getSub(){
            return sub;
        }
    
    }
    

    这种情况下,所有权就被发布了出去,这样的情况就要考虑同步机制进行保护。

    注意:交出所有权的时候一定要多加思考程序的运行情景,以防不备!

    ### 7. 实例封闭

    如果某个对象不是线程安全的,我们可以将其进行封装,或者通过单一锁进行保护。下面是使用实例封闭模式进行的一种样例:

    public class PersonSet{
        private final Set<Person> mySet = new HashSet<>();//mySet本身并非线程安全
    
        public synchronized void addPerson(Person p){
            mySet.add(p)
        }
        public synchronized boolean contains(Person p){
            return mySet.contain(p);
        }
    }
    

    说明:将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程再访问数据时总能持有正确的锁

    ### 8. 线程安全的又一方式:委托

    委托,其实就是将对象的涉及到的影响可变性条件的状态,放到JDK提供的一些线程安全的容器中去,进行统一管理。同样也是一个简单的例子:

    
    public class Tracher{
        private final ConcurrentMap<String,Object> localMap;
    
        public Tracher(){
            localMap = new ConcurrentHashMap<>();
        }
    
        public Map<String,Object> getLocations(){
            return localMap;
        }
        public Object getLocation(String key){
            return lcoalMap.get(key);
        }
    }
    
    

    上面讲统一使用ConcurrentMap进行管理。如果想要获取一个不变的状态的话,可以进行读复制:

    public Map<String,Object> getLocations(){
        return new ConcurrentHashMap<>(localMap);
    }
    
    ### 9. 委托不是万能的

    过分依赖原子类所造成的“残局”:

    public class NumberRange {
        private final AtomicInteger lower = new AtomicInteger(0);
        private final AtomicInteger upper = new AtomicInteger(0);
    
        public void setLower(int i){
            if(i>upper.get()){//注意这里
                throw new Exception("error");
            }
            lower.set(i)
         }
    
         public AtomicInteger getUpper(){
            return upper;
         }
    }
    

    说明:由于upper被暴露了出去,可是setLower方法内部进行了“先检查后执行”的步骤,依赖于upper值,这样,lower属性的值就出现了不可预估性,原子操作没达成,原子类失效了。可以使用加锁来修改上述代码

    ### 10. 特别需要注意的由委托引起的非线程安全

    这种模式属于一种叫做“客户端加锁”,其实就是写程序中很不注意的,将内置锁和属性对象的锁混淆所致,下面是问题代码:

    public class ListHelper<E>{
        public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    
        ...
    
        public synchronized boolean putIfAbsent(E e){
        //注意这里synchronized使用的是内置锁
            boolean absent = !list.contains(e);
            if(absent){
                //这里add使用的是list对象里面的同步锁
                list.add(e);
            }
            return absent;
        }
    }
    

    两种锁并不一样,导致并没有对“先判断再执行”进行同步操作,还是会存在不安全性问题。下面是解决的方式:

    public class ListHelper<E>{
        public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    
        ...
    
        public boolean putIfAbsent(E e){
            synchronized (list){//统一使用属性对象的锁
                boolean absent = !list.contains(e);
                if(absent){
                    list.add(e);
                }
                return absent;
            }
        }
    }
    
    ## 四、总结

    本次主要讲了三个方面:

    • 对象的发布
    • 不变性
    • 设计线程安全的类

    相对来说比较枯燥,尽量都是用简洁明了的例子来混合讲解了,望给位看官多多包涵~哈哈哈。接下来要分享的东西,就会实用很多,涉及到JDK线程工具的良好实用(如闭锁、FutureTask等),并且我在接下来的线程分享文章中,会每次安排一个大章节,逐步进行生活必备品之一的java.util.concurrent.ThreadPoolExecutor源码分析,敬请期待!



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

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

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

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




  • 相关阅读:
    【MySQL】若sql语句中order by指定了多个字段,则怎么排序?
    【golang】golang中结构体的初始化方法(new方法)
    【feign】拦截输出日志
    【Feign】Feign ,OpenFeign以及Ribbon之间的区别?
    没有安装插件:node-sass , sass-loader@6.0.7 导致在style标签中使用 lang="scss" 报错
    解决:'webpack-dev-server' 不是内部或外部命令,也不是可运行的程序 或批处理文件。
    解决:flex弹性布局和溢出隐藏使用ellipsis省略号提示的冲突
    缺少标签template抱歉的包裹报错:[Vue warn]: Failed to mount component: template or render function not defined.
    两个(div)元素使用了display:inline-block后出现错位问题解决
    块级标签使用 display:inline-block;显示一行,存在多余的边距的bug解决办法
  • 原文地址:https://www.cnblogs.com/1024Community/p/8685396.html
Copyright © 2020-2023  润新知