描述什么是线程安全
《Java Concurrency In Practice》的作者Brian Goetz对“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的”。
这句话的意思是:不管业务中遇到怎样的多个线程访问某对象或某方法的情况,而在编程这个业务逻辑的时候,都不需要额外做任何额外的处理(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全。相反,如果在编程的时候,需要考虑这些线程在运行时的调度和交替(例如在get()调用到期间不能调用set()),或者需要进行额外的同步(比如使用synchronized关键字等),那么就是线程不安全的。
什么情况下会出现线程安全问题,怎么避免
运行结果错误:a++ 多线程下出现消失的请求现象
public class MultiThreadsError implements Runnable {
static MultiThreadsError instance = new MultiThreadsError();
int index = 0;
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 < 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();
}
realIndex.incrementAndGet();
synchronized (instance) {
if (marked[index] && marked[index - 1]) {
System.out.println("发生错误:" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
}
活跃性问题:死锁、活锁、饥饿
public class MultiThreadDeadLockError implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
MultiThreadDeadLockError r1 = new MultiThreadDeadLockError();
MultiThreadDeadLockError r2 = new MultiThreadDeadLockError();
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("0");
}
}
}
}
}
数据争用:同时写数据,会造成一方的数据被丢弃或者写入错误,最终造成错误数据
竞争条件:执行顺序(读取文件内容:在文件写完之后读取,如果文件没写完就读取,造成顺序上的错误)
对象发布和初始化的时候安全问题
什么是发布:让这个对象可以让超出这个类范围的其他地方调用
- public修饰的方法
/**
* 描述:发布逸出
*/
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;
}
/**
* 改进方法:返回副本
* @return
*/
public Map<String, String> getStatesImproved() {
return new HashMap<>(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"));
// System.out.println(multiThreadsError3.getStatesImproved().get("1"));
// multiThreadsError3.getStatesImproved().remove("1");
// System.out.println(multiThreadsError3.getStatesImproved().get("1"));
}
}
运行结果
周一
null
返回null就是将发布对象给修改了,这个不是我们希望看到的
- return返回值是一个对象
- 把对象作为参数传入其他方法
什么是逸出:被发布到了不该发布的地方
-
1.方法返回一个private对象(private的本意是不让外部访问)
-
2.还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如:
2.1 在构造函数中未初始化完毕就this赋值/** * 描述:初始化未完毕,就this赋值 */ public class MultiThreadsError4 { static Point point; public static void main(String[] args) throws InterruptedException { new PointMaker().start(); Thread.sleep(10); // Thread.sleep(105); 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; MultiThreadsError4.point = this; Thread.sleep(100); this.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(); } } } sleep(10)的结果是1 , 0因为初始化未完毕 sleep(105)的结果是1 , 1 因为初始化完毕
2.2 隐式逸出——注册监听事件
/** * 观察者模式 */ public class MultiThreadsError5 { int count = 0; public MultiThreadsError5(MySource source){ source.registerListener(new EventListener() { @Override public void onEvent(Event e) { System.out.println("\n我得到的数字是" + count); } }); for(int i = 0; i < 10000; i++){ System.out.println(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); } catch (InterruptedException e) { e.printStackTrace(); } mySource.eventCome(new Event() { }); } }).start(); MultiThreadsError5 multiThreadsError5 = new MultiThreadsError5(mySource); } static class MySource { private EventListener listener; void registerListener(EventListener eventListener) { this.listener = eventListener; } void eventCome(Event e){ if(listener != null) { listener.onEvent(e); } else { System.out.println("还未初始化完毕"); } } } interface EventListener { void onEvent(Event e); } interface Event { } }
注册监听器比较隐晦的,这里监听器是一个匿名内部类,它可以持有外部类MultiThreadsError5的引用,所以它可以直接操作(读写)外部类的成员变量,注册监听器本身是线程安全的,但是由于count还没完成赋值100(for循环至少花了10ms以上的时间)。此时监听器拿到的count就是0
2.3 构造函数中运行线程
/**
* 描述:构造函数中新建线程
*/
public class MultiThreadsError6 {
private Map<String,String> states;
public MultiThreadsError6() {
new Thread(new Runnable() {
@Override
public void run() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
}).start();
}
public Map<String, String> getStates() {
return states;
}
public static void main(String[] args) throws InterruptedException {
MultiThreadsError6 multiThreadsError6 = new MultiThreadsError6();
// Thread.sleep(1000);
System.out.println(multiThreadsError6.getStates().get("1"));
}
}
结果
Exception in thread "main" java.lang.NullPointerException
at com.concurrency_core.background.MultiThreadsError6.main(MultiThreadsError6.java:33)
初始化的工作在另一个线程中,还没有执行完毕,会报错(时间不同,结果不同),当上面代码sleep(1000)就会正确显示
举个例子:比如说我们在构造函数中,需要去拿到一个线程池的引用或者数据库的连接池,而我们在创建这个连接池或者连接的时候,它往往都是在后台新开线程的,只不过我们根本察觉不到,因为我们是调用它的构造函数,比如调用一个数据库连接的构造函数,它在构造函数后面也做了很多的动作,它需要用多线程去加快自己的处理速度,所以我们其实也隐含了在构造函数中执行了一个新开线程的工作,这样一来,我们发布的过早,就容易造成线程安全的问题。
用工厂模式修改注册监听的方法
/**
* 描述:用工厂模式修复刚才的初始化问题
*/
public class MultiThreadsError7 {
int count;
private EventListener listener;
private MultiThreadsError7(MySource source) {
listener = new EventListener() {
@Override
public void onEvent(MultiThreadsError5.Event e) {
System.out.println("\n我得到的数字是" + count);
}
};
for (int i = 0; i < 10000; i++) {
System.out.println(i);
}
count = 100;
}
public static MultiThreadsError7 getInstance(MySource source) {
MultiThreadsError7 safeListener = new MultiThreadsError7(source);
source.registerListener(safeListener.listener);
return safeListener;
}
public static void main(String[] args) {
MySource mySource = new MySource();
MultiThreadsError7 safeListener =getInstance(mySource);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new MultiThreadsError5.Event() {});
}
}).start();
}
static class MySource {
private EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(MultiThreadsError5.Event e) {
if (listener != null) {
listener.onEvent(e);
} else {
System.out.println("还未初始化完毕");
}
}
}
interface EventListener {
void onEvent(MultiThreadsError5.Event e);
}
interface Event {
}
}
工厂模式解决的问题:注册过程不是在构造方法中执行,而是在工厂方法中执行,工厂方法调用两部分内容:1.初始化值,2.注册
需要考虑线程安全的情况有哪些
- 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
- 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check-then-act
1)read-modify-write: 一个线程读了一个共享数据,并且在此基础上更新数据。该例子在上面的index++ 已经展示过了
2)check-then-act: 一个线程读取了一个共享数据,并在此基础上决定其下一个的操作
- 不同的数据之间存在捆绑关系的时候:IP和端口号
- 我们使用其他类的时候,如果对方没有声明自己是线程安全的,那么大概率会存在并发问题
双刃剑:多线程会导致的问题
性能问题有哪些体现、什么是性能问题
为什么多线程会带来性能问题
调度:上下文切换
什么是上下文:保存现场(主要指在发生线程调度时,即可以运行的线程数超过CPU的数量)
上下文切换:可以认为是内核(操作系统的核心)在CPU上对于进程(包括线程)进行的活动:
(1)挂起一个进程,将这个进程在CPU中的状态(上下文)存储于内存中的某处,
(2)在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复,
(3)跳转到线程计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程
当某一个线程运行到比如Thread.sleep()的时候,调度器就会将线程阻塞,然后让另一个等待CPU的线程进入runnable状
态,上下文切换的开销时非常大,有时候甚至比线程执行的开销都大
缓存开销:缓存失效,一旦进行了上下文切换,cpu将执行不同线程的不同代码,导致原来的缓存失效。CPU将进行重新缓存
何时会导致密集的上下文切换
抢锁,IO