java 【多线程基础 三】

线程安全问题

因为多个线程并发执行,引起的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时,阻塞.

  1. 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自身的特性,那么我们只能从后两个条件入手。

针对第三个条件,我们可以采用避免锁嵌套的方法来避免,但一些特殊场景下,必须要多重加锁,因此避免锁嵌套的方法,也不是最好的解决方法。

针对第四个条件,我们可以采用给锁编号,并约定加锁顺序的形式来解决。

内存可见性

线程安全问题产生的原因多种多样,其中之一的原因就是内存可见性。

针对一个变量,一个线程修改,一个线程读取

相关推荐
ulias21218 小时前
AVL树的实现
开发语言·数据结构·c++·windows
想你依然心痛18 小时前
从x86到ARM的HPC之旅:鲲鹏开发工具链(编译器+数学库+MPI)上手与实战
java·开发语言·arm开发·鲲鹏·昇腾
967718 小时前
python基础自学
开发语言·windows·python
我的golang之路果然有问题18 小时前
积累的 java 找工作资源
java·笔记
毕设源码-朱学姐18 小时前
【开题答辩全过程】以 基于Python的茶语店饮品管理系统的设计与实现为例,包含答辩的问题和答案
开发语言·python
Legendary_00818 小时前
LDR6020:单C口可充可放电PD协议芯片,开启USB2.0数据传输新体验
c语言·开发语言
源代码•宸18 小时前
Golang基础语法(go语言error、go语言defer、go语言异常捕获、依赖管理、Go Modules命令)
开发语言·数据库·后端·算法·golang·defer·recover
行者9618 小时前
Flutter适配OpenHarmony:高效数据筛选组件的设计与实现
开发语言·前端·flutter·harmonyos·鸿蒙
xwill*18 小时前
wandb的使用方法,以navrl为例
开发语言·python·深度学习