美文网首页程序员专栏Java
快速鸟瞰并发编程, 呕心沥血整理的架构技术【2】

快速鸟瞰并发编程, 呕心沥血整理的架构技术【2】

作者: 享学课堂 | 来源:发表于2019-07-26 16:10 被阅读2次

    作者:享学课堂James老师

    转载请声明出处!

    接着第1篇后,我们继续来跟进一下并发编程的其它内容,如下:

    第5节 安全发布

    要安全的发布一个对象,对象的引用和对象的状态必须同时对其他线程可见。一般一个正确构造的对象(构造函数不发生this操作),可以通过如下方式来正确发布:

    • 在静态初始化函数中初始化一个对象引用。只有一个线程可以初始化静态变量,因为类的初始化是在独占锁下完成的。
    class  JamesStaticInitializer {
        // 在不进行其他初始化的情况下发布不可变对象
        public  static  final  Year year = Year.of(2017);
        public  static  final  Set<String> keywords;
        // 使用静态static来构造复杂对象
        static {
            // 创建可变集
            Set<String> keywordsSet = new  HashSet<>();
            // 初始化状态
            keywordsSet.add("james");
            keywordsSet.add("13号技师");
            // 使集合不可修改
            keywords = Collections.unmodifiableSet(keywordsSet);
        }
    }
    
    • 将一个对象引用保存在volatile类型的域或者是AtomicReference对象中。
    class  JamesVolatile {
        private  volatile  String state;
        void setState(String state) {
            this.state = state;
        }
        String getState() {
            return state;
        }
    }
    
    • AtomicInteger将值存储在volatile字段中,因此volatile变量的相同规则适用于此处。
    class  JamesAtomics {
        private  final  AtomicInteger state = new  AtomicInteger();
        void initializeState(int state) {
            this.state.compareAndSet(0, state);
        }
        int getState() {
            return state.get();
        }
    }
    
    • final域, 将对象的引用保存到某个正确构造对象的final类型的域中。
    class  JamesFinal {
        private  final  String state;
        JamesFinal(String state) {
            this.state = state;
        }
        String getState() {
            return state;
        }
    }
    

    确保此引用在构造期间不逃逸。

    this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了。这是危及到线程安全的,因为其他线程有可能通过这个逸出的引用访问到“初始化了一半”的对象(partially-constructed object)。这样就会出现某些线程中看到该对象的状态是没初始化完的状态,而在另外一些线程看到的却是已经初始化完的状态,这种不一致性是不确定的,程序也会因此而产生一些无法预知的并发错误。在说明并发编程中如何避免this引用逸出之前

    class  JamesThisEscapes {
        private  final  String name;
        ThisEscapes(String name) {
            JamesCache.putIntoCache(this);
            this.name = name;
        }
        String getName() {
            return name;
        }
    }
    class  JamesCache {
        private  static  final  Map<String, ThisEscapes> CACHE = new  ConcurrentHashMap<>();
        static  void putIntoCache(JamesThisEscapes thisEscapes) {
            //“this”引用在对象完全构造之前逃逸
            CACHE.putIfAbsent(thisEscapes.getName(), thisEscapes);
        }
    }
    
    • 正确同步成员变量。
    class  JamesSynchronization {
        private  String state;
        synchronized  String getState() {
            if (state == null)
            state = "Initial";
            return state;
        }
    }
    

    第6节 不可变的对象

    不可变对象具备执行安全的特性。此外,相较于可变对象,不可变对象通常也较合理,易于了解,而且提供较高的安全性。不可变对象的一个重要特性是它们都是线程安全的,因此不需要同步。当然对象不可变的是有如下要求滴:

    • 所有变量都是 final.
    • 所有变量必须是可变对象或不可变对象。
    • this 在构造方法执行期间引用不会逃脱。
    • 该类是final,因此无法在子类中覆盖此行为。

    不可变对象的示例:

    // 声明为final类
    public  final  class  JamesArtist {
        // 不可变对象, 字段为final
        private  final  String name;
        //用于保存不可变对象, final类型
        private  final  List<JamesTrack> tracks;
        public  JamesArtist(String name, List<JamesTrack> tracks) {
            this.name = name;
            //防止拷贝
            List<JamesTrack> copy = new  ArrayList<>(tracks);
            //标记为不可更改
            this.tracks = Collections.unmodifiableList(copy);
            // “this”在构造期间不会传递到任何地方。
        }
    }
    // 同上声明为final类
    public  final  class  JamesTrack {
        // 不可变对象, 字段为final
        private  final  String title;
        public  JamesTrack(String title) {
            this.title = title;
        }
    }
    

    第7节 线程Thread类

    java.lang.Thread类用于表示应用程序或JVM线程。代码总是在某些Thread类的上下文中执行(用于 Thread#currentThread()获取自己的Thread)。

    线程状态如下

    线程协调方法如下

    怎么处理 InterruptedException异常?

    • 在重新抛出 InterruptedException 之前执行特定于任务的清理工作。
    • 声明当前方法抛出 InterruptedException.
    • 如果未声明某个方法抛出 InterruptedException,则应通过调用将中断的标志恢复为true, Thread.currentThread().interrupt()并且应该抛出一个更合适的异常。将标志设置为true非常重要,以便有机会处理更高级别的中断。

    不可预知的异常处理

    线程可以指定 UncaughtExceptionHandler将接收任何导致线程突然终止的未捕获异常的通知。

    Thread thread = new  Thread(runnable);
    thread.setUncaughtExceptionHandler((failedThread,exception)->
    {
        logger.error("Caught unexpected exception in thread
    ‘{}’.", failedThread.getName(), exception);
    }
    );
    thread.start();
    

    第8节 线程的活跃度

    死锁

    当存在多个线程时会发生死锁,每个线程等待另一个线程持有的资源,从而形成资源循环和获取线程。

    潜在的死锁示例:

    class  JamesAccount {
        private  long amount;
        void plus(long amount) {
            this.amount += amount;
        }
        void minus(long amount) {
            if (this.amount < amount)
            throw  new  IllegalArgumentException(); else
            this.amount -= amount;
        }
        static  void transferWithDeadlock(long amount, JamesAccount first, JamesAccount second) {
            synchronized (first) {
                synchronized (second) {
                    first.minus(amount);
                    second.plus(amount);
                }
            }
        }
    }
    

    如果同时发生死锁:

    • 一个线程正在尝试从第一个帐户转移到第二个帐户,并且已经获得了第一个帐户的锁。
    • 另一个线程正在尝试从第二个帐户转移到第一个帐户,并且已经获得了第二个帐户的锁。

    避免死锁的技巧:

    • 锁定顺序 - 始终以相同的顺序获取锁。
    class  JamesAccount {
        private  long id;
        private  long amount;
        // 此处省略了一些方法
        static  void transferWithLockOrdering(long amount, JamesAccount first, JamesAccount second) {
            Boolean lockOnFirstAccountFirst = first.id < second.id;
            Account firstLock = lockOnFirstAccountFirst ? first : second;
            Account secondLock = lockOnFirstAccountFirst ? second : first;
            synchronized (firstLock) {
                synchronized (secondLock) {
                    first.minus(amount);
                    second.plus(amount);
                }
            }
        }
    }
    
    • 锁定超时 - 在获取锁时不要无限制地阻塞,应该释放所有锁后再尝试。
    class  JamesAccount {
        private  long amount;
        //省略了一些方法
        static  void transferWithTimeout(long amount, JamesAccount first, JamesAccount second, int retries, long timeoutMillis)
        throws  InterruptedException {
            for (int attempt = 0; attempt < retries; attempt++) {
                if (first.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS)) {
                    try {
                        if (second.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS)) {
                            try {
                                first.minus(amount);
                                second.plus(amount);
                            }
                            finally {
                                second.lock.unlock();
                            }
                        }
                    }
                    finally {
                        first.lock.unlock();
                    }
                }
            }
        }
    }
    

    JVM可以检测监视器死锁,并在线程转储中打印死锁信息。

    活锁和线程饥饿

    活锁恰恰与死锁的概念相反,死锁的意思是所有线程都拿不到资源且都占用着对方的资源,而活锁的意思是拿到资源后各线程却又相互释放不执行。

    如果多线程中出现了相互谦让,相当于13号技师得病后大家都不要她了,都主动将资源释放给其它的线程来使用,那么这个资源会不断在多个线程之间跳动但又得不到明确执行,这就是活锁;而且多线程执行中有线程的优先级,有的线程优先级高它是能够插队并优先执行的,但如果这些优先级高的线程一直抢占优先级低线程的资源,最终会导致低优先级线程它是无法得到执行的,这就是饥饿。

    (未完待续......)

    关注我,等待更新,还有更多技术干货分享~

    相关文章

      网友评论

        本文标题:快速鸟瞰并发编程, 呕心沥血整理的架构技术【2】

        本文链接:https://www.haomeiwen.com/subject/vgxrrctx.html