• 《Effective Java 第三版》——第十一章 并发


    《Effective Java 第三版》——第二章 创建和销毁对象

    《Effective Java 第三版》——第三章 所有对象都通用的方法

    《Effective Java 第三版》——第四章 类和接口 

    《Effective Java 第三版》——第五章 泛型

     《Effective Java 第三版》——第六章 枚举和注解

     《Effective Java 第三版》——第七章 Lambda 和 Stream 

     《Effective Java 第三版》——第八章 方法

    《Effective Java 第三版》——第九章 通用编程

    《Effective Java 第三版》——第十章 异常

      《Effective Java 第三版》——第十一章 并发

     

     

     

    Effective Item - 外部方法导致的过度同步问题

     
     

    我们时常用同步来解决并发程序的安全性和活跃性等问题,虽然性能有些许降低,但保证了程序的运行。

    而过度同步导致的问题不仅仅是性能问题,安全性、活跃性还有各种不确定的问题都有可能出现。

     

     

    To avoid liveness and safety failures, never cede control to the client within a synchronized method or block.

    我们需要保证在自己的代码中控制客户端(外部方法)以避免活跃性和安全性问题。

    换句话说,不要在同步区域中调用无法确定行为的方法,因为线程之间的通信,这样会导致无法预料的结果。

    那么如何给同步区域提供一个无法确定行为的外部方法?

    一种比较简单的方式是调用某个抽象声明的行为,并在运行期赋予其实例。

    比如观察者模式,每个观察者都由同一个抽象进行描述,被观察者通过同一种描述调用观察者的行为。

    只是在这个例子中,我们需要在同步区域调用观察者的行为。

    现在我打算在某个集合添加元素时提醒所有观察者,但由于add方法可能是self-use,这里先用一段composition,贴出代码仅为方便,阅读时请直接略过:

    import java.util.Collection;
    import java.util.Iterator;
    import java.util.Set;
    
    public class ForwardingSet<E> implements Set<E> {
        private final Set<E> s;
    
        public ForwardingSet(Set<E> s) {
            this.s = s;
        }
    
        public void clear() {
            s.clear();
        }
    
        public boolean contains(Object o) {
            return s.contains(o);
        }
    
        public boolean isEmpty() {
            return s.isEmpty();
        }
    
        public int size() {
            return s.size();
        }
    
        public Iterator<E> iterator() {
            return s.iterator();
        }
    
        public boolean add(E e) {
            return s.add(e);
        }
    
        public boolean remove(Object o) {
            return s.remove(o);
        }
    
        public boolean containsAll(Collection<?> c) {
            return s.containsAll(c);
        }
    
        public boolean addAll(Collection<? extends E> c) {
            return s.addAll(c);
        }
    
        public boolean removeAll(Collection<?> c) {
            return s.removeAll(c);
        }
    
        public boolean retainAll(Collection<?> c) {
            return s.retainAll(c);
        }
    
        public Object[] toArray() {
            return s.toArray();
        }
    
        public <T> T[] toArray(T[] a) {
            return s.toArray(a);
        }
    
        @Override
        public boolean equals(Object o) {
            return s.equals(o);
        }
    
        @Override
        public int hashCode() {
            return s.hashCode();
        }
    
        @Override
        public String toString() {
            return s.toString();
        }
    }
    ForwardingSet
     

    下面是被观察的ObservableSet,问题的关键就是那个notifyElementAdded方法,其同步区域中的observe.added正是无法确定的行为:

    public class ObservableSet<E> extends ForwardingSet<E> {
        public ObservableSet(Set<E> set) {
            super(set);
        }
    
        private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();
    
        public void addObserver(SetObserver<E> observer) {
            synchronized (observers) {
                observers.add(observer);
            }
        }
    
        public boolean removeObserver(SetObserver<E> observer) {
            synchronized (observers) {
                return observers.remove(observer);
            }
        }
    
        private void notifyElementAdded(E element) {
            synchronized (observers) {
                for (SetObserver<E> observer : observers)
                    observer.added(this, element); //key code
            }
        }
        
      // 重载 forwarding 的方法,调用 notifyElementAdded
        @Override
        public boolean add(E element) {
            boolean added = super.add(element);
            if (added)
                notifyElementAdded(element);
            return added;
        }
    
        @Override
        public boolean addAll(Collection<? extends E> c) {
            boolean result = false;
            for (E element : c)
                result |= add(element);
            return result;
        }
    }
     

    不出意料的,观察者接口仅仅描述上面的added方法:

    // 这是一个函数接口
    public interface SetObserver<E> {
        void added(ObservableSet<E> set, E element);
    }

     这个函数接口的实现,Test1 是Lambda;Test2 和 Test3是匿名类——因为要回调,将自身传出去

     

     

     

    不考虑任何复杂的情况,我们试着加入一个打印集合元素的observer,这段程序自然会打印0~99:

    //对应原书的 Test1 
    
    public static void main(String[] args) {
        ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
    
        set.addObserver(new SetObserver<Integer>() {
        public void added(ObservableSet<Integer> s, Integer e) {
            System.out.println(e);
        }
        });
    
        for (int i = 0; i < 100; i++) set.add(i);
    }
     

    接着换个花样,试着在循环过程中移除该observer,不出意外地,会出现ConcurrentModificationException:

    // 对应原书的 Test2
    
    public static void main(String[] args) {
        ObservableSet<Integer> set = new ObservableSet<Integer>(new HashSet<Integer>());
    
        set.addObserver(new SetObserver<Integer>() {
        public void added(ObservableSet<Integer> s, Integer e) {
            System.out.println(e);
            if (e == 23) s.removeObserver(this);
        }
        });
    
        for (int i = 0; i < 100; i++) set.add(i);
    }

    既然observer不能自己移除自己,我们试着将移除该observer的工作交给其他线程试试:

    //  对y应原书的 Test3
    
        public static void main(String[] args) {
            ObservableSet<Integer> set = new ObservableSet<Integer>(
                    new HashSet<Integer>());
    
            set.addObserver(new SetObserver<Integer>() {
                public void added(final ObservableSet<Integer> s, Integer e) {
                    System.out.println(e);
                    if (e == 23) {
                        ExecutorService executor = Executors
                                .newSingleThreadExecutor();
                        final SetObserver<Integer> observer = this;
                        try {
                            executor.submit(new Runnable() {
                                public void run() {
                                    s.removeObserver(observer);
                                }
                            }).get();
                        } catch (ExecutionException ex) {
                            throw new AssertionError(ex.getCause());
                        } catch (InterruptedException ex) {
                            throw new AssertionError(ex.getCause());
                        } finally {
                            executor.shutdown();
                        }
                    }
                }
            });
    
            for (int i = 0; i < 100; i++)
                set.add(i);
        }
     

    注意这里使用的是Executor,这样做的主要是为了防止锁重入,如果在方法内部再成功获得锁则毫无意义,和之前例子没有差异。

    但使用Executor将当前observer从集合中移除时却发生了死锁。

    原因也很简单,executor的removeObserver希望获得锁,但是锁已被主线程占用,而主线程却等待executor执行结束。

    而解决这一切的方法是不在同步区域内调用外来方法,即:

    private void notifyElementAdded(E element) {
        List<SetObserver<E>> snapshot = null;
        synchronized(observers) {
            snapshot = new ArrayList<SetObserver<E>>(observers);
        }
        for (SetObserver<E> observer : snapshot)
            observer.added(this, element);
    }

    (PS:对应Effective Java Second Edition Item 67)

     

     我的机器上observer移除成功了。。。

    汗。。。

        private void notifyElementAdded(E element) {
            System.out.print("observers.size() in func notifyElementAdded:	");
            System.out.println(observers.size());
            for (SetObserver<E> observer : observers)
                observer.added(this, element);
        }

    Test2 运行没报错:

    /Library/Java/JavaVirtualMachines/jdk-13.0.2.jdk/Contents/Home/bin/java -Dfile.encoding=UTF-8 -classpath /Users/didi/git/effective-java-3e-source-code/bin effectivejava.chapter11.item79.Test2
    observers.size() in func notifyElementAdded:    1
    0
    observers.size() in func notifyElementAdded:    1
    1
    observers.size() in func notifyElementAdded:    1
    2
    observers.size() in func notifyElementAdded:    1
    3
    observers.size() in func notifyElementAdded:    1
    4
    observers.size() in func notifyElementAdded:    1
    5
    observers.size() in func notifyElementAdded:    1
    6
    observers.size() in func notifyElementAdded:    1
    7
    observers.size() in func notifyElementAdded:    1
    8
    observers.size() in func notifyElementAdded:    1
    9
    observers.size() in func notifyElementAdded:    1
    10
    observers.size() in func notifyElementAdded:    1
    11
    observers.size() in func notifyElementAdded:    1
    12
    observers.size() in func notifyElementAdded:    1
    13
    observers.size() in func notifyElementAdded:    1
    14
    observers.size() in func notifyElementAdded:    1
    15
    observers.size() in func notifyElementAdded:    1
    16
    observers.size() in func notifyElementAdded:    1
    17
    observers.size() in func notifyElementAdded:    1
    18
    observers.size() in func notifyElementAdded:    1
    19
    observers.size() in func notifyElementAdded:    1
    20
    observers.size() in func notifyElementAdded:    1
    21
    observers.size() in func notifyElementAdded:    1
    22
    observers.size() in func notifyElementAdded:    1
    23
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    observers.size() in func notifyElementAdded:    0
    
    Process finished with exit code 0
        private void notifyElementAdded(E element) {
            synchronized(observers) {
                for (SetObserver<E> observer : observers)
                    observer.added(this, element);
            }
        }

    // Alien method moved outside of synchronized block - open calls
        private void notifyElementAdded(E element) {
            List<SetObserver<E>> snapshot = null;
            synchronized(observers) {
                snapshot = new ArrayList<>(observers);
            }
            for (SetObserver<E> observer : snapshot)
                observer.added(this, element);
        }

    private void notifyElementAdded(E element) {
            for (SetObserver<E> observer : observers)
                observer.added(this, element);
        }

    三种写法都能运行成功。。。

     

    put与putIfAbsent区别:

    put在放入数据时,如果放入数据的key已经存在与Map中,最后放入的数据会覆盖之前存在的数据,

    而putIfAbsent在放入数据时,如果存在重复的key,那么putIfAbsent不会放入值。

    putIfAbsent   如果传入key对应的value已经存在,就返回存在的value,不进行替换。如果不存在,就添加key和value,返回null

     

    String.intern()管理内部的本机实现的池,该池具有一些与GC相关的特殊功能。这是旧的代码,但是如果重新实现,将使用java.util.WeakHashMap。弱引用是一种保留指向对象的指针而又不阻止其被收集的方法。对于统一池(例如,内部字符串),这是正确的选择。

     
    package effectivejava.chapter11.item81;
    import java.util.concurrent.*;
    
    // Concurrent canonicalizing map atop ConcurrentMap - Pages 273-274
    public class Intern {
        // Concurrent canonicalizing map atop ConcurrentMap - not optimal
        private static final ConcurrentMap<String, String> map =
                new ConcurrentHashMap<>();
    
    //    public static String intern(String s) {
    //        String previousValue = map.putIfAbsent(s, s);
    //        return previousValue == null ? s : previousValue;
    //    }
    
        // Concurrent canonicalizing map atop ConcurrentMap - faster!
        public static String intern(String s) {
            String result = map.get(s);
            if (result == null) {
                result = map.putIfAbsent(s, s);
                if (result == null)
                    result = s;
            }
            return result;
        }
    }
     

    package effectivejava.chapter11.item81;
    import java.util.concurrent.*;
    
    // Simple framework for timing concurrent execution 327
    public class ConcurrentTimer {
        private ConcurrentTimer() { } // Noninstantiable
    
        public static long time(Executor executor, int concurrency,
                                Runnable action) throws InterruptedException {
            CountDownLatch ready = new CountDownLatch(concurrency);
            CountDownLatch start = new CountDownLatch(1);
            CountDownLatch done  = new CountDownLatch(concurrency);
    
            for (int i = 0; i < concurrency; i++) {
                executor.execute(() -> {
                    ready.countDown(); // Tell timer we're ready
                    try {
                        start.await(); // Wait till peers are ready
                        action.run();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    } finally {
                        done.countDown();  // Tell timer we're done
                    }
                });
            }
    
            ready.await();     // Wait for all workers to be ready
            long startNanos = System.nanoTime();
            start.countDown(); // And they're off!
            done.await();      // Wait for all workers to finish
            return System.nanoTime() - startNanos;
        }
    }

    例子比较精巧,值得学习:

    所有的 worker 线程都 ready 了,各worker 才/就 开始执行;

    最后一个跑完的 worker 结束执行了,才算全部执行完成;

     

     每个条目最后那段总结的话优先记住

     

    package effectivejava.chapter11.item83;
    
    // Initialization styles - Pages 333-
    public class Initialization {
    
        // Normal initialization of an instance field4 - Page 282
        private final FieldType field1 = computeFieldValue();
    
        // Lazy initialization of instance field4 - synchronized accessor - Page 333
        private FieldType field2;
        private synchronized FieldType getField2() {
            if (field2 == null)
                field2 = computeFieldValue();
            return field2;
        }
    
        // Lazy initialization holder class idiom for static fields - Page 334
        private static class FieldHolder {
            static final FieldType field = computeFieldValue();
        }
    
        private static FieldType getField() { return FieldHolder.field; }
    
    
        // Double-check idiom for lazy initialization of instance fields - Page 334
        private volatile FieldType field4;
    
        // NOTE: The code for this method in the first printing had a serious error (see errata for details)!
        private FieldType getField4() {
            FieldType result = field4;
            if (result != null)    // First check (no locking)
                return result;
    
            synchronized(this) {
                if (field4 == null) // Second check (with locking)
                    field4 = computeFieldValue();
                return field4;
            }
        }
    
    
    
        // Single-check idiom - can cause repeated initialization! - Page 334
        private volatile FieldType field5;
    
        private FieldType getField5() {
            FieldType result = field5;
            if (result == null)
                field5 = result = computeFieldValue();
            return result;
        }
    
        private static FieldType computeFieldValue() {
            return new FieldType();
        }
    }

     two variants:两个变种 ... 

    还是TMD得看英文原文。。。

    为什么双重检查锁模式需要 volatile ?

    双重检查锁定(Double check locked)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量。这个模式还可以用来创建单例。下面来看一个 Spring 中双重检查锁定的例子。

    DCL.png

    这个例子中需要将配置文件加载到 handlerMappings中,由于读取资源比较耗时,所以将动作放到真正需要 handlerMappings 的时候。我们可以看到 handlerMappings 前面使用了volatile 。有没有想过为什么一定需要 volatile?虽然之前了解了双重检查锁定模式的原理,但是却忽略变量使用了 volatile

    下面我们就来看下这背后的原因。

    错误的延迟初始化例子

    想到延迟初始化一个变量,最简单的例子就是取出变量进行判断。

    errorexample.png

    这个例子在单线程环境交易正常运行,但是在多线程环境就有可能会抛出空指针异常。为了防止这种情况,我们需要使用 synchronized 。这样该方法在多线程环境就是安全的,但是这么做就会导致每次调用该方法获取与释放锁,开销很大。

    深入分析可以得知只有在初始化的变量的需要真正加锁,一旦初始化之后,直接返回对象即可。

    所以我们可以将该方法改造以下的样子。

    DCLerror.png

    这个方法首先判断变量是否被初始化,没有被初始化,再去获取锁。获取锁之后,再次判断变量是否被初始化。第二次判断目的在于有可能其他线程获取过锁,已经初始化改变量。第二次检查还未通过,才会真正初始化变量。

    这个方法检查判定两次,并使用锁,所以形象称为双重检查锁定模式。

    这个方案缩小锁的范围,减少锁的开销,看起来很完美。然而这个方案有一些问题却很容易被忽略。

    new 实例背后的指令

    这个被忽略的问题在于 Cache cache=new Cache() 这行代码并不是一个原子指令。使用 javap -c指令,可以快速查看字节码。

    	// 创建 Cache 对象实例,分配内存
           0: new           #5                  // class com/query/Cache
           // 复制栈顶地址,并再将其压入栈顶
           3: dup
    	// 调用构造器方法,初始化 Cache 对象
           4: invokespecial #6                  // Method "<init>":()V
    	// 存入局部方法变量表
           7: astore_1
    复制代码

    从字节码可以看到创建一个对象实例,可以分为三步:

    1. 分配对象内存
    2. 调用构造器方法,执行初始化
    3. 将对象引用赋值给变量。

    虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。

    Java 语言规规定了线程执行程序时需要遵守 intra-thread semantics。**intra-thread semantics ** 保证重排序不会改变单线程内的程序执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。

    虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。

    image.png

    上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。

    volatile 作用

    正确的双重检查锁定模式需要需要使用 volatilevolatile主要包含两个功能。

    1. 保证可见性。使用 volatile 定义的变量,将会保证对所有线程的可见性。
    2. 禁止指令重排序优化。

    由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。

    注意,volatile禁止指令重排序在 JDK 5 之后才被修复

    使用局部变量优化性能

    重新查看 Spring 中双重检查锁定代码。

    DCL.png

    可以看到方法内部使用局部变量,首先将实例变量值赋值给该局部变量,然后再进行判断。最后内容先写入局部变量,然后再将局部变量赋值给实例变量。

    使用局部变量相对于不使用局部变量,可以提高性能。主要是由于 volatile 变量创建对象时需要禁止指令重排序,这就需要一些额外的操作。

    总结

    对象的创建可能发生指令的重排序,使用 volatile 可以禁止指令的重排序,保证多线程环境内的系统安全。

    帮助文档

    双重检查锁定与延迟初始化
    有关“双重检查锁定失效”的说明

     

    正确的双重检查锁定模式需要需要使用 volatilevolatile主要包含两个功能。

    1. 保证可见性。使用 volatile 定义的变量,将会保证对所有线程的可见性。
    2. 禁止指令重排序优化。
     

    Java中的双重检查锁(double checked locking)

     

    在实现单例模式时,如果未考虑多线程的情况,就容易写出下面的错误代码:

    public class Singleton {
        private static Singleton uniqueSingleton;
    
        private Singleton() {
        }
    
        public Singleton getInstance() {
            if (null == uniqueSingleton) {
                uniqueSingleton = new Singleton();
            }
            return uniqueSingleton;
        }
    }
    

    在多线程的情况下,这样写可能会导致uniqueSingleton有多个实例。比如下面这种情况,考虑有两个线程同时调用getInstance()

    TimeThread AThread B
    T1 检查到uniqueSingleton为空  
    T2   检查到uniqueSingleton为空
    T3   初始化对象A
    T4   返回对象A
    T5 初始化对象B  
    T6 返回对象B  

    可以看到,uniqueSingleton被实例化了两次并且被不同对象持有。完全违背了单例的初衷。

    加锁

    出现这种情况,第一反应就是加锁,如下:

    public class Singleton {
        private static Singleton uniqueSingleton;
    
        private Singleton() {
        }
    
        public synchronized Singleton getInstance() {
            if (null == uniqueSingleton) {
                uniqueSingleton = new Singleton();
            }
            return uniqueSingleton;
        }
    }
    

    这样虽然解决了问题,但是因为用到了synchronized,会导致很大的性能开销,并且加锁其实只需要在第一次初始化的时候用到,之后的调用都没必要再进行加锁。

    双重检查锁

    双重检查锁(double checked locking)是对上述问题的一种优化。先判断对象是否已经被初始化,再决定要不要加锁。

    错误的双重检查锁

    public class Singleton {
        private static Singleton uniqueSingleton;
    
        private Singleton() {
        }
    
        public Singleton getInstance() {
            if (null == uniqueSingleton) {
                synchronized (Singleton.class) {
                    if (null == uniqueSingleton) {
                        uniqueSingleton = new Singleton();   // error
                    }
                }
            }
            return uniqueSingleton;
        }
    }
    

    如果这样写,运行顺序就成了:

    1. 检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回。
    2. 获取锁。
    3. 再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象。

    执行双重检查是因为,如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。

    这样,除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题。

    隐患

    上述写法看似解决了问题,但是有个很大的隐患。实例化对象的那行代码(标记为error的那行),实际上可以分解成以下三个步骤:

    1. 分配内存空间
    2. 初始化对象
    3. 将对象指向刚分配的内存空间

    但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:

    1. 分配内存空间
    2. 将对象指向刚分配的内存空间
    3. 初始化对象

    现在考虑重排序后,两个线程发生了以下调用:

    TimeThread AThread B
    T1 检查到uniqueSingleton为空  
    T2 获取锁  
    T3 再次检查到uniqueSingleton为空  
    T4 uniqueSingleton分配内存空间  
    T5 uniqueSingleton指向内存空间  
    T6   检查到uniqueSingleton不为空
    T7   访问uniqueSingleton(此时对象还未完成初始化)
    T8 初始化uniqueSingleton  

    在这种情况下,T7时刻线程B对uniqueSingleton的访问,访问的是一个初始化未完成的对象。

    正确的双重检查锁

    public class Singleton {
        private volatile static Singleton uniqueSingleton;
    
        private Singleton() {
        }
    
        public Singleton getInstance() {
            if (null == uniqueSingleton) {
                synchronized (Singleton.class) {
                    if (null == uniqueSingleton) {
                        uniqueSingleton = new Singleton();
                    }
                }
            }
            return uniqueSingleton;
        }
    }
    

    为了解决上述问题,需要在uniqueSingleton前加入关键字volatile。使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。

    至此,双重检查锁就可以完美工作了。

    参考资料:

    1. 双重检查锁定模式
    2. 如何在Java中使用双重检查锁实现单例
    3. 双重检查锁定与延迟初始化
     
    分类: Java
     
       
  • 相关阅读:
    Vue生命周期,及父子组件生命周期顺序
    使用jquery制作可视化的组织结构
    用Moon.Orm来做分页数据显示
    bash脚本之代码统计
    CSS选择符总结
    css选择符归类
    APP测试与WEB测试的区别
    使用Jmeter 对APP进行压力测试
    Python基础之数据类型
    App测试要点以及Bug分类
  • 原文地址:https://www.cnblogs.com/cx2016/p/13277046.html
Copyright © 2020-2023  润新知