《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
 
   
原文地址:https://www.cnblogs.com/cx2016/p/13277046.html