多线程04 死锁,线程可见性

简介: 多线程04 死锁,线程可见性

前言

前面我们讲到了简单的线程安全问题以及简单的解决策略

其根本原因是cpu底层对线程的抢占式调度策略,随机调度

其他还有一些场景的问题如下

1.多个线程同时修改一个变量问题

2.执行的操作指令本身不是原子的

比如自增操作就分为三步,加载,自增,保存

3.内存可见性问题

4.指令重排序问题

下面两个问题将会在本文中被解决

前面我们说到了解决几个线程同时修改一个变量的问题,我们使用加锁的方式来解决

使用synchronized关键字

特殊用法:用synchronized修饰普通方法,此时同步监视器就变为了this

修饰静态方法的时候此时相当于使用类对象当做同步监视器

synchronized加的锁也可以称为互斥锁

1.synchronized的一些其他特性

先举个例子

public class ThreadDemo21 {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread t1 = new Thread(()->{
           synchronized (lock){
               synchronized (lock){
                   System.out.println("hello");
               }
           }
        });
        t1.start();
    }
}

这里我们直观上感觉,t1先持有了这个lock锁,此时在没有释放的情况下再进行加锁理论上应该会出现阻塞的情况,但是实际上并没有阻塞.这里的线程是会正确执行的?

为什么呢???

这是因为这里的两次加锁是同一个线程进行的,所以第二次锁实际上并没有添加,只是真正加了一次锁,第二次加锁实际上是以计数器的形式自增一次,而并没有真正的加锁,所以释放的时候也释放了一次.

有人问这有啥用呢???

其实是为了我们在写一些复杂逻辑的代码中可能会忘了这些加锁的过程,从而导致以上的阻塞的情况(称为死锁)

这个时候其实就巧妙的解决了问题,比如说如下情况

此时这种锁的机制称为"可重入锁"的机制

2.三种经典的死锁场景

1.一个线程一把锁

也就是我们刚刚讨论的场景,如果这个时候锁没有这个"可重入锁"的机制,我们就会发生死锁问题.

2.两个线程两把锁

举个例子,这里假设线程1拿到a锁,想获取b锁,同时线程2拿到b锁想获取a锁,此时两者都在等另一个线程释放另一个锁,就发生了僵持的效果

你可以想象两者发生一个交易,一个想先交钱,一个想先交货,两个人一直僵持而迟迟不能完成交易.

public class ThreadDemo23 {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2){
                    System.out.println("t1我拿到了两个锁");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock1){
                    System.out.println("t1我拿到了两个锁");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

此时加上两个sleep是因为,希望在获取对应锁执行希望对应的一方获取到了对应的锁,此时执行就会发生僵持的效果

上述想解决僵持效果只需要将其中的一个线程的获取锁顺序的

3.n个线程m把锁

这里就涉及到一个哲学家进餐的问题

由Dijkstra提出并解决的哲学家就餐问题是典型的同步问题。该问题描述的是五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。

这个时候,加入五个人同时想进餐,这个时候就会发生每个人都拿到一只筷子,而不愿意放下,这就构成了一个死锁

谈解决方案之前,我们要先讨论一下构成死锁的四个必要条件

1.互斥使用,使用锁的过程是互斥的,一个线程拿到这个锁就,另一个线程想要获取就得阻塞等待

2.不可抢占 一个线程获取这个锁,只能等其他的线程主动释放

3.请求保持 持有a获取b

4.环路等待

这里1和2都不太容易破坏,只有3和4方便破坏

3可能是代码业务逻辑需求的

所以此时修改4是最合理的

此时想解决这个问题,提出几个思路

1.去掉一个哲学家

2.增加一支筷子

3.引入计数器,限制同时可以支持多少个人一起吃吃面

4.引入加锁的规则(较为常用,这里就可以控制获取筷子的顺序,此时给筷子排上编号,只能先获取编号小的筷子,此时2号获取了筷子1,以此类推,最后5获取了两个筷子,最后他结束了,其他线程/哲学家就可以吃到饭了)

5.银行家算法(太过复杂,一般不用)

3.内存可见性问题

老样子,先举个例子

public class ThreadDemo22 {
    private  static  int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
           while(flag == 0){
           }
            System.out.println("线程结束!");
        });
        Thread t2 = new Thread(()->{
            System.out.println("请输入一个数字");
            Scanner sc = new Scanner(System.in);
            flag = sc.nextInt();
        });
        t1.start();
        t2.start();
    }
}

此时我们想进行修改flag为任何值都发现是不成功的

这是因为,flag == 0这个操作分为两个指令

1.从内存中读取flag的值到寄存器中

2.读取完和0进行比较,然后进行一个跳转

在我们输入这个数字之前,其实这个while循环已经实现了很多次了

在这两个指令中,比较是没有多大开销的,然而从内存中加载的开销是比较大的

JVM认为这么多次这个变量始终没有修改,为了提高效率,直接把这个加载的动作直接优化掉了

其实可以理解为JVM的一个bug,此时我们可以使用sleep(n)让这个加载的频率降低,这样就不会优化了.但是治标不治本我们这里引入一个新的关键字volatile

用这个关键字来修饰flag就会让其强制读取内存,这样的结果就会更精确!!

网上还有一个说法就是将这里的内存(缓存)和寄存器的概念换成了"主存"和"工作内存"的概念,显得更严谨.

相关文章
|
26天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
26天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
26天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
28 2
|
26天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
31 1
|
26天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
34 1
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
44 1
C++ 多线程之初识多线程
|
26天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
25 1
|
2月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
46 6
|
1月前
|
存储 运维 NoSQL
Redis为什么最开始被设计成单线程而不是多线程
总之,Redis采用单线程设计是基于对系统特性的深刻洞察和权衡的结果。这种设计不仅保持了Redis的高性能,还确保了其代码的简洁性、可维护性以及部署的便捷性,使之成为众多应用场景下的首选数据存储解决方案。
41 1

热门文章

最新文章

相关实验场景

更多