avatar

目录
并发编程与线程安全(3):安全发布对象&线程安全策略

[TOC]

第5章 安全发布对象

5-1 发布与溢出

  • 发布对象:使一个对象能够被当前范围之外的代码所使用
  • 对象溢出:一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见

不安全的发布

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.machine.concurrency.example.publish;

import com.machine.concurrency.annotations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;

/**
* 不安全的发布
*/
@Slf4j
@NotThreadSafe
public class UnsafePublish {
private String[] states = {"a","b","c"};

/**
* 通过public发布级别发布了类的域,在类的外部,任何线程都可以访问这个域
* 这样是不安全的,因为我们无法检查其他线程是否会修改这个域导致了错误
*/
public String[] getStates() {
return states;
}

public static void main(String[] args) {
UnsafePublish unsafePublish = new UnsafePublish();
log.info("{}", Arrays.toString(unsafePublish.getStates()));

//可以修改值
unsafePublish.getStates()[0] = "d";
log.info("{}", Arrays.toString(unsafePublish.getStates()));

}
}

对象溢出

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.machine.concurrency.example.publish;

import com.machine.concurrency.annotations.NotRecommend;
import com.machine.concurrency.annotations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;

/**
* 对象溢出
* 在对象构造完成之前,不可以将其发布
*/
@Slf4j
@NotThreadSafe
@NotRecommend
public class Escape {

private int thisCannBeEscape = 0;

public Escape(){
new InnerClass();
}

/**
* 包含了对封装实例的隐藏和引用,这样在对象没有被正确构造完成之前就会被发布,由此导致不安全的因素在里面
* 1.导致this引用在构造期间溢出的错误,他是在构造函数构造过程中启动了一个线程,造成this引用的溢出
* 新线程只是在对象构造完毕之前就已经看到他了,所以如果要在构造函数中创建线程,那么不要启动它,
* 而是用一个专有的start,或是其他的方式统一启动线程
* 使用工厂方法和私有构造函数来完成对象创建和监听器的注册来避免不正确的发布
*/
private class InnerClass{
public InnerClass(){
log.info("{}",Escape.this.thisCannBeEscape);
}
}

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

5-2 安全发布对象的4种方法

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到volatile类型域或者AtomicReference对象中
  • 将对象的引用保存到某个正确构造对象的final类型域中
  • 将对象的引用保存到一个由锁保护的域中

单例模式(懒汉、饿汉、枚举)

以下代码讲述一个懒汉模式逐步优化的过程

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.machine.concurrency.example.singleton;


import com.machine.concurrency.annotations.NotThreadSafe;

/**
* 懒汉模式:单例实例在第一次使用时进行创建
* 缺点:线程不安全
*/
@NotThreadSafe
public class SingletonExample1 {

// 私有构造函数:外部不能通过new方法多次创建对象
private SingletonExample1() {

}

// 单例对象
private static SingletonExample1 instance = null;

// 静态的工厂方法
public static SingletonExample1 getInstance() {
//当2个线程同时执行到这一步,2个线程都会执行new,
// 违背了单例模式,线程不安全
if (instance == null) {

instance = new SingletonExample1();
}
return instance;
}
}
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.machine.concurrency.example.singleton;


import com.machine.concurrency.annotations.NotRecommend;
import com.machine.concurrency.annotations.ThreadSafe;

/**
* 懒汉模式:使用synchronized
* 缺点:synchronized使同一时间只能一个线程进入getInstance方法,并发性能差
*
*/
@ThreadSafe
@NotRecommend
public class SingletonExample3 {


private SingletonExample3() {

}

private static SingletonExample3 instance = null;

// 使用synchronized
public static synchronized SingletonExample3 getInstance() {
if (instance == null) {
instance = new SingletonExample3();
}
return instance;
}
}
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.machine.concurrency.example.singleton;


import com.machine.concurrency.annotations.NotThreadSafe;

/**
* 懒汉模式 -》 双重同步锁单例模式
*/
@NotThreadSafe
public class SingletonExample4 {


private SingletonExample4() {

}

// new SingletonExample4()时发生了什么
// 1、memory = allocate() 分配对象的内存空间
// 2、ctorInstance() 初始化对象
// 3、instance = memory 设置instance指向刚分配的内存
// 如果不指令重排序,此方法线程安全,而且并发性能好

// 但是...JVM和cpu优化,发生了指令重排
// 1、memory = allocate() 分配对象的内存空间
// 3、instance = memory 设置instance指向刚分配的内存
// 2、ctorInstance() 初始化对象


private static SingletonExample4 instance = null;

// 静态的工厂方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 双重检测机制 // B
synchronized (SingletonExample4.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample4(); // A - 3
}
}
}
//重拍后,可能返回未初始化的对象
return instance;
}
}

懒汉模式 最终解决方案

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.machine.concurrency.example.singleton;


import com.machine.concurrency.annotations.ThreadSafe;

/**
* 懒汉模式 -》 双重同步锁单例模式
* 双重检测+同步锁+volatile
*/
@ThreadSafe
public class SingletonExample5 {


private SingletonExample5() {

}

// 1、memory = allocate() 分配对象的内存空间
// 2、ctorInstance() 初始化对象
// 3、instance = memory 设置instance指向刚分配的内存

// 单例对象 volatile + 双重检测机制 -> 禁止指令重排
private volatile static SingletonExample5 instance = null;

// 静态的工厂方法
public static SingletonExample5 getInstance() {
if (instance == null) { // 双重检测机制 // B
synchronized (SingletonExample5.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample5(); // A - 3
}
}
}
return instance;
}
}

以下代码讲述一个饿汉模式逐步优化的过程

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.machine.concurrency.example.singleton;

import com.machine.concurrency.annotations.ThreadSafe;

/**
* 饿汉模式:单例实例在类装载时进行创建
* 缺点:1.如果创建过程中进行很多的运算,会导致类加载的时候特别的慢
* 2.如果创建出来的实例要很久以后才被调用,那么会导致资源的浪费
*/
@ThreadSafe
public class SingletonExample2 {


private SingletonExample2() {

}

//装载时创建
private static SingletonExample2 instance = new SingletonExample2();


public static SingletonExample2 getInstance() {
return instance;
}
}
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.machine.concurrency.example.singleton;


import com.machine.concurrency.annotations.ThreadSafe;

/**
* 饿汉模式: 通过静态代码块创建
*/
@ThreadSafe
public class SingletonExample6 {


private SingletonExample6() {

}

//1和2的顺序不能反,不然会造成空指针异常

//1
private static SingletonExample6 instance = null;
//2
static {
instance = new SingletonExample6();
}


public static SingletonExample6 getInstance() {
return instance;
}

public static void main(String[] args) {
System.out.println(getInstance().hashCode());
System.out.println(getInstance().hashCode());
}
}

枚举模式优化过程:最推荐

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.machine.concurrency.example.singleton;

import com.machine.concurrency.annotations.Recommend;
import com.machine.concurrency.annotations.ThreadSafe;

/**
* 枚举模式:最安全
*/
@ThreadSafe
@Recommend //没有饿汉的并发性能问题,也没有懒汉需要额外代码来维护线程安全
public class SingletonExample7 {

// 私有构造函数
private SingletonExample7() {

}

public static SingletonExample7 getInstance() {
return Singleton.INSTANCE.getInstance();
}

//枚举类
private enum Singleton {
INSTANCE;

private SingletonExample7 singleton;

// JVM保证这个方法绝对只调用一次
Singleton() {
singleton = new SingletonExample7();
}

public SingletonExample7 getInstance() {
return singleton;
}
}
}
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.machine.concurrency.example.singleton;

import com.machine.concurrency.annotations.Recommend;
import com.machine.concurrency.annotations.ThreadSafe;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

/**
* 枚举类型的单例,最安全最简单
*/
@Slf4j
@ThreadSafe
@Recommend
public class SingletonExample8 {
/**
* 私有构造函数
*/
private SingletonExample8(){
// 可能会进行很多≠操作,很多运算
}

/**
* 静态工厂方法
* @return
*/
public static SingletonExample8 getInstance(){
return Singleton.INSTANCE.getSingletonExample8();
}

/**
* 由枚举类创建单例对象
*/
@Getter
private enum Singleton{
INSTANCE;

private SingletonExample8 singletonExample8;

//JVM保证只被执行一次
Singleton(){
singletonExample8 = new SingletonExample8();
System.out.println("233");
}

}

public static void main(String[] args) {

SingletonExample8.getInstance();
SingletonExample8.getInstance();
}
}

第6章 线程安全策略

6-1 不可变对象

不可变对象需要满足的条件

  • 对象创建以后其状态就不能修改
  • 对象所有域都是final类型
  • 对象是正确创建的(在对象创建期间,this引用没有逸出)

创建不可变对象的方式(参考String类型)

  • 将类声明成final类型,使其不可以被继承
  • 将所有的成员设置成私有的,使其他的类和对象不能直接访问这些成员
  • 对变量不提供set方法
  • 将所有可变的成员声明为final,这样只能对他们赋值一次
  • 通过构造器初始化所有成员,进行深度拷贝
  • 在get方法中,不直接返回对象本身,而是克隆对象,返回对象的拷贝

final关键字:类、方法、变量

  • 修饰类:不能被继承(final类中的所有方法都会被隐式的声明为final方法)
  • 修饰方法:1、锁定方法不被继承类修改;2、提升效率(private方法被隐式修饰为final方法)
  • 修饰变量:基本数据类型变量(初始化之后不能修改)、引用类型变量(初始化之后不能再修改其引用,但原对象内部的值可以被改变)
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.machine.concurrency.example.immutable;

import com.google.common.collect.Maps;
import com.machine.concurrency.annotations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;

@Slf4j
@NotThreadSafe
public class ImmutableExample1 {

private final static Integer a = 1;
private final static String b = "2";
private final static Map<Integer, Integer> map = Maps.newHashMap();

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

public static void main(String[] args) {
// a = 2;
// b = "3";
// map = Maps.newHashMap();
map.put(1, 3);
log.info("{}", map.get(1));
}

private void test(final int a) {
// a = 1;
}
}

其他的不可变对象的创建

  • Collection.unmodifiableXXX:Clollection、List、Set、Map…
  • Guava:ImmutableXXX:Collection、List、Set、Map…
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.machine.concurrency.example.immutable;

import com.google.common.collect.Maps;
import com.machine.concurrency.annotations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.Map;

/**
* Collections.unmodifiableMap 创建完以后不允许被修改源码
*/
@Slf4j
@ThreadSafe
public class ImmutableExample2 {

private static Map<Integer, Integer> map = Maps.newHashMap();

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

//内部原理:初始化的时候将传进来的map赋值给一个final类型的map,然后将所有会修改的方法直接抛出UnsupportedOperationException异常
map = Collections.unmodifiableMap(map);
}

public static void main(String[] args) {
map.put(1, 3);

//会报错
log.info("{}", map.get(1));
}
}
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.machine.concurrency.example.immutable;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.machine.concurrency.annotations.ThreadSafe;

/**
* Guava:Immutablexxx
*/
@ThreadSafe
public class ImmutableExample3 {

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) {

//put方法执行都会跑异常
System.out.println(map2.get(3));
}
}

6-2 线程封闭

把对象封装到一个线程里,只有这个线程能看到这个对象(所以不存在并发下的线程安全,因为只有一个线程操作该对象)

实现线程封闭

  • Ad-hoc 线程封闭:程序控制实现,最糟糕,忽略

  • 堆栈封闭:局部变量,无并发问题(在方法内部定义局部变量)

  • ThreadLocal 线程封闭:特别好的封闭方法

ThreadLocal:主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

参考:https://www.jianshu.com/p/98b68c97df9b

6-3 线程不安全类与写法

常用的线程不安全类

  1. StringBuilder 线程不安全,StringBuffer线程安全

    原因:StringBuffer几乎所有的方法都加了synchronized关键字

    为什么需要StringBuilder:由于StringBuffer 加了 synchronized 所以性能会下降很多,所以在堆栈封闭等线程安全的环境下应该首先选用StringBuilder

  1. SimpleDateFormat
    SimpleDateFormat 在多线程共享使用的时候回抛出转换异常,应该才用堆栈封闭在每次调用方法的时候在方法里创建一个SimpleDateFormat
    另一种方式是使用joda-time的DateTimeFormatter(推荐使用)
java
1
2
3
private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");

DateTime.parse("20180320",dateTimeFormatter).toDate();
  1. ArrayList,HashMap,HashSet等Collections

    ArrayList,HashMap,HashSet都不安全

线程不安全写法

先检查再执行

java
1
2
3
4
// 非原子性,需要加个锁保证原子性
if(condition(a)){
handle(a);
}

6-4 同步容器

  1. 同一接口,不同实现的线程安全类

    • ArrayList -> Vector、Stack
    • HashMap -> HashTable(key、Value不能为null)

    vector的所有方法都是有synchronized关键字保护的
    stack继承了vector,并且提供了栈操作(先进后出)
    hashtable也是由synchronized关键字保护

  2. Collections.synchronizedXXX (list,set,map)

同步容器并不一定线程安全

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.machine.concurrency.example.syncContainer;

import com.machine.concurrency.annotations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.List;
import java.util.Vector;

/**
* 并发测试
* 同步容器不一定线程安全
* @author gaowenfeng
*/
@Slf4j
@NotThreadSafe
public class VectorExample {

/** 请求总数 */
public static int clientTotal = 5000;
/** 同时并发执行的线程数 */
public static int threadTotal = 50;

public static List<Integer> list = new Vector<>();

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
list.add(i);
}
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
list.remove(i);
}
});

Thread thread2 = new Thread(() -> {
// thread2想get i=9的元素的时候,thread1将i=9的元素remove了,导致数组越界
for (int i = 0; i < 10; i++) {
list.get(i);
}
});

thread1.start();
thread2.start();
}
}

在foreach或迭代器遍历的过程中不要做删除操作,应该先标记,然后最后再统一删除

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.machine.concurrency.example.syncContainer;

import java.util.Iterator;
import java.util.Vector;

public class VectorExample2 {
// java.util.ConcurrentModificationException
// 在遍历的同时进行了删除的操作,导致抛出了并发修改的异常
private static void test1(Vector<Integer> v1) { // foreach
for(Integer i : v1) {
if (i.equals(3)) {
v1.remove(i);
}
}
}

// java.util.ConcurrentModificationException
private static void test2(Vector<Integer> v1) { // iterator
Iterator<Integer> iterator = v1.iterator();
while (iterator.hasNext()) {
Integer i = iterator.next();
if (i.equals(3)) {
v1.remove(i);
}
}
}

// success
private static void test3(Vector<Integer> v1) { // for
for (int i = 0; i < v1.size(); i++) {
if (v1.get(i).equals(3)) {
v1.remove(i);
}
}
}

public static void main(String[] args) {

Vector<Integer> vector = new Vector<>();
vector.add(1);
vector.add(2);
vector.add(3);
test1(vector);
}

}

同步容器也存在很多安全问题,因此java提供了并发容器J.U.C

6-5 并发容器

  • HashSet、TreeSet -> CopyOnWriteArraySet、ConcurrentSkipListSet

  • HashMap、TreeMap -> ConcurrentHashMap、ConcurrentSkipListMap

J.U.C体系:tools、locks、aotmic、collections、executor

6-6 安全共享对象策略-总结

  • 线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改

  • 共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它

  • 线程安全对象:一个线程安全的对象或容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它

  • 被守护对象:被守护对象只能通过获取特定的锁来访问

文章作者: Machine
文章链接: https://machine4869.gitee.io/2018/10/27/20181027141018462/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 哑舍
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论