目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

线程安全与程序性能

线程安全

 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象时线程安全的。----《并发编程实战》

主要是两个问题

  • 数据争用:多个线程同时修改共享数据,会造成错误数据。(原子性)
  • 竞争条件:操作顺序造成的问题,例如:读取发生在写入之前。(可见性、重排序)

线程安全带来的性能开销
 运行速度、设计成本(增加编码的复杂度)、trade off

线程安全问题分类

  • 运行结果错误:a++ 多线程下出现消失的请求现象
    线程安全问题
  • 活跃性问题:死锁、活锁、饥饿
    死锁
  • 对象发布和初始化的时候的安全问题:由于顺序源于依然会造成错误,比如在写入之前就读取了。
1* 发布:一个对象被声明为public,它就是被发布出去了,或者return 对象,或者方法传参
2* 初始化
3* 逸出:
4	1、方法返回了一个 private 对象(private 的本意是不让外部访问,这样就没人可以访问此对象了)。
5	2、还未完成初始化(构造函数完全执行完毕)就把对象提供给外界,比如:
6		在构造函数中为初始化未完毕就 this 赋值
7		引式逸出——注册监听事件
8		构造函数中运行线程

示例:运行结果错误:a++ 多线程下出现消失的请求现象
 read-modify-write

 1package com.xdclass.couponapp.test.uncaughtexception;
 2
 3import java.util.concurrent.BrokenBarrierException;
 4import java.util.concurrent.CyclicBarrier;
 5import java.util.concurrent.atomic.AtomicInteger;
 6
 7/**
 8 * 发生错误38501
 9 * 
10 * 表面上结果是199999
11 * 真正运行的次数200000
12 * 错误次数1
13 */
14public class MultiThreadsError implements Runnable {
15    static MultiThreadsError instance = new MultiThreadsError();
16    int index = 0;
17    static AtomicInteger realIndex = new AtomicInteger();
18    static AtomicInteger wrongCount = new AtomicInteger();
19    static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
20    static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
21
22    final boolean[] marked = new boolean[10000000];
23
24    @Override
25    public void run() {
26        marked[0] = true;
27        for (int i = 0; i < 100000; i++) {
28            try {
29                cyclicBarrier2.reset();
30                cyclicBarrier1.await();//栅栏1
31            } catch (InterruptedException e) {
32                e.printStackTrace();
33            } catch (BrokenBarrierException e) {
34                e.printStackTrace();
35            }
36            index++;
37            try {
38                cyclicBarrier1.reset();
39                cyclicBarrier2.await();//栅栏2 :让两个线程都完成index++之后再进行判断工作:这样判断过程中就不会出现index被篡改。
40            } catch (InterruptedException e) {
41                e.printStackTrace();
42            } catch (BrokenBarrierException e) {
43                e.printStackTrace();
44            }
45            realIndex.incrementAndGet();
46            synchronized (instance) {//可见性导致了需要修改判断条件(两个线程都看到的是2)  数据中排序  false  ture 是对的   ture ture 是错误
47                // index++  两个线程   2  正确    数组排列    ture false true
48                // index++  两个线程   1  错误    数组排列    ture true  true
49                if (marked[index] && marked[index - 1]) {
50                    System.out.println("发生错误" + index);
51                    wrongCount.incrementAndGet();
52                }
53                marked[index] = true;
54            }
55        }
56    }
57
58    public static void main(String[] args) throws InterruptedException {
59
60        Thread thread1 = new Thread(instance);
61        Thread thread2 = new Thread(instance);
62        thread1.start();
63        thread2.start();
64        thread1.join();
65        thread2.join();
66        System.out.println("表面上结果是" + instance.index);
67        System.out.println("真正运行的次数" + realIndex.get());
68        System.out.println("错误次数" + wrongCount.get());
69    }
70}

查看字节码指令:inCreate()方法的字节码指令片段

  • javac -encoding UTF-8 UnsafeThread.java 编译成。class
  • javap -c UnsafeThread.class 进行反编译,得到相应的字节码指令

 导致线程不安全的根本原因是,在同一个时刻多个线程同时读取到静态域的值,之后进行操作,导致多个线程对静态域进程完操作之后,其结果相当于只进行了一次操作。
究其根本原因是因为 num++ 是一个“非原子性”的操作,查看字节码指令发现该操作被拆分成多个步骤,在多线程并发执行的情况下,因为 CPU 调度、多线程快速的切换,有可能造成多个线程同一时刻都读取了同一个 num 值,之后对此值进行 +1 操作,导致线程安全性问题。
线程不安全问题的起因

1public static void inCreate();
2    Code:
3       0: getstatic     #2                  // Field num:I	#获取指定类的静态域,并将其压入栈顶
4       3: iconst_1						#是int类型1压入栈顶
5       4: iadd						#将栈顶两个int型相加,将结果压入栈顶
6       5: putstatic     #2                  // Field num:I	#为指定静态域赋值
7       8: return

示例:死锁

 1public class DeadLockDemo {
 2    private static Object lockA = new Object();
 3    private static Object lockB = new Object();
 4
 5    public static void main(String[] args) {
 6        new Thread(() -> {
 7            synchronized (lockA) {
 8                System.out.println(Thread.currentThread().getName() + " 已持有 lockA");
 9                System.out.println(Thread.currentThread().getName() + " 尝试获取 lockB");
10                try {
11                    Thread.sleep(20);
12                } catch (InterruptedException e) {
13                    e.printStackTrace();
14                }
15                synchronized (lockB) {
16                    System.out.println(Thread.currentThread().getName() + " 已持有 lockB");
17                }
18            }
19        }).start();
20        new Thread(() -> {
21            synchronized (lockB) {
22                System.out.println(Thread.currentThread().getName() + " 已持有 lockB");
23                System.out.println(Thread.currentThread().getName() + " 尝试获取 lockA");
24                try {
25                    Thread.sleep(20);
26                } catch (InterruptedException e) {
27                    e.printStackTrace();
28                }
29                synchronized (lockA) {
30                    System.out.println(Thread.currentThread().getName() + " 已持有 lockA");
31                }
32            }
33        }).start();
34
35    }
36}

返回 “副本” —— 逸出:方法返回了一个 private 对象(private 的本意是不让外部访问,这样就没人可以访问此对象了)。

 1import com.sun.javafx.geom.Matrix3f;
 2import java.util.HashMap;
 3import java.util.Map;
 4
 5/**
 6 * 描述:     发布逸出
 7 */
 8public class MultiThreadsError3 {
 9
10    private Map<String, String> states;
11
12    public MultiThreadsError3() {
13        states = new HashMap<>();
14        states.put("1", "周一");
15        states.put("2", "周二");
16        states.put("3", "周三");
17        states.put("4", "周四");
18    }
19
20    public Map<String, String> getStates() {
21        return states;
22    }
23
24    public Map<String, String> getStatesImproved() {
25        return new HashMap<>(states);
26    }
27
28    public static void main(String[] args) {
29        MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();
30        Map<String, String> states = multiThreadsError3.getStates();
31//        System.out.println(states.get("1"));
32//        states.remove("1");
33//        System.out.println(states.get("1"));
34
35        System.out.println(multiThreadsError3.getStatesImproved().get("1"));
36        multiThreadsError3.getStatesImproved().remove("1");
37        System.out.println(multiThreadsError3.getStatesImproved().get("1"));
38
39    }
40}

工厂模式——还未完成初始化(构造函数完全执行完毕)就把对象提供给外界,

 1/**
 2 * 描述:     用工厂模式修复刚才的初始化问题
 3 */
 4public class MultiThreadsError7 {
 5
 6    int count;
 7    private EventListener listener;
 8
 9    private MultiThreadsError7(MySource source) {
10        listener = new EventListener() {
11            @Override
12            public void onEvent(MultiThreadsError5.Event e) {
13                System.out.println("\n 我得到的数字是" + count);
14            }
15
16        };
17        for (int i = 0; i < 10000; i++) {
18            System.out.print(i);
19        }
20        count = 100;
21    }
22
23    public static MultiThreadsError7 getInstance(MySource source) {
24        MultiThreadsError7 safeListener = new MultiThreadsError7(source);
25        source.registerListener(safeListener.listener);
26        return safeListener;
27    }
28
29    public static void main(String[] args) {
30        MySource mySource = new MySource();
31        new Thread(new Runnable() {
32            @Override
33            public void run() {
34                try {
35                    Thread.sleep(10);
36                } catch (InterruptedException e) {
37                    e.printStackTrace();
38                }
39                mySource.eventCome(new MultiThreadsError5.Event() {
40                });
41            }
42        }).start();
43        MultiThreadsError7 multiThreadsError7 = new MultiThreadsError7(mySource);
44    }
45
46    static class MySource {
47
48        private EventListener listener;
49
50        void registerListener(EventListener eventListener) {
51            this.listener = eventListener;
52        }
53
54        void eventCome(MultiThreadsError5.Event e) {
55            if (listener != null) {
56                listener.onEvent(e);
57            } else {
58                System.out.println("还未初始化完毕");
59            }
60        }
61
62    }
63
64    interface EventListener {
65
66        void onEvent(MultiThreadsError5.Event e);
67    }
68
69    interface Event {
70
71    }
72}

需要考虑线程安全的情况

  • 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等…
  • 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check-then-act
  • 不同的数据之间存在捆绑关系的时候 (必须进行原子操作)
  • 我们使用其他类的时候,如果对方没有声明自己是线程安全的,应该慎用,或做额外的处理。

多线带来的性能问题

调度:上下文切换

  • 保存现场:耗费 CPU 很多时钟周期
  • 缓存开销:内存失效(CPU 有最小执行时间,避免上下文切换过于频繁)
  • 何时会导致密集的上下文切换:抢锁、IO(等待 IO)

协作:内存同步

  • JMM 同步也会带来性能开销

volatile 关键字及其使用场景

volatile 关键字的功能

  • 能且仅能修饰变量。
  • 保证该变量的可见性(A、B 两个线程同时读取 volatile 关键字修饰的对象,A 读取之后,修改了变量的值,修改后的值,对 B 线程来说,是可见),volatile 关键字仅仅保证可见性,并不保证原子性
  • 禁止指令重排序。

volatile 关键字的常见使用场景

  • 作为线程开关。
  • 单例,修饰对象实例,禁止指令重排序

单例设计模式与线程安全

饿汉式(本身线程安全)
 在类加载的时候,就已经进行实例化,无论之后用不用到。如果该类比较占内存,之后又没用到,就白白浪费了资源。

 1/**
 2 * 饿汉式单例
 3 * 在类加载的时候,就已经进行实例化,无论之后用不用到。
 4 * 如果该类比较占内存,之后又没用到,就白白浪费了资源。
 5 */
 6public class HungerSingleton {
 7
 8    private static HungerSingleton ourInstance = new HungerSingleton();
 9
10    public static HungerSingleton getInstance() {
11        return ourInstance;
12    }
13
14    //无参构造方法私有化
15    private HungerSingleton() {
16    }
17
18    public static void main(String[] args) {
19        for (int i = 0; i < 10; i++) {
20            new Thread(()->{
21                System.out.println(HungerSingleton.getInstance());
22            }).start();
23        }
24    }
25}

懒汉式(最简单的写法是非线程安全的)
 在需要的时候再实例化。

 1/**
 2 * 懒汉式单例
 3 * 在需要的时候再实例化
 4 */
 5
 6public class LazySingleton {
 7
 8    //使用volatile关键字禁止JVM的指重排序:否则会导致线程不安全
 9    private static volatile LazySingleton lazySingleton = null;
10
11    //无参构造方法私有化
12    private LazySingleton() {
13    }
14
15    public static LazySingleton getInstance() {
16        //判断实例是否为空,为空则实例化
17        if (null == lazySingleton) { //第一次判断
18
19            //模拟实例化消耗时间
20            try {
21                Thread.sleep(1000L);
22            } catch (InterruptedException e) {
23                e.printStackTrace();
24            }
25
26            synchronized (LazySingleton.class) {
27                if (null == lazySingleton) { //第二次判断,避免再次创建新的实例
28                    lazySingleton = new LazySingleton();
29                }
30            }
31            
32        }
33        //否则直接返回
34        return lazySingleton;
35    }
36
37    public static void main(String[] args) {
38        for (int i = 0; i < 10; i++) {
39            new Thread(() -> {
40                System.out.println(LazySingleton.getInstance());
41            }).start();
42        }
43    }
44}

如何避免线程安全性问题

线程安全性问题成因

  • 多线程环境
  • 多个线程操作同一共享资源
  • 对该共享资源进行了非原子性操作

如何避免
 打破成因中三点任意一点即可。

  • 多线程环境:将多线程改单线程(必要的代码,加锁访问),比如:synchronized 、JDK 提供的锁、自己实现属于自己的锁等…
  • 多个线程操作同一共享资源:不共享资源(ThreadLocal、不共享、操作无状态化、不可变) 。
  • 对该共享资源进行了非原子性操作:将非原子性操作改成原子性操作(加锁、使用 JDK 自带的原子性操作的类、JUC 提供的相应的并发工具类)。

作者:Soulboy