javaEE初阶————多线程初阶(3)

news2025/2/8 21:54:27

大家新年快乐呀,今天是第三期啦,大家前几期的内容掌握的怎么样啦?

1,线程死锁 

1.1 构成死锁的场景

a)一个线程一把锁

这个在java中是不会发生的,因为我们之前讲的可重入机制,在其他语言中可能会发生的;

public static void main(String[] args) throws InterruptedException {

        Object locker = new Object();

        Thread t1 = new Thread(()->{
            synchronized (locker){
                synchronized (locker){
                    System.out.println(1111);
                }
            }
        });

        t1.start();
        t1.join();
        System.out.println("main");
    }

按理来说,t1线程刚进synchronized就获取到了锁对象,就要保持,进入第二个synchronized就要请求第一个锁对象,第一个要保持不给你锁对象,它让第二个先给他,第二个synchronized说你先给我我才有锁对象给你呀,它俩就这么一直僵持着,但是java有可重入机制不会发生这样的死锁的; 

b)两个线程两把锁

我们来模拟一个吃饺子的过程,小明小亮吃饺子,有酱油和醋对应两把锁,他们喜欢这两个东西一起加(我不喜欢),

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();//酱油
        Object locker2 = new Object();//醋

        Thread t1 = new Thread(()->{
            synchronized (locker1){
                System.out.println("获取到了酱油");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker2){
                    System.out.println("酱油和醋都是" + Thread.currentThread().getName() + "的啦");
                }
            }
        },"小明");

        Thread t2 = new Thread(()->{
            synchronized (locker2){
                System.out.println("获取到了醋");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker1){
                    System.out.println("酱油和醋都是" + Thread.currentThread().getName() + "的啦");
                }
            }
        },"小亮");

        t1.start();
        t2.start();
    }
}

我们来看运行结果

没有人获得酱油和醋,并且程序也没有正常停止,

 

这俩线程都因为锁竞争阻塞了,这就构成了死锁,我们加那个sleep是为了保证小亮拿醋,小明拿酱油之后再竞争互相的,不然可能就小明太快了直接全拿走了,或者小亮全拿走了; 

c)n个线程m把锁

一个很经典的模型,哲学家就餐问题:

1,哲学家可以放下筷子思考

2,哲学家拿筷子可以吃面条(没有洁癖)两根才能吃

但是哲学家都很固执,拿到了筷子是不会放手的,那么如果在当前这个图上每个人都想吃面条,每个人都到拿到了面前的筷子,要吃面条还需要一个筷子,他们就会想要别人的筷子,然而每个人都不会放开自己的筷子,你等我,我等你,最后大家都饿死,这就构成了死锁;但是按理来说这个模型出现这样的情况非常非常低那么中国10几亿人,这个概率就会无限放大,线程安全要做到完全没有危险的概率;

1.2 死锁的四个必要条件

a)互斥(基本特性)

一个线程获取到锁,其他线程再想获得这个锁就要阻塞等待;

b)不可剥夺(基本特性)

也可以叫不可抢占,如果线程1获取到了锁,线程2再想获取锁是不可以抢夺的,必须阻塞等待;

c)请求和保持

一个线程获取了锁1之后再不放弃锁1的前提下获取锁2;

d)循环等待

a等待b,b等待c,c等待d,d等待a;构成死锁循环;

1.3 如何避免死锁

我们刚才说的构成死锁的四种情况中,互斥和不可剥夺是锁的基本特性,我们是改变不了的,我们只能去改变(请求保持和循环等待);

a)打破请求和保持

请求和保持大概率是发生在嵌套中的,我们可以用并列来代替嵌套,但是通用性较低;

我们就拿刚才的吃饺子来举例子把;

public class Demo1 {
    public static void main(String[] args) {
        Object locker1 = new Object();//酱油
        Object locker2 = new Object();//醋
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                System.out.println("小明拿到酱油");
            }

            try {
                Thread.sleep(1111);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            synchronized (locker2){
                System.out.println("小明拿到醋");
            }
        },"小明");

        Thread t2 = new Thread(()->{
            synchronized (locker1){
                System.out.println("小亮拿到酱油");
            }

            try {
                Thread.sleep(1111);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            synchronized (locker2){
                System.out.println("小亮拿到醋");
            }
        },"小亮");

        t1.start();
        t2.start();

    }
}

并列锁,虽然没有构成死锁,但是违背了我们的想法就是让小明和小亮获得两个锁,刚才说的通用性不强也是在这里; 

b)打破循环等待

第二个方法,改变加锁的顺序,我们还有吃饺子的例子,但是这次要拿到两个锁:

public class Demo2 {
    public static void main(String[] args) {
        Object locker1 = new Object();//酱油
        Object locker2 = new Object();//醋
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                System.out.println("小明拿到酱油啦");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker2){
                    System.out.println("小明拿到醋和酱油啦");
                }
            }
        },"小明");

        Thread t2 = new Thread(()->{
            synchronized (locker1){
                System.out.println("小亮拿到酱油啦");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker2){
                    System.out.println("小亮拿到醋和酱油啦");
                }
            }
        },"小亮");

        t1.start();
        t2.start();
        
    }
}

我们改变了加锁的顺序,

 

也是能避免死锁问题的;

———————————————————————————————————————————

2,内存可见性

这也是导致线程安全的问题之一

我们来写一个例子嗷:
 

import java.util.Scanner;

public class Demo3 {
    static int i = 0;
    public static void main(String[] args) {
        Object obj = new Object();


        Thread t1 = new Thread(()->{
            while(i==0){

            }
            System.out.println("结束");
        });

        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            Scanner scanner = new Scanner(System.in);
            synchronized (obj){
                i = scanner.nextInt();
            }
        });
        t1.start();
        t2.start();
    }
}

我们来看这个代码,t1线程根据i的数值一直循环直到i的值被t2线程修改才停止,事实是这样的吗。我们来试试,

无法暂停,这是为啥:

这就是因为内存可见性问题,程序员的水平参差不齐,java大佬为了照顾我们这样的小卡拉米,就弄了个编译器优化,所以我们写的代码并不会直接执行,我们刚才写的while(i==0)这段代码,我们要等待t2线程来修改i,可能我们就用了几秒的时间但对于t1线程,这这边是沧海桑田,万物轮回,谁还记得什么t1呀它等于0就得了,再底层一点解释呢,就是有“工作内存” 和 “主内存”我们应该是从主内存中拿到数据,放到工作内存中,再从工作内存放回主内存,但是这么一直一直重复,去主内存的时间开销是工作内存的几千倍,编译器就不去主内存了直接去工作内存中拿数据,但是后期修改了主内存,然而此处代码已经完全忽略主内存了,就无法修改了;这就是内存可见性问题那么怎么避免呢?

———————————————————————————————————————————

3,volatile 关键字

我们可以使用volatile关键字避免内存可见性问题;

3.1 volatile 能保证内存可见性

import java.util.Scanner;

public class Demo3 {
    volatile static int i = 0;
    public static void main(String[] args) {

        Object obj = new Object();


        Thread t1 = new Thread(()->{
            while(i==0){

            }
            System.out.println("结束");
        });

        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            Scanner scanner = new Scanner(System.in);
            synchronized (obj){
                i = scanner.nextInt();
            }
        });
        t1.start();
        t2.start();
    }
}

看,解决了吧,就加了一个volatile;

3.2 volatile 不能保证原子性

还记得原子性吗,就是这个操作在底层是不是原子的,是不是分几步,再多线程中会影响到这个操作,我们拿之前那个两个线程修改一个整形:

public class Demo4 {
    volatile static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for(int i=0;i<100000;i++){
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for(int i=0;i<100000;i++){
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final count: " + count);
    }
}

所有我们只有使用锁才行;

———————————————————————————————————————————

4,wait 和 notify

这是个什么玩意,线程不是随机调度,抢占式执行的吗,我们可以用这个玩意稍加限制,协调线程之间的逻辑顺序;

4.1 wait() 方法

这个东西跟锁和sleep不一样,都是等待,但是是有区别的,wait()是等的时候会释放锁,被唤醒再拿到锁而sleep这个byd它抱着锁睡,............锁的话就是阻塞等待嘛,

我们来试试wait()方法是搭配锁来使用的,最好还要加while循环

public class Demo5 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("Thread 1");
            synchronized (locker){
                System.out.println(1000);
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(2000);
            }
        });

        t1.start();
    }
}

 我们来看看运行结果

死等,因为没有东西能够唤醒wait();

我们可以设置超时时间,也可以使用notify方法;

public class Demo5 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("Thread 1");
            synchronized (locker){
                System.out.println(1000);
                try {
                    locker.wait(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(3000);
            }
        });

        t1.start();
    }
}

这样代码会在2秒后打印3000;

4.2 notify() 方法

用来唤醒wait()注意这些都是搭配锁对象来用的;

对于notify,如果存在多个使用同一个锁对象的wait,它没有规律,会随机唤醒一个wait

public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();

        Thread t1 = new Thread(()->{
            System.out.println("Thread 1");
            synchronized (locker){
                System.out.println("线程1获得锁");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程1释放锁");
            }
        });
        Thread t2 = new Thread(()->{
            System.out.println("Thread 2");
            synchronized (locker){
                System.out.println("线程2获得锁");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程2释放锁");
            }
        });

        t1.start();
        t2.start();
        Thread.sleep(1000);
        synchronized (locker){
            locker.notify();
        }

    }
}

 线程1成功释放了锁,说明notify唤醒了线程1的waite

我们再试试

4.3 notifyAll() 方法

这个就是全部释放

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();

        Thread t1 = new Thread(()->{
            System.out.println("Thread 1");
            synchronized (locker){
                System.out.println("线程1获得锁");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程1释放锁");
            }
        });
        Thread t2 = new Thread(()->{
            System.out.println("Thread 2");
            synchronized (locker){
                System.out.println("线程2获得锁");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程2释放锁");
            }
        });

        t1.start();
        t2.start();
        Thread.sleep(1000);
        synchronized (locker){
            locker.notifyAll();
        }

    }
}

完美

4.4 wait 和 sleep的对比

这个没啥好说的了,wait先加锁,到。wait()操作的时候释放锁,唤醒的时候再拿着锁,而sleep纯抱着锁睡,还会被interrupt唤醒,说实话抱着锁睡听不好的,可能会有很多线程都等着它很浪费时间的,sleep会释放Cpu的资源,不再占用了;就这样吧,大家加油,等我更新下一期;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2294955.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Flink快速入门-1.Flink 简介与环境配置】

Flink 简介与环境配置 实验介绍 在学习一门新的技术之前&#xff0c;我们首先要了解它的历史渊源&#xff0c;也就是说它为什么会出现&#xff0c;它能够解决什么业务痛点。所以本节我们的学习目的是了解 Flink 的背景&#xff0c;并运行第一个 Flink 程序&#xff0c;对它有…

WPF基础 | 初探 WPF:理解其核心架构与开发环境搭建

WPF基础 | 初探 WPF&#xff1a;理解其核心架构与开发环境搭建 一、前言二、WPF 核心架构2.1 核心组件2.2 布局系统2.3 数据绑定机制2.4 事件处理机制 三、WPF 开发环境搭建3.1 安装 Visual Studio3.2 创建第一个 WPF 应用程序 结束语优质源码分享 WPF基础 | 初探 WPF&#xff…

JVM 四虚拟机栈

虚拟机栈出现的背景 由于跨平台性的设计&#xff0c;Java的指令都是根据栈来设计的。不同平台CPU架构不同&#xff0c;所以不能设计为基于寄存器的。优点是跨平台&#xff0c;指令集小&#xff0c;编译器容易实现&#xff0c;缺点是性能下降&#xff0c;实现同样的功能需要更多…

深入理解小波变换:信号处理的强大工具

引言 在科学与工程领域&#xff0c;信号处理一直是关键环节&#xff0c;傅里叶变换与小波变换作为重要的分析工具&#xff0c;在其中发挥着重要作用。本文将深入探讨小波变换&#xff0c;阐述其原理、优势以及与傅里叶变换的对比&#xff0c;并通过具体案例展示其应用价值。 一…

【大数据技术】搭建完全分布式高可用大数据集群(Kafka)

搭建完全分布式高可用大数据集群(Kafka) kafka_2.13-3.9.0.tgz注:请在阅读本篇文章前,将以上资源下载下来。 写在前面 本文主要介绍搭建完全分布式高可用集群 Kafka 的详细步骤。 注意: 统一约定将软件安装包存放于虚拟机的/software目录下,软件安装至/opt目录下。 安…

关于ESP-IDF 5.4 中添加第三方组件esp32-camera找不到文件,编译错误解决办法(花了一天时间解决)

最近需要使用ESP32-S3-CAM 的OV2640摄像头采集图像&#xff0c;为了加速开发进度&#xff0c;于是选择了esp32-camera组件&#xff0c;该组件不是官方组件&#xff0c;需要自己git clone。但在为项目添加esp32-camera组件时&#xff0c;一直编译错误&#xff0c;找不到头文件&a…

Android LifecycleOwner 闪退,java 继承、多态特性!

1. 闪退 同意隐私政策后&#xff0c;启动进入游戏 Activity 闪退 getLifecycle NullPointerException 空指针异常 FATAL EXCEPTION: main Process: com.primer.aa.gg, PID: 15722 java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.primer.aa.…

[LeetCode]day16 242.有效的字母异位词

242. 有效的字母异位词 - 力扣&#xff08;LeetCode&#xff09; 题目描述 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的 字母异位词 示例 1: 输入: s "anagram", t "nagaram" 输出: true示例 2: 输入: s "rat"…

基于SpringBoot养老院平台系统功能实现五

一、前言介绍&#xff1a; 1.1 项目摘要 随着全球人口老龄化的不断加剧&#xff0c;养老服务需求日益增长。特别是在中国&#xff0c;随着经济的快速发展和人民生活水平的提高&#xff0c;老年人口数量不断增加&#xff0c;对养老服务的质量和效率提出了更高的要求。传统的养…

【3分钟极速部署】在本地快速部署deepseek

第一步&#xff0c;找到网站&#xff0c;下载&#xff1a; 首先找到Ollama &#xff0c; 根据自己的电脑下载对应的版本 。 我个人用的是Windows 我就先尝试用Windows版本了 &#xff0c;文件不是很大&#xff0c;下载也比较的快 第二部就是安装了 &#xff1a; 安装完成后提示…

Linux ftrace 内核跟踪入门

文章目录 ftrace介绍开启ftraceftrace使用ftrace跟踪指定内核函数ftrace跟踪指定pid ftrace原理ftrace与stracetrace-cmd 工具KernelShark参考 ftrace介绍 Ftrace is an internal tracer designed to help out developers and designers of systems to find what is going on i…

[Day 16]螺旋遍历二维数组

今天我们看一下力扣上的这个题目&#xff1a;146.螺旋遍历二维数组 题目描述&#xff1a; 给定一个二维数组 array&#xff0c;请返回「螺旋遍历」该数组的结果。 螺旋遍历&#xff1a;从左上角开始&#xff0c;按照 向右、向下、向左、向上 的顺序 依次 提取元素&#xff0c…

【教程】docker升级镜像

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 目录 自动升级 手动升级 无论哪种方式&#xff0c;最重要的是一定要通过-v参数做数据的持久化&#xff01; 自动升级 使用watchtower&#xff0c;可…

使用jmeter进行压力测试

使用jmeter进行压力测试 jmeter安装 官网安装包下载&#xff0c;选择二进制文件&#xff0c;解压。 tar -xzvf apache-jmeter-x.tgz依赖jdk安装。 yum install java-1.8.0-openjdk环境变量配置&#xff0c;修改/etc/profile文件&#xff0c;添加以下内容。 export JMETER/…

链表和 list

一、单链表的模拟实现 1.实现方式 链表的实现方式分为动态实现和静态实现两种。 动态实现是通过 new 申请结点&#xff0c;然后通过 delete 释放结点的形式构造链表。这种实现方式最能体 现链表的特性&#xff1b; 静态实现是利用两个数组配合来模拟链表。一个表示数据域&am…

【AI大模型】Ubuntu18.04安装deepseek-r1模型+服务器部署+内网访问

以下内容主要参考博文&#xff1a;DeepSeek火爆全网&#xff0c;官网宕机&#xff1f;本地部署一个随便玩「LLM探索」 - 程序设计实验室 - 博客园 安装 ollama Download Ollama on Linux curl -fsSL https://ollama.com/install.sh | sh 配置 ollama 监听地址 ollama 安装后…

cmd执行mysql命令

安装mysql之后如果想使用cmd执行mysql命令&#xff0c;需要怎么操作呢&#xff0c;下面一起看一下。 安装mysql之后&#xff0c;如果直接去cmd窗口执行MySQL命令&#xff0c;窗口可能会提示mysql不是可执行命令。 需要配置系统的环境变量&#xff0c;将mysql的安装路径配置系…

网络安全威胁框架与入侵分析模型概述

引言 “网络安全攻防的本质是人与人之间的对抗&#xff0c;每一次入侵背后都有一个实体&#xff08;个人或组织&#xff09;”。这一经典观点概括了网络攻防的深层本质。无论是APT&#xff08;高级持续性威胁&#xff09;攻击、零日漏洞利用&#xff0c;还是简单的钓鱼攻击&am…

详细教程 | 如何使用DolphinScheduler调度Flink实时任务

Apache DolphinScheduler 非常适用于实时数据处理场景&#xff0c;尤其是与 Apache Flink 的集成。DolphinScheduler 提供了丰富的功能&#xff0c;包括任务依赖管理、动态调度、实时监控和日志管理&#xff0c;能够有效简化 Flink 实时任务的管理和部署。通过 DolphinSchedule…

【通俗易懂说模型】线性回归(附深度学习、机器学习发展史)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;深度学习_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前言 2. …