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


目录

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();// 创建线程 t1Thread 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 线程两个锁都获取到");}}});// 创建线程 t2Thread 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 和 t2t1.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.rhkb.cn/news/27677.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[IP] DDR_FIFO(DDR3 用户FIFO接口)

IP(DDR_FIFO)将DDR3 IP的用户侧复杂接口修改为简易的FIFO接口&#xff0c;用户侧更加简易例化使用MIG 核 IP介绍 c0_xx (连接DDR app接口) 此IP 仅需根据MIG配置进行有限修改&#xff0c;即可使用&#xff01; 关于IP详细使用说明&#xff0c;参考IP datasheet&#xff01; 示…

USRP6330-通用软件无线电平台

1、产品描述 USRP6330平台以XILINX XCZU15EG SOC处理器为核心&#xff0c;搭配两片ADI ADRV9026射频集成芯片&#xff0c;提供了瞬时带宽高达200MHz的8收8发射频通道。通过驯服的高精度GPSDO时钟参考方案&#xff0c;USRP可以支持高性能的MIMO通信系统&#xff0c;提供了部署大…

P8615 [蓝桥杯 2014 国 C] 拼接平方数--substr、to_string、stoi

P8615 [蓝桥杯 2014 国 C] 拼接平方数--substr、to_string、stoi 题目 解析介绍一下这三个函数&#xff1a;1、to_string&#xff08;&#xff09;&#xff1a;2、stoi&#xff08;&#xff09;&#xff1a;3、substr&#xff08;&#xff09;&#xff1a;代码 题目 解析 首先…

一键安装Nginx部署脚本之Linux在线安装Nginx,脚本化自动化执行服务器部署(附执行脚本下载)

相关链接 一键安装Nginx部署脚本之Linux在线安装Nginx一键安装Redis部署脚本之Linux在线安装Redis一键安装Mysql部署脚本之Linux在线安装Mysql一键安装JAVA部署脚本之Linux在线安装JDKXshell客户端免费版无需注册XFtp客户端免费版无需注册 前言 简化服务器部署操作&#xff0…

反向代理以及其使用场景

一、反向代理概念 反向代理(Reverse Proxy)是一种服务器配置,它将客户端的请求转发给内部的另一台或多台服务器处理,然后将响应返回给客户端。与正向代理(Forward Proxy)不同,正向代理是客户端的代理,客户端将请求发送到代理服务器,再由代理服务器访问目标服务器;而…

Linux网络 NAT、代理服务、内网穿透

NAT 技术 IPv4 协议中存在 IP 地址数量不充足的问题&#xff0c;而 NAT 技术是当前解决 IP 地址不够用的主要手段 , 是路由器的一个重要功能。NAT 能够将私有 IP 对外通信时转为全局 IP&#xff0c;也就是就是一种将私有 IP 和全局 IP 相互转化的技术方法。 这可以让很多学…

广义线性模型下的数据分析(R语言)

一、实验目的&#xff1a; 通过上机试验&#xff0c;掌握利用R实现线性回归分析、逻辑回归、列联分析及方差分析&#xff0c;并能对分析结果进行解读。 数据&#xff1a; 链接: https://pan.baidu.com/s/1JqZ_KbZJEk-pqSUWKwOFEw 提取码: hxts 二、实验内容&#xff1a; 1、2…

Windows环境下Maven的配置

Windows环境下Maven的配置 一、Maven下载 Maven官网地址 apache-maven-3.8.8-bin.zip 二、安装和配置 解压到本地目录&#xff0c;例如&#xff1a;D:\software\apache-maven-3.8.8 新建变量MAVEN_HOMED:\software\apache-maven-3.8.8&#xff08;以自己的安装路径为准&…

Spring MVC 处理请求

目录 1、SpringMVC 处理请求1.1、HTTP 请求报文1.2、获取 URL 中参数 PathVariable1.3、获取请求头数据1.3.1、传统获取 Header/Cookie1.3.2、获取 Header—RequestHeader1.3.3、获取 Cookie—CookieValue1.3.4、Session 的存储和获取—SessionAttribute 1.4、获取请求数据1.4.…

OpenAI 最后一代非推理模型:OpenAI 发布 GPT-4.5预览版

最后一代非推理大模型 在人工智能领域&#xff0c;OpenAI 一直以其创新的技术和卓越的产品引领着行业的发展。近期&#xff0c;OpenAI 正式发布了 GPT-4.5 研究预览版。不仅如此&#xff0c;官方还宣称 GPT-4.5 被定位为 “最后一代非推理模型”&#xff0c;这一消息再次引起了…

什么是JTAG、SWD?

一、什么是JTAG&#xff1f; JTAG&#xff08;Joint Test Action Group&#xff0c;联合测试行动小组&#xff09;是一种国际标准测试协议&#xff0c;常用于芯片内部测试及对系统进行调试、编程等操作。以下从其起源、工作原理、接口标准、应用场景等方面详细介绍&#xff1a…

知识周汇|SAP脚本自动化-淋过雨的人更懂得伞的价值

目录 摘要 1 知识概览 1.1SAP GUI脚本 1.2Tracker工具 2 实践案例 2.1步骤1&#xff1a;SAP启动并进入系统&#xff08;文本关键&#xff09; 2.1.1手动操作&#xff1a;鼠标双击SAP&#xff0c;并点击所需要系统 2.1.2代码实现 2.2步骤2&#xff1a;通过tracker完善后…

【GPU使用】如何在物理机和Docker中指定GPU进行推理和训练

我的机器上有4张H100卡&#xff0c;我现在只想用某一张卡跑程序&#xff0c;该如何设置。 代码里面设置 import os # 记住要写在impot torch前 os.environ[CUDA_VISIBLE_DEVICES] "0, 1"命令行设置 export CUDA_VISIBLE_DEVICES0,2 # Linux 环境 python test.py …

【无标题】ABP更换MySql数据库

原因&#xff1a;ABP默认使用的数据库是sqlServer&#xff0c;本地没有安装sqlServer&#xff0c;安装的是mysql&#xff0c;需要更换数据库 ABP版本&#xff1a;9.0 此处以官网TodoApp项目为例 打开EntityFrameworkCore程序集&#xff0c;可以看到默认使用的是sqlServer&…

【网络编程】之TCP实现客户端远程控制服务器端及断线重连

【网络编程】之TCP实现客户端远程控制服务器端及断线重连 TCP网络通信实现客户端简单远程控制主机基本功能演示通信过程代码实现服务器模块执行命令模块popen系列函数 客户端模块服务器主程序 windows作为客户端与服务器通信#pragma comment介绍 客户端使用状态机断线重连代码实…

Git快速入门

文章目录 Git简介准备工作常用的Linux命令git配置 git工作原理git项目创建和克隆git基本操作命令git忽略文件配置ssh远程连接 IDEA集成Gitgit分支&#xff08;多人开发&#xff09;公司中用到的&#xff08;很清楚&#xff09; Git 简介 Git就是版本控制的工具 下面这个叫手动…

Redis 的几个热点知识

前言 Redis 是一款内存级的数据库&#xff0c;凭借其卓越的性能&#xff0c;几乎成为每位开发者的标配工具。 虽然 Redis 包含大量需要掌握的知识&#xff0c;但其中的热点知识并不多。今天&#xff0c;『知行』就和大家分享一些 Redis 中的热点知识。 Redis 数据结构 Redis…

深入解析Java虚拟机(JVM)的核心组成

深入解析Java虚拟机&#xff08;JVM&#xff09;的核心组成 Java虚拟机&#xff08;JVM&#xff09;作为Java语言跨平台的核心实现&#xff0c;其架构设计精妙而复杂。理解JVM的组成部分&#xff0c;是掌握Java内存管理、性能调优和问题排查的关键。本文将从四大核心模块剖析J…

GIT工具学习【2】:分支

1.什么是分支 新建一个分支&#xff0c;可以认为把当前项目copy了一份&#xff0c;不太严谨&#xff0c;没毛病&#xff0c;里面虽然文件内容和名字相同&#xff0c;其实互相没有关系。 2.什么是合并分支 就是把两个分支&#xff08;项目文件夹&#xff09;合并在一起 git m…

40岁开始学Java:Java中单例模式(Singleton Pattern),适用场景有哪些?

在Java中&#xff0c;单例模式&#xff08;Singleton Pattern&#xff09;用于确保一个类只有一个实例&#xff0c;并提供全局访问点。以下是详细的实现方式、适用场景及注意事项&#xff1a; 一、单例模式的实现方式 1. 饿汉式&#xff08;Eager Initialization&#xff09; …