1. 观察线程不安全
java
class Counter {
public int count = 0;
public void increase() {
count++;
}
}
// 线程安全问题演示.
public class Demo12 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
上述的代码中两个线程, 针对同一个变量, 进行循环自增. 各自自增5w次,预期最终应该是10w, 但实际上,并不是这样的结果. 每次运行的结果都不一样, 并且还都是错的.


在多线程下,发现由于多线程执行,导致的bug, 统称为"线程安全问题". 如果某个代码, 在单线程下执行没有问题, 多个线程下执行也没问题, 则称为"线程安全",反之就可以称为"线程不安全".
那么啥是bug呢? bug是一个非常广义的概念. bug 的中文名,可以翻译成"幺蛾子". 只要是实际运行效果和预期效果(需求中的效果)不一致,就可以称为是一个bug.
线程安全和线程不安全的区别也就是多线程代码是否有bug.
那么上述的代码为啥会出现bug呢?


如果上述操作, 在两个线程或者多个线程并发执行的情况下, 就可能会出现问题.

如果上述两个线程是这样串行执行的, 那么结果就会是对的. 但是真的能这样吗? 上述图片中虽然是只是自增两次,但是由于两个线程并发执行, 就可能在一定的执行顺序下,导致运算的中间结果就被覆盖了. 在这5w次的循环过程中, 有多少次这俩线程执行++是"串行的"?,有多少次会出现覆盖结果的? 这些都不确定. 因为线程的调度是随机的, 是抢占式执行的过程.

上述的过程就是结果被覆盖的例子. 此处这两个线程的调度是不确定的, 这两组对应的操作也会有差异. 而且上述代码得到的结果一定是小于100000的, 因为有结果被覆盖掉了.
2. 线程安全问题的原因
1) [根本原因]多个线程之间的调度顺序是"随机的", 操作系统使用"抢占式"执行的策略来调度线程.这就是罪魁祸首,万恶之源.
和单线程不同的是, 在多线程下, 代码的执行顺序,产生了更多的变化.
以往只需要考虑代码在一个固定的顺序下执行,执行正确即可. 现在则要考虑多线程下, N种执行顺序下,代码执行结果都得正确.
这件事情,木已成舟,咱们无力改变.当前主流的操作系统,都是这样的抢占式执行的.
2) 多个线程同时修改同一个变量就容易产生线程安全问题.
一个线程修改一个变量, 没事.
多个线程读取同一个变量, 没事.
多个线程修改多个变量, 没事.
3) 进行的修改, 不是"原子的".
如果修改操作,能够按照原子的方式来完成, 此时也不会有线程安全问题.
count++ 不是原子的~
= 直接赋值, 可以视为原子.
if = 先判定, 再赋值, 也不是原子的~~
所以解决线程安全, 最主要的切入手段就是"加锁".
"加锁"相当于是把一组操作, 给打包成一个"原子"的操作.
事务的那个原子操作, 主要是靠回滚. 此处这里的原子, 则是通过锁进行"互斥", 也就是这个线程进行工作的时候, 其他线程无法进行工作.
那根据上面的例子和代码, 我们就可以知道要给count++加锁, 使用synchronized关键字即可.

于是乎代码变动成了这样.
java
class Counter {
public int count = 0;
synchronized public void increase() {
count++;
}
}
// 线程安全问题演示.
public class Demo12 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}


那么就有一个问题了, 通过加锁操作之后, 把并发执行=>串行执行了. 此时, 多线程还有存在的意义嘛?
必然是有的. 代码中的线程并不是只做了count++这一件事, for循环并没有加锁, for循环中操作的变量i是栈上的一个局部变量. 两个线程, 是有两个独立的栈空间, 也就是完全不同的变量, 就不涉及到线程安全问题. 因此,这两个线程,有一部分代码是串行执行的, 有一部分是并发执行的, 就仍然要比纯粹的串行执行效率要高.
synchronized进行加锁解锁, 其实是以"对象"为维度进行展开的.

加锁目的是为了互斥使用资源.(互斥的修改变量)
synchronized每次加锁,也是针对某个特定的对象加锁!
如果两个线程针对同一个对象进行加锁
就会出现锁竞争/锁冲突(一个线程能加锁成功,另一个线程阻塞等待), 那么就可以解决线程安全问题.
具体是针对哪个对象加锁,不重要.
重要的是, 两个线程, 是不是针对同一个对象加锁.
就比如更改一下代码, 也一样可有算出正确答案.
java
class Counter {
public int count = 0;
private Object locker = new Object();
public void increase() {
synchronized (locker) {
count++;
}
}
public void increase2() {
synchronized (locker) {
count++;
}
}
}
// 线程安全问题演示.
public class Demo12 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase2();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}


4) 内存可见性,引起的线程安全问题.
java
public class Demo13 {
private static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (isQuit == 0) {
;
}
System.out.println("t1 执行结束. ");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入 isQuit 的值: ");
isQuit = scanner.nextInt();
});
t1.start();
t2.start();
}
}


这就是内存可见性的问题.
可⻅性是指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到.
程序在编译运行的时候, java编译器和jvm可能会对代码做出一些"优化". 一般的程序猿是负责写代码的.当写好一个代码之后,人家开发java编译器,开发jvm的大佬,可能会认为你这个代码写的不够好. 当你的代码实际执行的时候, 编译器/jvm就可能把你的代码给改了,保持原有逻辑不变的情况下,提高代码的执行效率.
编译器优化, 本质上是靠代码,智能地对你写的代码进行分析判断进而进行调整. 这个调整过程大部分情况下都是ok的, 都能保证逻辑不变. 但是, 如果遇到多线程了,此时的优化可能就会出现差错, 也就是使原有的代码逻辑改变了.

volatile关键字的出现, 就弥补了上述的问题.




Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.
⽬的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果.
