线程安全问题
什么是线程安全问题
线程安全概念
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线
程安全的。
线程不安全的案例
多个线程对同一个变量进行修改操作时,可能会有线程安全问题。
我们创建两个线程同时对同一个变量count自增5k次,那么这个变量最终的结果应该是1w。
class Counter {
public int count = 0;
public void add() {
count++;
}
}
public class ThreadDemo11 {
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
// 启动两个线程
t1.start();
t2.start();
// 等待
t1.join();
t2.join();
// 等两个线程都结束后打印
System.out.println(counter.count);
}
}
执行结果:
从执行结果可以看出,这个值是不符合我们的预期,这就是线程安全问题。
这个值是随机的,而且小于等于10000。
线程不安全的原因
- 操作系统随机调度线程,抢占式执行。线程中的代码执行到任意一行,都随时可能被切换出去。
- 多个线程修改同一个变量。上述案例两个线程对 Counter.count 这个变量同时修改。
- 修改操作,不是原子的。上述案例对 count++ 这个操作可以细分为 三个cpu指令。
load
从内存中读取变量的值到cpu寄存器中。add
在寄存器中对这个值加一。save
将cpu寄存器中的值保存至内存中。
再加上线程抢占式执行的原因,当线程1进行 load
,add
后,还没来得及save
,线程2 就 load
了,那么此时两个线程 load
到的值是一样的,这两个线程执行完这次自增操作后,count的值只加了1。所以这就是上述案例中,最终结果会小于等于10000的原因了。
- 内存可见性问题
案例:
t1线程包含一个循环,循环结束条件是 flag == 0。
t2线程从键盘读入一个整数,并给flag赋值。
预期结果是输入一个非0的数,然后t1线程结束。
public class ThreadDemo12 {
public static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0) {
// 空着
}
System.out.println("t线程结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入一个整数:");
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
执行结果:
这个执行结果与我们的预期不符。
这是编译器优化导致的,t1线程中频繁读取内存中 flag 的值,一开始读内存中 flag 的值,flag 一直不变,编译器就进行优化,t1线程直接用寄存器中 flag 的值(从寄存器读取比从内存读取快几个数量级),这时候t2线程对 flag 进行修改,t1线程也感知不到。
- 指令重排序问题
这也是编译器优化导致的,一个操作有多条cpu指令,由于编译器优化改变执行cpu指令的顺序,而出现的问题。
编译器优化在单线程环境下的准确性可以保证,但是在多线程环境下就没那么容易了。
如何解决线程安全问题
synchronized 关键字
synchronized 的特性
一、 互斥
一个线程进入 synchronized 代码块对一个对象加锁,其他线程如果也想对这个对象加锁就只能阻塞等待。
进入 synchronized 修饰的代码块, 相当于 加锁。
退出 synchronized 修饰的代码块, 相当于 解锁。
二、内存刷新
synchronized 的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
这说明 synchronized 也能解决内存可见性问题
三、可重入
一个线程进入 synchronized 对某个对象加锁,这个线程还没释放锁就又进入一个 synchronized
对同一个对象加锁,这个时候不会互斥。synchronized 代码块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
代码示例:
static class Counter {
public int count = 0;
synchronized void add() {
count++;
}
synchronized void add2() {
add();
}
}
synchronized 如何使用
- 修饰普通方法
锁的是 Test 的对象
class Test {
synchronized public void method() {
}
}
- 修饰静态方法
锁的是 Test 类的对象
class Test {
synchronized public static void method() {
// 锁的是
}
}
- 修饰代码块,指定加锁对象
class Test {
public void method() {
synchronized (this) {
// this 与修饰普通方法等价
}
}
}
class Test {
public void method() {
synchronized (Test.class) {
// Test.class 与修饰静态方法等价
}
}
}
synchronized 解决问题
在第一个案例中,由于 count++ 这个操作不是原子的,而引起的线程不安全问题。
我们可以通过 synchronized 关键字对这个操作加锁来解决这个问题。
class Counter {
public int count = 0;
public void add() {
synchronized (this) {
// this 当前对象
count++;
}
}
}
public class ThreadDemo11 {
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
// 启动两个线程
t1.start();
t2.start();
// 等待
t1.join();
t2.join();
// 等两个线程都结束后打印
System.out.println(counter.count);
}
}
执行结果:
synchronized 解决内存可见性问题
public class ThreadDemo12 {
public static int flag = 0;
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(() -> {
while (true) {
synchronized (locker) {
if (flag != 0) {
break;
}
}
}
System.out.println("t线程结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入一个整数:");
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
执行结果:
volatile关键字
volatile 修饰的变量,可以保证 内存可见性。
前面在内存可见性说的问题,直接读取寄存器的值,导致数据不一致的情况。
用 volatile 修饰变量后,可以强制读写内存,速度变慢了,但是准确性提高了。
代码示例:
public class ThreadDemo12 {
// 加上volatile
volatile public static int flg = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flg == 0) {
// 空着
}
System.out.println("t线程结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入一个整数:");
flg = scanner.nextInt();
});
t1.start();
t2.start();
}
}
执行结果:
volatile 不能保证原子性。
volatile 可以禁止指令重排序。