4.1 线程是不安全的
线程不安全:程序没有按照预期正常工作。
public class Wrong {
private static int n = 0;
private static final int COUNT = 10_0000;
private static class Add extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
n++;
}
}
}
private static class Sub extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
n--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Add();
Thread thread2 = new Sub();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(n);
}
}
理论结果应该是:0
运行结果:
4.2 为什么会出现线程不安全的问题?
4.2.1 共享数据
- 私有数据是不需要考虑线程安全问题的,共享数据才是导致线程不安全的原因。
- 共享资源//共享变量共享数据
1.每个线程都有自己独立的调用栈(不共享):局部变量+形参
2.堆(除栈外)都是共享的
java内存分布 | 是否共享 |
---|---|
PC/栈(Java栈+本地栈) | 私有的(局部变量/形参) |
栈/方法/常量池 | 共享的(对象/类的信息(类的对象))、属性/静态属性等 |
4.2.2 调度问题
线程之间会因为调度问题,穿插着进行,数据的方法有一点特殊规则。
4.2.3 代码的原子性/内存的可读性/代码的重排序
1.代码的原子性
- 原子性:一段代码在运行期间是不可分割的
eg:一件事情由A–>B–>C–>D组成,若在B执行完后执行G,则破坏原子性了。
注意:一句Java代码不一定是原子的
eg:n++;
1.先取n的值 2.把n的值加1 3.再把 n+1 的值写回内存中 - 原子性中不可再分的代码片段 —— 临界区
2.内存的可见性
- 计算机的存储三角形:数字越小,速度越快;数字越大,容量越大。
- Java中:程序运行过程中,Java只保证了单线程情况下,工作内存中的数据是正确的,如果要保证多线程情况下,可以看到线程的工作情况(内存的变化),就需要保证变量的可见性问题。
解释:每个线程工作时,不能直接操作主内存中的数据,需要拷贝到自己的工作内存中操作,结束后写回主内存。在多线程下,存在内存的可见性问题。
3.代码的重排序
- 为什么会进行重排序?
答:CPU / javac编译器 / 运行时JIT对代码进行适度的优化。 - Java规定了优化必须保证单线程情况下的正确性。
程序测试:
public class Reorder {
private static int a0 = 0;
private static int a1 = 0;
private static int a2 = 0;
private static int a3 = 0;
private static int a4 = 0;
private static int a5 = 0;
private static int a6 = 0;
private static int a7 = 0;
private static int a8 = 0;
private static int a9 = 0;
private static class Set extends Thread {
@Override
public void run() {
a0 = 1;
a1 = 2;
a2 = 3;
a3 = 4;
a4 = 5;
a5 = 6;
a6 = 7;
a7 = 8;
a8 = 9;
a9 = 10;
}
}
private static class Print extends Thread {
@Override
public void run() {
System.out.println(a0);
System.out.println(a1);
System.out.println(a2);
System.out.println(a3);
System.out.println(a4);
System.out.println(a5);
System.out.println(a6);
System.out.println(a7);
System.out.println(a8);
System.out.println(a9);
}
}
public static void main(String[] args) {
Thread t1 = new Set();
Thread t2 = new Print();
t1.start();
t2.start();
}
}
理论结果:1 2 3 4 5 6 7 8 9 10 但由于代码的重排序问题,导致结果出现偏差。
运行结果:
4.3 如何保证线程安全?
- 如果可以的话,设计出不需要数据共享的程序,天生安全。
- 如果一定要共享数据,尽可能保证数据的只读性——不可变对象。
- 利用各种机制保证:原子性、可见性、重排序。