Java并发编程03-对象的安全发布与共享策略

一、安全发布

1. 对象发布与逸出

发布对象:使对象能够在当前作用域之外的代码中使用

逸出:某个不应该发布的对象被发布

(1)变量逸出原有作用域

程序示例:

public class N00_Test {
    private String[] strs = {"1", "2", "3"};

    public String[] getStrs() {
        return strs;
    }

    public static void main(String[] args) {
        N00_Test m1 = new N00_Test();
        System.out.println(Arrays.toString(m1.getStrs()));
        m1.getStrs()[0] = "4";
        System.out.println(Arrays.toString(m1.getStrs()));
    }
}

运行结果:

[1, 2, 3]
[4, 2, 3]

通过访问对象的公用方法来获取私有变量,并修改私有变量的值,则导致了变量逸出作用域。

但是我们有时候就是想暴露私有成员变量出来让我们去修改,所以提供 getter 是没毛病的,不过要注意的是我们在编码的时候需要对仅仅需要发布的对象进行发布。

(2)对象逸出

当一个对象还没构造完成,就使它被其他线程所见。

以下代码发布了一个未完成构造的对象到另一个对象中:

public class N00_Test {
    public N00_Test() {
        System.out.println(N00_Test.this);
        System.out.println(Thread.currentThread());
        Thread t = new Thread(InnerClass::new);
        t.start();
    }

    class InnerClass {
        public InnerClass() {
            System.out.println(N00_Test.this);
            System.out.println(Thread.currentThread());
        }
    }

    public static void main(String[] args) {
        new N00_Test();
    }
}

运行结果:

cn.zenoyang.juc.thread.N00_Test@1b6d3586
Thread[main,5,main]
cn.zenoyang.juc.thread.N00_Test@1b6d3586
Thread[Thread-0,5,main]

this 引用被线程 t 共享,故线程 t 的发布将导致 N00_Test 对象的发布,由于 N00_Test 对象被发布时可能还未构造完成,这将导致 N00_Test 对象逸出(在构造函数中创建线程是没有问题的,但是不要在构造函数执行完之前启动线程)。

可通过私有构造函数 + 工厂模式解决:

public class N00_Test {
    private Thread t;

    private N00_Test() {
        System.out.println(N00_Test.this);
        System.out.println(Thread.currentThread());
        this.t = new Thread(InnerClass::new);
    }

    class InnerClass {
        public InnerClass() {
            System.out.println(N00_Test.this);
            System.out.println(Thread.currentThread());
        }
    }

    public static N00_Test getInstance() {
        N00_Test main = new N00_Test();
        main.t.start();
        return main;
    }

    public static void main(String[] args) {
        getInstance();
    }
}

2. 安全发布策略

安全地发布对象是保证对象在发布之前一定是初始化完成的。所以我们要做的控制初始化过程,首先是使构造函数私有化,然后再使用不同的方式来初始化对象。

(1)将对象的引用在静态初始化函数中初始化

public class N00_Test {
    private static N00_Test instance = new N00_Test();

    private N00_Test() {
    }

    public static N00_Test getInstance() {
        return instance;
    }
}

通过在静态变量后 new 出实例或者在 static 代码块中初始化,通过 JVM 的单线程类加载机制来保证该对象在其它对象访问之前被初始化。

(2)将对象的初始化手动同步处理

public class N00_Test {
    private static N00_Test instance;

    private N00_Test() { }

    public synchronized N00_Test getInstance() {
        if (instance == null) {
            instance = new N00_Test();
        }
        return instance;
    }
}

(3)使用 volatile 修饰变量

public class N00_Test {
    private static volatile N00_Test instance;

    private N00_Test() { }

    public N00_Test getInstance() {
        if (instance == null) {
            synchronized (N00_Test.class) {
                if (instance == null) {
                    instance = new N00_Test();
                }
            }
        }
        return instance;
    }
}

为了实现懒汉模式 + 线程安全,需要使用 DCL 双重检验锁来实现,那么 instance = new N00_Test()就涉及到了同步问题,所以需要使用 volatile 来修饰。

安全共享

1. 安全共享

安全共享是在多线程访问共享对象时,让对象的行为保持逻辑正常。

2. 安全共享策略

(1)线程封闭

将一个对象封锁在线程内部,那么其它线程无法访问,就不会涉及到共享的问题了。有以下方式:

  • Ad-hoc 线程封闭:维护线程完封闭完全由编程承担,不推荐
  • 局部变量封闭:局部变量的固有属性就是封闭在执行线程内,无法被外界引用,所以我们应该尽量使用局部变量,可以减少逸出的发生。
  • 使用 ThreadLocal:ThreadLocal 是一个提供线程私有变量的工具类。线程维护自己的私有变量,就不会发生共享问题。

(2)final

使对象不可变,那么一定是多线程安全的,可以放心的被用来数据共享。

但 final 仅保证引用不被修改,不能保证内容不被修改。程序示例:

public class N00_Test {
    private final static Integer a = 1;
    private final static String b = "2";
    private final static Map<Integer, Integer> map = new HashMap<>();

    static {
        map.put(1, 2);
        map.put(3, 4);
        map.put(5, 6);
    }

    public static void main(String[] args) {
        // 引用不可被修改
//        N00_Test.a = 2;
//        N00_Test.b = "3";
//        N00_Test.map = new HashMap<>();

        // 内容可以被修改
        N00_Test.map.put(1, 3);
        System.out.println(N00_Test.map.get(1));
    }
}

Collections.unmodifiableMap(map) 则可以将可修改的 map 转换为不可修改的 map,或者使用使用 com.google.guava中的ImmutableXXX 集合类可以的禁止对集合修改的操作:

public class N00_Test {
    private final static ImmutableList<Integer> list = ImmutableList.of(1, 2, 3);

    private final static ImmutableSet set = ImmutableSet.copyOf(list);

    private final static ImmutableMap<Integer, Integer> map = ImmutableMap.of(1, 2, 3, 4);

    private final static ImmutableMap<Integer, Integer> map2
            = ImmutableMap
            .<Integer, Integer>builder()
            .put(1, 2).put(3, 4).put(5, 6).build();


    public static void main(String[] args) {
        System.out.println(list.asList());
        System.out.println(set.asList());
        System.out.println(map.toString());
        System.out.println(map2.get(3));
    }
}

(3)使用线程安全的类

  • StringBuilder -> StringBuffer
  • SimpleDateFormat -> JodaTime
  • ArrayList -> Vector, Stack, CopyOnWriteArrayList
  • HashSet -> Collections.synchronizedSet(new HashSet()), CopyOnWriteArraySet
  • TreeSet -> Collections.synchronizedSortedSet(new TreeSet()), ConcurrentSkipListSet
  • HashMap -> HashTable, ConcurrentHashMap, Collections.synchronizedMap(new HashMap())
  • TreeMap -> ConcurrentSkipListMap, Collections.synchronizedSortedMap(new TreeMap())

参考:

posted @ 2020-03-28 00:23  吹不散的流云  阅读(116)  评论(0编辑  收藏  举报