由于调度器的抢占式执行, 或者说随机性很强的调度行为, 会让我们捉摸不透程序实际中的运行模式, 特别是在多线程的模式下, 就容易出现线程安全的问题, 例如我们举一个非常经典的例子:
创建两个线程, 让两个线程同时对一个静态变量 cnt 各自进行自增操作 10000 次
例如: 线程 1 让 cnt 自增 10000 次, 线程2 同样让 cnt 自增 10000 次
按我们的原始想法来说, 各自自增 10000, 最终值应该为 20000 才合理
但是: 实际上 cnt 最终结果并非 20000
public class Tmp {
//创建两个线程,让这俩线程同时对一个变量进行自增操作
public static int cnt = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for(int i = 0; i < 10000; i++) {
cnt++;
}
});
Thread t2 = new Thread(() -> {
for(int i = 0; i < 10000; i++) {
cnt++;
}
});
t1.start();
t2.start();
try {
t1.join(); //线程等待, 让两个线程都执行完, 在打印 cnt 最终次数
t2.join();
} catch (InterruptedException e) { e.printStackTrace();}
System.out.println("最终运算结果为" + cnt);
}
}
我们执行上述程序, 如果什么都不考虑, 那么每个线程对 cnt 分别自增 10000 次, 最终 cnt 应该是20000 , 但是实际上:
那么为什么会出现上面那种情况呢, 我们分析一波
cnt ++;
在理想状态下, 我们如果想顺利将 cnt 自增到 20000, 那么站在两个线程的角度上, 应该是按照下面这种操作方式, 让一个操作完整的完成 cnt ++ 后, 再让另外一个线程去操作
但是操作系统对线程的调度都是很随机的, 实际上每个线程的这 3 个操作在这种情况下, 根本就没法保证执行顺序 ! 我们举个例子 ↓
有可能出现这种情况↑ , 例如刚开始 cnt = 0;
- 线程 1 读取 cnt = 0, 线程 2 也读取 cnt = 0;
- 线程 1 进行 cnt ++, 然后写入内存, 之后 cnt = 1;
- 线程 2 再进行 cnt ++, 但是由于实际上 cnt 刚刚读取的是 0 , 所以自增完 cnt = 1, 再写入内存, 之后 cnt 还是为 1
- 也就是说两个自增操作, cnt 仍然是 1
除了这种情况之外, 还有很多排列组合的情况, 大致如上, 不再一一列举
为了解决以上问题, 我们可以使用synchronized关键字来给对象或者类加锁。加这个关键词后线程执行任务前都会上锁, 执行完任务就会解锁。 在上锁后, 其他线程需要执行任务前要上锁, 但是由于已经被锁住了, 就无法完成上锁操作, 由此进入阻塞状态, 直到锁被释放。 例如:
class Test {
public int cnt = 0;
public void increase() {
synchronized (this) { //同步代码块
this.cnt ++;
}
}
}
public class Tmp {
//创建两个线程,让这俩线程同时对一个变量进行自增操作
public static Test test = new Test();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for(int i = 0; i < 10000; i++) {
test.increase();
}
});
Thread t2 = new Thread(() -> {
for(int i = 0; i < 10000; i++) {
test.increase();
}
});
t1.start();
t2.start();
try {
t1.join(); //线程等待, 让两个线程都执行完, 在打印 cnt 最终次数
t2.join();
} catch (InterruptedException e) { e.printStackTrace();}
System.out.println("最终运算结果为" + test.cnt);
}
}
- 创建一个静态变量 test = 1;
- 让线程1 进入 whlie (test == 1) {} 但什么都不做的死循环, 如果循环结束, 打印"线程1结束"
- 在线程2 中修改 test 的值为6
- 按原始的想法来说, 在线程2中将 test 改为 6 之后, 线程1应该停下循环, 然后打印"线程1结束"
public class Tmp {
public static int test = 0; //定义一个整形变量
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (test == 0) {
//啥都不干
}
System.out.println("线程1执行完了"); //循环结束, 则打印这个语句
});
Thread t2 = new Thread(() -> {
Scanner in = new Scanner(System.in);
System.out.println("输入 ->"); //在线程2中偷偷改变 test 的值
test = in.nextInt();
});
t1.start();
t2.start();
try {
t1.join(); //线程等待
t2.join();
} catch (InterruptedException e) { e.printStackTrace();}
}
}
如上代码, 在修改test = 6之后, 线程1并不会停下来:
可以看出, 线程1并没有停下, 还在一直死循环(没有打印)
我们再来分析一波:
为了让上述情况的操作更有可控性, 我们可以使用关键字 volatile, 我们示范一次
上述代码在 test 前加上一个 volatile 后
public static volatile int test = 0;
就可以按照我们的逻辑搞定了
那么 volatile 有在上面起到了什么作用呢