文章目录
前言
本文主要介绍多线程的安全和性能问题,包括几个线程不安全的例子和解决办法
线程安全
当多线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
即 不管业务中遇到怎样的多个线程访问某对象或某方法的情况,而在编程这个业务逻辑的时候,都不需要做任何额外的处理(也就是像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全。
1 运行结果错误
1.1 a++的例子
创建两个线程,让这两个线程对同一个数进行++操作,执行10000次
public class MultiThreadError implements Runnable {
int index = 0;
static MultiThreadError instance = new MultiThreadError();
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++;
}
}
}
打印结果:
结果不是20000原因:
在线程1将index+1后,由于线程2已经读到了index被线程1 ++前的数据,因此,相当于线程2拿到了一个假数据,线程1对index的++无效。
1.2 a++的改正:打印出错误的地方和次数
要让一个线程变成安全的就要付出代价,包括运行速度、设计成本等,在设计程序时,要考虑程序对线程安全的需求,比如程序是否要完全确保线程安全。
改进程序的思路:
- 要打印错误的位置,可以添加一个标志,当一个线程修改这个位置的数据后,就把它设置为true,当第二个线程读到true时候,就说明冲突了。
- 针对1的问题:线程可能在设置成true之前就已经冲突了,因此添加一个synchronized 锁,保证依次读到。
- 然而,会出现这样的情况:线程1将要执行mark[index] = true; ,而线程2正在执行index++,这样就会导致在线程1标志的位置出错,因此要确保线程1 在执行synchronized代码块时,线程2已经执行完index++,在等待。所以在index++后面添加了CyclicBarrier,确保两个线程执行到synchronized代码块前的位置。
- 而在线程1执行synchronized代码块时,有可能线程2 在index++,因此在index++前面也要用CyclicBarrier确保位置。
public class MultiThreadError implements Runnable {
int index = 0;
static MultiThreadError instance = new MultiThreadError();
final boolean[] mark = new boolean[100000];
static AtomicInteger realCount = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2); //参数为要等待几个线程
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
index++;
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
realCount.incrementAndGet();
synchronized (instance) {
if (mark[index] == true && mark[index - 1] == true) {
wrongCount.incrementAndGet();
System.out.println("在" + index + "发生错误");
} else {
mark[index] = true;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
// thread1.setDaemon(true); //设置为守护线程
// thread2.setDaemon(true);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("打印的结果是:" + instance.index);
System.out.println("真正运行次数:" + realCount.get());
System.out.println("错误次数:" + wrongCount.get());
}
}
输出结果:
可以准确找到出错位置.
1.3 题外话——守护线程
在另一篇博客中https://blog.csdn.net/qq_44357371/article/details/108344885,谈到了守护线程:将用户线程设置为守护线程,会变得危险
这句话就可以在这里得到印证,假若我们将创建的两个子线程设置为守护线程,就会导致主线程会在子线程之前就结束,导致无法将20000打印完。
2 活跃性问题:死锁
死锁就是两个线程互相等待对方持有的资源。
public class DeadLockMultiThreadError implements Runnable {
int flag = 1;
static Object object1 = new Object();
static Object object2 = new Object();
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1) {
synchronized (object1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
System.out.println("1 结束了");
}
}
}
if (flag == 0) {
synchronized (object2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1) {
System.out.println("2 结束了");
}
}
}
}
public static void main(String[] args) {
DeadLockMultiThreadError deadLockMultiThreadError1 = new DeadLockMultiThreadError();
DeadLockMultiThreadError deadLockMultiThreadError2 = new DeadLockMultiThreadError();
deadLockMultiThreadError1.flag = 1;
deadLockMultiThreadError2.flag = 0;
new Thread(deadLockMultiThreadError1).start(); //让线程1获取object1,等待资源object2
new Thread(deadLockMultiThreadError2).start(); //让线程2获取object2,等待资源object1
}
}
两个线程都需要获得到对方持有的资源才可以结束,因此死锁,程序永远无法结束。
3 对象发布和初始化的时候的安全问题
3.1 方法内返回了一个private对象
用private创建一个表,不让外部访问,但是在下面的getStates方法中,将private对象发布了出去,使其丧失了原本的属性。这就导致外部可以对private数据随意的篡改,使线程不安全。
public class EscapeMultiThreadError {
private Map<String,String> states;
public EscapeMultiThreadError(){
states = new HashMap<>();
states.put("1","周一");
states.put("2","周二");
states.put("3","周三");
states.put("4","周四");
}
public Map<String, String> getStates() {
return states; //这里将private对象states发布了出去
}
public static void main(String[] args) {
EscapeMultiThreadError escapeMultiThreadError = new EscapeMultiThreadError();
Map<String ,String> states = escapeMultiThreadError.getStates();
System.out.println(states.get("1"));
states.remove("1");
System.out.println(states.get("1"));
}
}
3.2 解决逸出:返回副本
在上面发布的时候,是将原有的private数据直接发布,造成不安全。可以创建一个副本,发布时候只发出副本,而不会对原数据造成影响。
public class EscapeMultiThreadError {
private Map<String,String> states;
public EscapeMultiThreadError(){
states = new HashMap<>();
states.put("1","周一");
states.put("2","周二");
states.put("3","周三");
states.put("4","周四");
}
public Map<String, String> getStates() {
return states; //这里将private对象states发布了出去
}
public Map<String, String> getStatesImproved() {
return new HashMap<>(states); //这里将private对象states发布了出去
}
public static void main(String[] args) {
EscapeMultiThreadError escapeMultiThreadError = new EscapeMultiThreadError();
Map<String ,String> states = escapeMultiThreadError.getStates();
// System.out.println(states.get("1"));
// states.remove("1");
// System.out.println(states.get("1"));
Map<String ,String> statesImproved = escapeMultiThreadError.getStatesImproved();
System.out.println(statesImproved.get("1"));
statesImproved.remove("1");
System.out.println(states.get("1"));
}
}
4 构造函数中未初始化完就this赋值
public class EscapeMultiThreadError2 {
static Point point;
public static void main(String[] args) throws InterruptedException {
new PointMaker().start();
//这里会随时间的不同导致结果不一样
// Thread.sleep(1000);
if (point != null) {
System.out.println(point);
}
}
}
class Point {
private final int x, y;
public Point(int x, int y) throws InterruptedException {
this.x = x;
EscapeMultiThreadError2.point = this; //未初始化完毕就构造赋值
Thread.sleep(100);
this.y = y; //这里才给y赋值
}
@Override
public String toString() {
return x + "," + y;
}
}
class PointMaker extends Thread {
@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5 监听器模式中的隐式逸出
public class ObserverMultiThreadError {
int count;
public ObserverMultiThreadError(MySource source) {
source.registerListener(new EventListener() { //这里注册的监听器其实可以直接获取到外部的count,所以当count没有赋值完成时,它会直接打印出0
@Override
public void onEvent(Event e) {
System.out.println("\n我得到的数字是" + count);
}
});
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10); //当这里的休眠时间很短时,由于前面打印数字的任务还没有结束,对count的赋值还没有完成,所有,后面直接运行了eventCome,跳到onEvent中,将count=0打印了出来。
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new Event() {
});
}
}).start();
ObserverMultiThreadError multiThreadError = new ObserverMultiThreadError(mySource);
}
static class MySource {
private EventListener eventListener;
void registerListener(EventListener eventListener) {
this.eventListener = eventListener;
}
void eventCome(Event e) {
if (eventListener != null) {
eventListener.onEvent(e);
} else {
System.out.println("还未初始化完毕");
}
}
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
5.1 对监听器中隐式逸出的修正
思路:要保证对count的赋值已完成才能打印
方法:用工厂模式,先去得到一个ObserverMulyiThreadErrorFix的实例,但这时并没有真正的注册创建,在实例获取到之后,在对实例进行注册
ObserverMulyiThreadErrorFix safeListener = new ObserverMulyiThreadErrorFix(mySource); //这里只是创建了出来,还没有真正的注册上去
mySource.registerListener(safeListener.eventListener); //这里才真正的注册进去,这时的count值才真正生效
public class ObserverMulyiThreadErrorFix {
int count;
private EventListener eventListener;
private ObserverMulyiThreadErrorFix(ObserverMulyiThreadErrorFix.MySource source) {
eventListener = new ObserverMulyiThreadErrorFix.EventListener() { //这里注册的监听器其实可以直接获取到外部的count,所以当count没有赋值完成时,它会直接打印出0
@Override
public void onEvent(ObserverMulyiThreadErrorFix.Event e) {
System.out.println("\n我得到的数字是" + count);
}
};
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
public static ObserverMulyiThreadErrorFix getInstance(MySource mySource) {
ObserverMulyiThreadErrorFix safeListener = new ObserverMulyiThreadErrorFix(mySource); //这里只是创建了出来,还没有真正的注册上去
mySource.registerListener(safeListener.eventListener); //这里才真正的注册进去,这时的count值才真正生效
return safeListener;
}
public static void main(String[] args) {
ObserverMulyiThreadErrorFix.MySource mySource = new ObserverMulyiThreadErrorFix.MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new ObserverMulyiThreadErrorFix.Event() {
});
}
}).start();
ObserverMulyiThreadErrorFix multiThreadError = new ObserverMulyiThreadErrorFix(mySource);
}
static class MySource {
private ObserverMulyiThreadErrorFix.EventListener eventListener;
void registerListener(ObserverMulyiThreadErrorFix.EventListener eventListener) {
this.eventListener = eventListener;
}
void eventCome(ObserverMulyiThreadErrorFix.Event e) {
if (eventListener != null) {
eventListener.onEvent(e);
} else {
System.out.println("还未初始化完毕");
}
}
}
interface EventListener {
void onEvent(ObserverMulyiThreadErrorFix.Event e);
}
interface Event {
}
}
总结
需要考虑线程安全的情况
线程性能
当可用性的线程数大于CPU数时,会发生线程调度,而在线程调度时,需要上下文来保存线程(寄存器里面暂存的内容,比如线程状态)。
当某一个线程运行到Thread.sleep(),调度器会将线程阻塞,然后让另一个等待CPU的线程进入到runnable状态,这样的动作就是上下文切换。
上下文切换的步骤:
- 挂起当前线程
- 将线程状态存储在内存中
因此上下文切换的开销是非常大的,包括时间开销和缓存开销,这会极大的影响线程的性能。