1. 线程安全问题
1.1 什么是线程安全?
- 某权威作者定义:“当多个线程访问同一个对象时,如果不用考虑这些线程的调度,也不需要额外的同步,调用这个对象的行为都可以获得正确的结果,那么这个对象时线程安全的“。
- 通俗易懂的定义:当多个线程访问同一个对象时,不需要做额外的任何处理,像单线程编程一样,程序可以正常运行,就可以称为线程安全。
1.2 线程安全问题有哪些?
- 运行结果错误:a++多线程下出现结果错误
- 活跃性问题:死锁、活锁、饥饿
1.2.1 运行结果错误
下面演示a++运行结果错误:
public class APlusPlus implements Runnable {
static int a = 0;
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
a++;
}
}
public static void main(String[] args) throws InterruptedException {
APlusPlus aPlusPlus = new APlusPlus();
Thread t1 = new Thread(aPlusPlus);
Thread t2 = new Thread(aPlusPlus);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(a);
}
}
11054
我们预计的结果是20000,最后结果比预计的少,因为a++,看上去是一个操作,实际上包含了三个动作:
1. 读取a
2. 将a加一
3. 将a的值写入到内存中
说明a++操作是不具备原子性的,因此需要对a++操作进行同步,保证同一时刻只有一个线程执行a++操作。
1.2.2 活跃性问题:死锁、活锁、饥饿
下面演示死锁问题:
public class DeadLock {
private static Object lockA = new Object();
private static Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取到lockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在尝试获取lockB...");
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取到lockB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取到lockB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在尝试获取lockA...");
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取到lockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
t1.start();
t2.start();
}
}
由于t1获取到lockA,t2获取到lockB,t1和t2分别想获取lockB和lockA,由于双方都没有释放锁,所以t1和t2都进入到了死锁状态。
1.3 各种需要考虑线程安全的情况
- 访问共享变量,比如对象的属性、静态变量、共享缓存等;
- 所有依赖顺序的操作,比如:读改写(原子性)、读取-检查-操作(内存可见性);
- 数据之间存在绑定关系;
- 使用其他类的时候,比如使用HashMap就不是线程安全的;
2. 性能问题
为什么多线程会带来性能问题?一共分为以下两个原因:
- 上下文切换
- 内存同步
2.1 上下文切换
- 什么是上下文切换:进行一次上下文切换时,先挂起当前线程,然后把当前线程状态存在某处,以便线程切换回来知道执行到哪里了,这个线程状态包含当前线程执行的指令和位置,这个状态就是上下文。
- 由于上下文切换也会使得CPU自身的一些缓存失效,相对而言,也降低了执行速度。
- 何时会密集地导致上下文切换:抢锁、IO
2.2 内存同步
- 由于JMM规定了主内存和线程内存,而线程内存会缓存数据,可以增加计算速度。由于在多线程编程时使用了synchronized、volatile关键字等,禁止了线程缓存,也禁止了指令重排序,编译器CPU等优化手段,使得是能只用主内存了,相对而言就降低了性能