线程安全问题
因为多个线程并发执行,引起的bug,这样的bug被称为"线程安全问题"或"线程不安全"
如果还不是很清楚,没问题,请看下面的代码
java
public class Demo22 {
private static int count;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i = 0; i < 50000 ; i++){
count++;
}
});
Thread t2 = new Thread(()->{
for(int i = 0; i < 50000 ; i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
在上面的代码中,t1,t2各自的逻辑中都count自增 50000次,因此正常情况下,结果应该输出为100000.

我们可以看到,程序执行了三次,三次的结果都不相同。此种结果与逻辑目标不同的情况就归为"线程安全问题".
但为什么会出现上述情况呢?
一条java语句不一定是原子的,也不一定只是一条指令
count++操作,虽然我们看来是一个语句,但其实在 cpu 视角来看,是3个指令
1)把内存中的数据,读取到 cpu 寄存器中 (load)
2)把 cpu 寄存器里的数据 + 1 (add)
3)把寄存器的值,写回内存 (save)
load,add,save 是cpu 中指令集的指令.不同架构的cpu有不同的指令集,在此只是为了方便介绍。
由于 cpu 调度执行线程时是抢占式执行,随机调度。说不定在执行某个指令时就会调走,因此count++ 是三个指令,可能会出现 cpu 执行了其中的一个指令或两个指令就调走的情况

但上述的执行顺序,只是一个可能的调度顺序.由于调度过程是"随机"的,因此会产生其他的执行顺序。


以上执行顺序都是没有保证原子性,导致此次结果不是100000的情况.
什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
那么如何解决上面的线程不安全问题呢
java
public class Demo22 {
private static int count;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(()->{
for(int i = 0; i < 50000 ; i++){
//添加synchronized 关键字,给count++操作 上"锁"
synchronized (locker){
count++;
}
}
});
Thread t2 = new Thread(()->{
for(int i = 0; i < 50000 ; i++){
//添加synchronized 关键字,给count++操作 上"锁"
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}

具体内容逻辑:

由于,t1 和 t2 都是针对 locker 对象加锁。t1 先加锁,于是 t1 就继续执行{ }中的代码,t2 后加锁,发现locker对象已经被加锁了,于是 t2 只能排队等待。

synchronized关键字
synchronized的特性
1)互斥
synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块,相当于加锁
退出 synchronized 修饰的代码块,相当于解锁

2)可重入
synchronized 代码块对应同一条线程来说是可重入的,不会出现死锁的情况;

加锁的时候,需要判定当前这个锁,是否是 被占用状态。
java在synchronized中引入计数器,记录该线程加锁几次,后续解锁时,可以在正确位置进行解锁。
可重入锁,就是在锁中,记录当前是哪个线程持有锁,后续加锁时都会进行判定
死锁:
1)一个线程没有释放锁,然后又尝试再次加锁

synchronized 不会 出现上述情况,只是借此例子解释死锁情况。
2)两个线程,两把锁
线程1 线程2 锁A 锁B
1)线程1 先针对 A 加锁,线程2 针对 B 加锁
2)线程1 不释放锁A的情况下,再对 B 加锁. 同时,线程2 不释放 B 的情况下对A 加锁
java
public class Demo24 {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (locker1){
System.out.println("t1 加锁 locker1成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2){
System.out.println("t1 加锁 locker2成功");
}
}
});
Thread t2 = new Thread(()->{
synchronized (locker2){
System.out.println("t2 加锁 locker2 成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker1){
System.out.println("t1 加锁 locker1 成功");
}
}
});
t1.start();
t2.start();
}
}
此时就只打印以下语句

我们在jconsole看一下线程具体情况


说明这两个线程 t1、t2 都在第二次 synchronized时,阻塞.
- N个线程,M个锁
哲学家就餐问题
synchronized 使用示例
1)修饰代码块:明确指定锁哪个对象
java
public class Demo23 {
private Object locker = new Object();
public void method(){
synchronized (locker){
}
}
}
锁当前对象
java
public class Demo23 {
public void method(){
synchronized (this){
}
}
}
2)直接修饰普通方法
相当于针对 this 加锁
java
public class Demo23 {
public synchronized void method(){
}
}

3)修饰静态方法
相当于针对 对应的类对象 加锁
java
public class Demo23 {
public synchronized static void method(){
}
}
理解锁对象的作用
可以把任意的 Object/Object 子类的对象,作为锁对象
锁对象是谁不重要,重要的是,两个线程的锁对象是否是同一个
是同一个,才会出现 阻塞 / 锁竞争
不是同一个,不会出现 阻塞 / 锁竞争
如何解决死锁问题?
那我们需要先知道死锁是如何产生的
死锁的四个必要条件
1.互斥的 [锁的基本特性]
2.不可抢占 [锁的基本特性]
3.请求和保持 [代码结构]
4.循环等待 [代码结构]
前两个条件我们很难解决,因为是synchronized自身的特性,那么我们只能从后两个条件入手。
针对第三个条件,我们可以采用避免锁嵌套的方法来避免,但一些特殊场景下,必须要多重加锁,因此避免锁嵌套的方法,也不是最好的解决方法。
针对第四个条件,我们可以采用给锁编号,并约定加锁顺序的形式来解决。
内存可见性
线程安全问题产生的原因多种多样,其中之一的原因就是内存可见性。
针对一个变量,一个线程修改,一个线程读取