开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
学习MOOC视频记录的笔记
多线程是把双刃剑:可能导致安全、性能问题
一共有哪几类线程安全问题?
哪些场景需要额外注意线程安全问题?
什么是多线程带来的上下文切换?
1.线程安全
1.1 什么是线程安全?
《Java Concurrency In Practice》的作者Brian Goetz对“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
简单定义:
不管业务中遇到怎样的多个线程访问某对象或某方法的情况,而在编程这个业务逻辑的时候,都不需要额外做任何额外的处理(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全。
线程不安全:get 同时 set、额外同步
全都线程安全?:运行速度、设计成本、trade off
完全不用于多线程:不过度设计
主要是两个问题:
- 数据争用:数据读写由于同时写,会造成错误数据。
- 竞争条件:即使不是同时写造成的错误数据,由于顺序原因依然会造成错误,例如在写入前就读取了。
1.2 什么情况下会出现线程安全问题,怎么避免?
运行结果错误(a++多线程下出现消失的请求现象,属于read-modify-write)
- 原子性
- 找到a++出错的地方
死锁等活跃性问题(包括死锁、活锁、饥饿)
对象发布和初始化的时候的安全问题
什么是发布
- 声明为public
- return一个对象
- 把对作为参数传递到其他类的方法中
什么是逸出
方法返回一个private.对象(private的本意是不让外部访问)
还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界
- 在构造函数中未初始化完毕就this赋值
- 隐式逸出一注册监听事件
- 构造函数中运行线程
如何解决逸出
- 副本
- 工厂模式
总结归纳:各种需要考虑线程安全的情况
- 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
- 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:
- read-modify-write操作:一个线程读取了一个共享数据,并在此基础上更新该数据。该例子在上面的index-++已经展示过了。
- check-then-act操作:一个线程读取了一个共享数据,并在此基础上决定其下一个的操作
- 不同的数据之间存在捆绑关系的时候
- IP和端口号
- 我们使用其他类的时候,如果对方没有声明自己是线程安全的,那么大概率会存在并发问题
- 比如
HashMap没有声明自己是并发安全的,所以我们并发调用HashMap的时候会出错
运行结果错误:a++多线程下出现消失的请求现象
/**
* 第一种:运行结果出错
* 演示计数不准确(减少),找出具体出错的位置。
*/
public class MultiThreadsError implements Runnable {
int index = 0;
static MultiThreadsError instance = new MultiThreadsError();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上结果是: " + instance.index);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
}
}
}
运行结果:
表面上结果是: 12121
i++看似是一条语句,其实在汇编的层面上涉及到三个步骤:取值,加1,写回;这里线程2读到了线程1还没有写回的数据,最终两次+1其实只执行了一次。
public class MultiThreadsError implements Runnable {
int index = 0;
static MultiThreadsError instance = new MultiThreadsError();
static AtomicInteger realIndex = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();
final boolean[] marked = new boolean[10000000];
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上结果是: " + instance.index);
System.out.println("真正运行的次数: " + realIndex.get());
System.out.println("错误次数: " + wrongCount.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
realIndex.incrementAndGet();
if (marked[index]) {
System.out.println("发生错误" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
运行结果:
表面上结果是: 19947
真正运行的次数: 20000
错误次数: 61
public class MultiThreadsError implements Runnable {
int index = 0;
static MultiThreadsError instance = new MultiThreadsError();
static AtomicInteger realIndex = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();
final boolean[] marked = new boolean[10000000];
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上结果是: " + instance.index);
System.out.println("真正运行的次数: " + realIndex.get());
System.out.println("错误次数: " + wrongCount.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
realIndex.incrementAndGet();
synchronized (instance) {
if (marked[index]) {
System.out.println("发生错误" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
}
运行结果:
表面上结果是: 19999
真正运行的次数: 20000
错误次数: 745
结果说明:
- 对
marked数组的操作不加同步的话,线程1先到if (marked[index])语句处,发现这个位置之前没有被别人写过,跳过判断内部的语句,在使用marked[index] = true标记已经被写过之前,线程2先进行if (marked[index])的判断,这个时候由于线程1还未标记,因此线程2也不会发现错误。 - 对
marked数组的操作加了synchronized之后还是有问题。两个线程执行完前面两个代码,到synchronized(instance)处拿锁,线程1拿到锁,并且此时index = 1,线程1将marked数组的第一位置为1,第二个线程进来之后,CPU切换回去,继续让线程1执行,线程1执行index++将index更新了,此时index的值变成了2,线程2继续执行判断的是marked[index]即2位置上是否有冲突。
public class MultiThreadsError implements Runnable {
int index = 0;
static MultiThreadsError instance = new MultiThreadsError();
static AtomicInteger realIndex = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
final boolean[] marked = new boolean[10000000];
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上结果是: " + instance.index);
System.out.println("真正运行的次数: " + realIndex.get());
System.out.println("错误次数: " + wrongCount.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
// 需要两个线程都执行完await之后才继续往下执行
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
index++;
// 等待两个线程都将 index+1 之后再判断
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
realIndex.incrementAndGet();
synchronized (instance) {
if (marked[index]) {
System.out.println("发生错误" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
}
运行结果:
表面上结果是: 19999
真正运行的次数: 20000
错误次数: 10000
可见还是有问题。
public class MultiThreadsError implements Runnable {
int index = 0;
static MultiThreadsError instance = new MultiThreadsError();
static AtomicInteger realIndex = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
final boolean[] marked = new boolean[10000000];
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上结果是: " + instance.index);
System.out.println("真正运行的次数: " + realIndex.get());
System.out.println("错误次数: " + wrongCount.get());
}
@Override
public void run() {
marked[0] = true;
for (int i = 0; i < 100000; i++) {
// 需要两个线程都执行完await之后才继续往下执行
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
index++;
// 等待两个线程都将 index+1 之后再判断
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
realIndex.incrementAndGet();
synchronized (instance) {
// 两个线程在这里看到的index是一样的,都是偶数
if (marked[index] && marked[index - 1]) {
System.out.println("发生错误" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
}
结果:
发生错误141343
表面上结果是: 199999
真正运行的次数: 200000
错误次数: 1
出现 false, true, false, true, … 这种现象的原因是大部分情况下是两个线程分别执行了 index++ 之后 index 的值变加了2,这种属于正常的情况,因此需要修改判断的条件。
活跃性问题:死锁、活锁、饥饿
public class MultiThreadError implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
MultiThreadError r1 = new MultiThreadError();
MultiThreadError r2 = new MultiThreadError();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("1");
}
}
}
}
}
对象发布和初始化的时候的安全问题
什么是发布?
让这个对象让超过类范围之内的其他位置使用。public修饰的对象;方法的return是一个对象,那么调用这个方法的类都获得了这个对象;将这个类作为参数。
什么是逸出?
也即发布到了不该发布的地方,分为以下几种情况:
- 方法返回一个
private对象(private的本意是不让外部访问) - 还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如:
- 在构造函数中未初始化完毕就this赋值
- 隐式逸出—注册监听事件
- 构造函数中运行线程
public class MultiThreadsError3 {
private Map<String, String> states;
public MultiThreadsError3() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
public Map<String, String> getStates() {
return states;
}
public static void main(String[] args) {
MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();
Map<String, String> states = multiThreadsError3.getStates();
System.out.println(states.get("1"));
states.remove("1");
System.out.println(states.get("1"));
}
}
states 被发送出去了,在外面被修改了,导致无法正常获取 key=1 对应的值。
如何解决:
- 返回副本
- 工厂模式
各种需要考虑线程安全的情况
- 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
- 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发 问题:read-modify-write、check-then-act
- 不同的数据之间存在捆绑关系的时候
- 我们使用其他类的时候,如果对方没有声明自己是线程安全的
2.性能问题有哪些体现、什么是性能问题
- 服务响应慢、吞吐量低、资源消耗(例如内存)过高等
- 虽然不是结果错误,但依然危害巨大
- 引入多线程不能本末倒置
3.为什么多线程会带来性能问题
-
调度:上下文切换
-
什么是上下文?保存现场 【context,当可运行的线程数量超过CPU的数量,那么操作系统就要调度线程,以便让每个线程都有机会运行;一次上下文切换会消耗5000~10000个CPU的时钟周期,大约几微秒;与寄存器相关】
上下文切换可以认为是内核(操作系统的核心)在CPU上对于进程(包括线程)进行以下的活动:(1)挂起一个进程,将这个进程在CPU中的状态(上下文)存储于内存中的某处,(2)在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复,(3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。
-
缓存开销:缓存失效 【程序有很大概率会访问之前访问过的数据,CPU为了加快执行的速度,会将不同的数据缓存在CPU里面,这样下次再次使用的时候很快就可以使用到了,但是一旦进行了上下文切换,CPU即将执行不同线程的不同代码,原来的缓存就失去了价值,CPU就需要重新进行缓存,这导致线程在被调度之后一开始的启动速度很慢,这是因为之前的缓存都失效了】
CPU重新缓存
-
何时会导致密集的上下文切换:抢锁、IO 【频繁的线程阻塞】
频繁地竞争锁,或者由于IO读写等原因导致频繁阻塞
-
-
协作:内存同步
-
编译器和CPU会帮助我们将程序进行优化,指令重排序,锁优化,让缓存失效了,无法使用自己的内存只能使用主存
Java内存模型
为了数据的正确性,同步手段往往会使用禁止编译器优化、使CPU内的缓存失效
-
4.常见面试问题
- 你知道有哪些线程不安全的情况?
- 运行结果错误(a++多线程下出现消失的请求现象,属于read-modify-write)
- 死锁等活跃性问题(包括死锁、活锁、饥饿)
- 对象发布和初始化的时候的安全问题
- 平时哪些情况下需要额外注意线程安全问题?
- 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
- 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题
- 不同的数据之间存在捆绑关系的时候
- 我们使用其他类的时候,如果对方没有声明自己是线程安全的,那么大概率会存在并发问题
- 什么是多线程的上下文切换?