高并发及线程安全
- 高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。
- 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。
1. 多线程的运行机制
- 当一个线程启动后,JVM 会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行;
- 线程的代码:
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("i = " + i);
}
}
}
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
- 多个线程在各自栈区中独立、无序的运行,当访问一些代码,或者同一个变量时,就可能会产生一些问题;
- 多线程的三个特性:原子性、可见性、有序性,对应了三种安全性问题;
2. 多线程的安全性问题——可见性
- 例如下面的程序,先启动一个线程,在线程中将一个变量的值更改,而主线程却一直无法获得此变量的新值:
class MyThread extends Thread {
public static int a = 0;
@Override
public void run() {
System.out.println("线程启动,休息2秒...");
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("将a的值改为1");
a = 1;
System.out.println("线程结束...");
}
}
public class Test {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
while (true) {
if (MyThread.a == 1) {
System.out.println("主线程读到了a = 1");
}
}
}
}
3. 多线程的安全性问题——有序性
- 有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:
int a = 10;
int b = 20;
int c = a + b;
- 第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。1和2先编译谁,不影响第三行的结果;
- 但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响:
- 多线程环境下,我们通常不希望对一些代码进行重排的;
4. 多线程的安全性问题——原子性
- 原子性是指一个操作是不可中断的,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰;
- 请看以下示例:
class MyThread extends Thread {
public static int a = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
}
System.out.println("修改完毕!");
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("获取a最终值:" + MyThread.a);
}
}
- 线程t1先读取a 的值为:0,
t1被暂停,
线程t2读取a的值为:0,
t2将a = 1,
t2将a写回主内存,
t1将a = 1,
t1将a写回主内存(将t2更改的1,又更改为1),
所以两次加1,但结果仍为1,少加了一次;
- 原因:两个线程访问同一个变量a的代码不具有"原子性";