Java【多线程】(2)线程属性与线程安全

news2025/3/4 18:43:27


目录

1.前言

2.正文

2.1线程的进阶实现

2.2线程的核心属性

2.3线程安全

2.3.1线程安全问题的原因

2.3.2加锁和互斥

2.3.3可重入(如何自己实现可重入锁)

2.4.4死锁(三种情况)

2.4.4.1第一种情况

2.4.4.2第二种情况

2.4.4.3第三种情况

2.4.5避免死锁

3.小结


1.前言

哈喽大家好吖,今天继续来给大家分享线程相关的内容,介绍一部分线程的核心属性,后一部分主要为线程安全部分,当然一篇博文无法讲解完全,会在后续接着为大家讲解。

2.正文


2.1线程的进阶实现

上一篇关于线程的博文我们通过Thread类或实现Runnable接口来达到了多线程的实现,接下来给大家一个最推荐的实现方式:lambda表达式实现。

Thread类的构造函数接受一个Runnable接口类型的参数,而Runnable接口有一个run方法。因此,我们可以通过lambda表达式来实现这个接口,并将其传递给Thread构造器。

public class test {
    public static void main(String[] args) {
        // 使用lambda表达式创建线程
        Thread thread = new Thread(() -> {
            // 线程执行的代码
            for (int i = 0; i < 5; i++) {
                System.out.println("线程正在运行: " + i);
                try {
                    Thread.sleep(1000); // 模拟线程工作1秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();  // 启动线程
    }
}

详解

  • Runnable接口Runnable接口包含一个run方法,定义了线程要执行的任务。
  • Lambda表达式()->{}部分是Lambda表达式,它实现了Runnable接口的run方法。这个方法中包含了线程要执行的代码。
  • Thread对象:使用Thread类创建一个新线程,并传入Runnable的实现(即Lambda表达式)。
  • thread.start():调用start()方法来启动线程。线程开始执行Lambda表达式中的run方法。

2.2线程的核心属性

线程有不同的生命周期状态,主要包括以下几种:

  • NEW:线程被创建,但还未启动。
  • RUNNABLE:线程正在执行或等待操作系统分配CPU时间片。就绪状态分为俩种:
  1. 随时可以到cpu上去工作。
  2. 在cpu上正在工作。
  • BLOCKED:线程因为竞争资源(如同步锁)而被阻塞,无法执行。
  • WAITING:线程正在等待另一个线程的通知。
  • TIMED_WAITING:线程正在等待一个特定的时间段,直到超时或被唤醒。(例如线程的join方法会使线程进入此状态)
  • TERMINATED:线程执行完毕,已终止。

附上别的大佬总结很详细的图片。 


2.3线程安全

再将这个板块之前,先给大家一个案例来引入线程安全这个概念。我们当下有这么一个场景:

public class demo2 {
    public static int count = 0;
    public static void main(String[] args) {
        
        Thread t1 = new Thread(()->{
            for (int i = 0;i < 500;i++){
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0;i < 500;i++){
                count++;
            }
        });
        t1.start();
        t2.start();
        System.out.println(count);
    }
}

我们可以看到,我们希望通过俩个线程来完成count自增到1000的操作,打没输出结果并不是我们想要的。

原因是线程刚启动,可能还没有分配到cpu上开始执行,count便被打印出来。

我们这样处理后:

public class demo2 {
    public static int count = 0;
    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            for (int i = 0;i < 500;i++){
                count++;
            }

        });
        Thread t2 = new Thread(()->{
            for (int i = 0;i < 500;i++){
                count++;
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

发现可以出来希望的结果:

那如果我们只让一个线程加上join呢?会发现结果开始变得随机起来:

因此我们可以知道,上述有线程产生的“bug”即没有输出想要的结果,就被称为线程安全问题,相反,如果在多线程并发的情况下,输出理想结果就叫做“线程安全”。

2.3.1线程安全问题的原因

  1. 【根因】随即调度,抢占执行(上文例子就是如此)
  2. 多个线程同时修改一个变量
  3. 修改操作不是原子性的(意思是某些操作如count++是由多个线程组成完成的)
  4. 内存可见性(意思是某些变量的访问不一定直接访问到内存,而是有可能访问到寄存器当中)
  5. 不当锁的使用(下文细讲)

2.3.2加锁和互斥

如何处理这些线程安全问题呢,这里我们要引入加锁的概念与synchronized关键字。

加锁是一种同步机制,用于控制多个线程访问共享资源的顺序。
当一个线程获得了锁时,其它线程必须等待该线程释放锁后才能继续访问共享资源。

加锁的特点:

  1. 串行化访问
    • 同一时刻只有一个线程可以访问被加锁的资源。
  2. 防止数据竞争
    • 确保共享资源的操作是原子性的(不会被其他线程中断)。
  3. 提升数据一致性
    • 确保共享资源不会因为多个线程同时操作而引发不一致问题。

加锁的过程:

  • 加锁(Locking): 一个线程试图获取资源的锁,若获取成功,进入临界区;若失败,则阻塞或等待。
  • 解锁(Unlocking): 线程释放锁,允许其他线程获取锁并继续执行

互斥(Mutual Exclusion,缩写为 Mutex)是加锁的目的之一,强调同一时刻只能有一个线程访问某个共享资源,达到线程之间的互斥访问


如何实现加锁呢,继续拿上文来举例子:

public class demo2 {
    private int count = 0;

    // 同步实例方法
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        demo2 demo = new demo2();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                demo.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                demo.increment();
            }
        });

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + demo.getCount());
    }
}

运行结果:

2.3.3可重入(如何自己实现可重入锁)

什么叫可重入呢,我们用一段代码来引入这个概念:

class Counter {
    private int count = 0;

    public void add() {
        synchronized (this) {
            count++;//第一次加锁
        }
    }

    public int get() {
        return count;
    }
}
public class demo3 {

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for(int i = 0;i < 100;i++){
                synchronized (counter){
                    counter.add();//第二次加锁
                }
            }
        });
        t1.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("count = " + counter.get());
    }
}

上面代码我们可以看到(如果没有可重入这个概念):

  1. 第一次加锁操作,能够成功(锁没人使用)。
  2. 第二次进行加锁,此时意味着,锁对象已经是被占用的状态,第二次加锁就会出现阻塞等待。

要想解除阻塞,只能往下执行才可以,要想往下执行,就需要等到第一次锁被释放,这样就叫做出现了死锁。


为了解决上述问题,Java中的synchronized引入了可重入的概念:

可重入锁是一种允许同一线程多次获取同一把锁的同步机制,解决了嵌套调用或递归场景下线程自我阻塞的问题,是避免死锁的重要设计。

所以多个锁递归,只有最外层的锁涉及真正的加锁与解锁

那我们如何自己实现一个可重入锁呢,抓住下面核心就有头绪了:

可重入锁的核心机制

  • 锁计数器

    • 每个锁对象内部维护一个计数器,记录被同一线程获取的次数。

    • 首次获取锁时计数器=1,每次重入加1,释放时减1,归零后其他线程可竞争锁。

  • 持有线程标识

    • 锁对象记录当前持有锁的线程,确保仅持有线程可重入。

下面附上示例:

public class MyLock {
    private Thread ownerThread;  // 当前持有锁的线程
    private int lockCount = 0;   // 锁计数器
    
    // 获取锁
    public synchronized void lock() throws InterruptedException {
        Thread currentThread = Thread.currentThread();
        // 若锁已被其他线程持有,则当前线程等待
        while (ownerThread != null && ownerThread != currentThread) {
            wait();
        }
        // 锁未被持有或当前线程重入,更新计数器和持有线程
        ownerThread = currentThread;
        lockCount++;
    }
    
    // 释放锁
    public synchronized void unlock() {
        Thread currentThread = Thread.currentThread();
        // 只有持有锁的线程可以释放锁
        if (ownerThread != currentThread) {
            throw new IllegalMonitorStateException("当前线程未持有锁!");
        }
        lockCount--;
        // 锁计数器归零时完全释放锁
        if (lockCount == 0) {
            ownerThread = null;
            notify(); // 唤醒一个等待线程
        }
    }
}

2.4.4死锁(三种情况)

2.4.4.1第一种情况

一个线程,一个锁,被加锁多次。想必这个上文刚讲过,就不多言了,着重讲后文。

2.4.4.2第二种情况

两个线程,两个锁,互相尝试获得对方的锁。可能直接这样讲不是很好懂,附上代码与注释就可以了:

public class Demo20 {
    public static void main(String[] args) throws InterruptedException {
        // 创建两个锁对象,用于线程同步
        Object locker1 = new Object();
        Object locker2 = new Object();

        // 创建线程 t1
        Thread t1 = new Thread(() -> {
            // 获取 locker1 的锁
            synchronized (locker1) {
                try {
                    // 线程休眠 1 秒,模拟耗时操作
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 如果线程被中断,抛出异常
                    throw new RuntimeException(e);
                }
                // 尝试获取 locker2 的锁
                synchronized (locker2) {
                    // 如果成功获取到 locker2 的锁,打印消息
                    System.out.println("t1 线程两个锁都获取到");
                }
            }
        });

        // 创建线程 t2
        Thread t2 = new Thread(() -> {
            // 获取 locker1 的锁
            synchronized (locker1) {
                try {
                    // 线程休眠 1 秒,模拟耗时操作
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 如果线程被中断,抛出异常
                    throw new RuntimeException(e);
                }
                // 尝试获取 locker2 的锁
                synchronized (locker2) {
                    // 如果成功获取到 locker2 的锁,打印消息
                    System.out.println("t2 线程两个锁都获取到");
                }
            }
        });

        // 启动线程 t1 和 t2
        t1.start();
        t2.start();

        // 主线程等待 t1 和 t2 执行完毕
        t1.join();
        t2.join();
    }
}
  1. 线程 t1

    • 先获取 locker1 的锁,然后休眠 1 秒。

    • 接着尝试获取 locker2 的锁。

  2. 线程 t2

    • 同样先获取 locker1 的锁,然后休眠 1 秒。

    • 接着尝试获取 locker2 的锁。

  3. 问题:此时死锁就出现了

    • t1 持有 locker1 并等待 locker2

    • t2 持有 locker1 并等待 locker2

    • 两个线程互相等待对方释放锁,导致程序无法继续执行。

2.4.4.3第三种情况

死锁的第三种情况,即n个线程和m把锁,这里就要引入一个很著名的问题,哲学家就餐问题:

哲学家就餐问题(Dining Philosophers Problem) 是计算机科学中经典的同步与死锁问题,由 Edsger Dijkstra 提出,用于演示多线程环境中的资源竞争和死锁风险。


1. 问题描述

  • 场景:5 位哲学家围坐在圆桌旁,每人面前有一碗饭,相邻两人之间放一支筷子(共 5 支筷子)。

  • 行为

    • 哲学家交替进行 思考 和 就餐

    • 就餐时需要 同时拿起左右两边的筷子

    • 完成就餐后放下筷子,继续思考。

  • 核心问题:如何设计算法,使得所有哲学家都能公平、高效地就餐,且避免死锁。


2. 死锁的产生

如果所有哲学家 同时拿起左边的筷子,会发生以下情况:

  1. 每个哲学家都持有左边的筷子,等待右边的筷子。

  2. 右边的筷子被其他哲学家持有,形成 循环等待

  3. 所有哲学家无法继续,导致 死锁


3. 解决思路

  • 核心思想:为所有资源(筷子)定义一个全局顺序,要求哲学家必须按固定顺序获取资源。

  • 实现方式

    1. 将筷子编号为 0 到 4。

    2. 每位哲学家必须先拿编号较小的筷子,再拿编号较大的筷子。

  • 效果

    • 破坏循环等待条件(不可能所有人同时等待右侧筷子)。

    • 保证至少一位哲学家可以拿到两只筷子。

2.4.5避免死锁

上述讲完了死锁出现的场景,这里可以总结死锁出现的四个必要条件:

  1. 锁是互斥的。(一个线程拿到锁之后,另一个线程再尝试获取锁,必须要阻塞等待)
  2. 锁是不可抢占的。(即线程1拿到锁, 线程2也尝试获取这个锁,线程2 必须阻塞等待2而不是线程2直接把锁抢过来)
  3. 请求和保持。(一个线程拿到锁1之后,不释放锁1 的前提下,获取锁2)
  4. 循环等待。(多个线程, 多把锁之间的等待过程,构成了"循环",即A 等待 B, B 也等待 A 或者 A 等待 B,B 等待 C,C等待 A)

既然我们知道死锁是如何产生的,那么解决死锁的思路就有啦:

  1. 打破3条件,可以把嵌套的锁改成并列的锁。
  2. 打破4条件,加锁的顺序进行约定。

3.小结

今天的分享到这里就结束了,喜欢的小伙伴不要忘记点点赞点个关注,你的鼓励就是对我最大的支持,加油!

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

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

相关文章

vue These dependencies were not found

These dependencies were not found: * vxe-table in ./src/main.js * vxe-table/lib/style.css in ./src/main.js To install them, you can run: npm install --save vxe-table vxe-table/lib/style.css 解决&#xff1a; nodejs执行以下语句 npm install --save vxe-t…

Yak 在 AI 浪潮中应该如何存活?

MCP 是 Claude 发起的一个协议&#xff0c;在2024年10月左右发布&#xff0c;在2025年2月开始逐步有大批量的 AI 应用体开始支持这个协议。这个协议目的是让 AI 同时可以感知有什么工具可以用&#xff0c;如果要调用这些工具的话&#xff0c;应该是用什么样的方式。 这个 MCP 协…

AI是否能真正理解人类情感?从语音助手到情感机器人

引言&#xff1a;AI与情感的交集 在过去的几十年里&#xff0c;人工智能&#xff08;AI&#xff09;的发展速度令人惊叹&#xff0c;从简单的语音识别到如今的深度学习和情感计算&#xff0c;AI已经深入到我们生活的方方面面。尤其是在语音助手和情感机器人领域&#xff0c;AI不…

大语言模型学习--本地部署DeepSeek

本地部署一个DeepSeek大语言模型 研究学习一下。 本地快速部署大模型的一个工具 先根据操作系统版本下载Ollama客户端 1.Ollama安装 ollama是一个开源的大型语言模型&#xff08;LLM&#xff09;本地化部署与管理工具&#xff0c;旨在简化在本地计算机上运行和管理大语言模型…

linux上面安装 向量数据库 mlivus和 可视化面板Attu

1. 确保docker(docker 19.0以上即可) 和 docker-compose&#xff08;V2.2.2以上&#xff09; 都已安装 2. 创建milvus工作目录 # 新建一个名为milvus的目录用于存放数据 目录名称可以自定义 mkdir milvus# 进入到新建的目录 cd milvus 3. 下载并编辑docker-compose.yml 在下载…

用工厂函数简化redis配置

工厂函数&#xff08;Factory Function&#xff09;不同于构造函数&#xff0c;工厂函数就是一个普通函数&#xff0c;通常用于创建对象或实例。它的核心思想是通过一个函数来封装对象的创建逻辑&#xff0c;而不是直接使用类的构造函数。工厂函数可以根据输入参数动态地决定创…

类和对象-继承-C++

1.定义 面向对象的三大特征之一&#xff0c;为了减少重复的代码 2.语法 class 子类 &#xff1a;继承方式 父类 &#xff08;子类也叫派生类&#xff0c;父类也称为基类&#xff09; 例&#xff1a;class age&#xff1a;public person&#xff1b; #include<iostrea…

使用Maven搭建Spring Boot框架

文章目录 前言1.环境准备2.创建SpringBoot项目3.配置Maven3.1 pom.xml文件3.2 添加其他依赖 4. 编写代码4.1 启动类4.2 控制器4.3 配置文件 5.运行项目6.打包与部署6.1 打包6.2 运行JAR文件 7.总结 前言 Spring Boot 是一个用于快速构建 Spring 应用程序的框架&#xff0c;它简…

RockyLinux 为 k8s 集群做准备

1.准备VM 镜像 开启虚拟机 选择安装 Rocky linux 9.5 软件选择最小安装就可以了 在 rocky 9 以后版本中 他全部 采用 network manager 去替换老的 network 去实现网络的管理 1.网卡配置 cat /etc/NetworkManager/system-connections/ens160.nmconnection 我们配置了两块网…

二十三种设计模式

2 工厂方法模式 工厂模式&#xff08;Factory Pattern&#xff09;是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。 在工厂模式中&#xff0c;我们在创建对象时不会对客户端暴露创建逻辑&#xff0c;并且是通…

SqlSugar 语法糖推荐方式

//方式1&#xff1a;var dd _repository._Db.Queryable<ConfigAggregateRoot, UserRoleEntity>((o, p) > o.Id p.Id).Select((o, p) > new{o.Id,o.Remark,p.RoleId,});//方式2&#xff1a;不推荐使用&#xff0c;建议优先使用 Lambda 表达式&#xff0c;因为它更…

SQL 全面指南:从基础语法到高级查询与权限控制

SQL&#xff1a;全称 Structured Query Language&#xff0c;结构化查询语言。操作关系型数据库的编程语言&#xff0c;定义了一套操作关系型数据库统一标准 。 一、SQL通用语法 在学习具体的SQL语句之前&#xff0c;先来了解一下SQL语言的同于语法。 1). SQL语句可以单行或多…

Spring Cloud Gateway 网关的使用

在之前的学习中&#xff0c;所有的微服务接口都是对外开放的&#xff0c;这就意味着用户可以直接访问&#xff0c;为了保证对外服务的安全性&#xff0c;服务端实现的微服务接口都带有一定的权限校验机制&#xff0c;但是由于使用了微服务&#xff0c;就需要每一个服务都进行一…

JavaWeb-jdk17安装

下载jdk17 地址&#xff1a;https://www.oracle.com/java/technologies/downloads/#jdk17-windows 安装jdk 配置环境变量 右键点击我的电脑>属性>高级系统设置>环境变量 在系统变量Path变量中添加 测试 java -version javac -version

【银河麒麟高级服务器操作系统】服务器测试业务耗时问题分析及处理全流程分享

更多银河麒麟操作系统产品及技术讨论&#xff0c;欢迎加入银河麒麟操作系统官方论坛 https://forum.kylinos.cn 了解更多银河麒麟操作系统全新产品&#xff0c;请点击访问 麒麟软件产品专区&#xff1a;https://product.kylinos.cn 开发者专区&#xff1a;https://developer…

算法1-4 蜜蜂路线

题目描述 一只蜜蜂在下图所示的数字蜂房上爬动,已知它只能从标号小的蜂房爬到标号大的相邻蜂房,现在问你&#xff1a;蜜蜂从蜂房 m 开始爬到蜂房 n&#xff0c;m<n&#xff0c;有多少种爬行路线&#xff1f;&#xff08;备注&#xff1a;题面有误&#xff0c;右上角应为 n−…

Android 常见View的防抖

在开发Android应用时&#xff0c;我们经常会遇到用户快速点击按钮或者频繁触发某个事件的情况。这种行为可能会导致不必要的重复操作&#xff0c;例如多次提交表单、重复加载数据等。为了避免这些问题&#xff0c;我们需要对这些事件进行防抖处理。本文将详细介绍如何在Kotlin中…

数据库原理SQL查询(习题+知识点)

一、查询学生表所有学生记录 1.题目内容代码编写 select * from stu; 2.知识点提醒 1&#xff09;选择表中的所有属性列有两种方法 在select关键字后列出所有列名若列的显示顺序与其在表中的顺序相同&#xff0c;则也可用 * 表示所有列 二、查询学生表中部分信息 1.题目内…

安路FPGA开发入门:软件安装与点灯与仿真(TangDynasty ModelSim)

文章目录 前言软件安装开发软件仿真软件 点灯测试代码编写与编译引脚分配固件下载 仿真测试ModelSim添加仿真库TangDynasty仿真设置进行仿真 后记 前言 最近因为工作需要用安路的FPGA&#xff0c;这里对安路FPGA开发相关流程做个记录。作为测试只需要一个核心板&#xff08;我这…

浅克隆与深克隆区别

package d12_api_object;public class Test2 {public static void main(String[] args) throws CloneNotSupportedException {//目标&#xff1a;掌握Object类提供的对象克隆方法//1、protected Object clone():对象克隆User u1 new User(1,"min","1120",…