Java多线程初阶(二)(图片+源码+超详细)

news2024/11/18 11:37:08

在这之前可以参照:Java多线程初阶(一)这篇文章🐻

目录

1. 线程的状态

2. 线程安全问题

2.1 引出线程安全问题

2.2 线程安全问题出现的原因

2.3 解决线程安全问题的方法 

2.4 synchronized关键字详解

2.5 volatile关键字详解

3. wait方法和notify/notifyAll方法详解


1. 线程的状态

😄线程的状态在Java中就是一个枚举类型,我们可以输出这个枚举类中的类型观察线程总共有哪几种状态:

class ThreadStateTestDrive {
    public static void main(String[] args) {
        for(Thread.State state : Thread.State.values) {
            System.out.println(state);
        }
    }    
}

这些状态及这些状态的含义为:

  • NEW:线程处于已创建的状态。线程对象在new之后,调用start方法之前都处于这个状态
  • RUNNABLE:处于JVM中工作的线程都处于这个状态。这个状态又分为准备工作(ready)状态和工作中(running)状态
  • BLOCKED:称为线程的阻塞状态。等待另一个线程执行任务的那个线程处于这个状态
  • WAITING:等待状态。也是等待另一个线程执行特定动作的线程处于这个状态
  • TIMED_WAITING:超时等待状态。也是等待另一个线程执行特定动作的线程处于这个状态,不同的是,处于这个状态的线程不会像等待状态的线程一样——死等
  • TERMINATED:线程的终止状态。执行结束的线程处于这个状态。或者说我们自定义线程的run方法执行结束后,这个线程处于的状态。

这些线程之间的状态转换具有以下这张图上的关系(备忘):

 

2. 线程安全问题

2.1 引出线程安全问题

用一个栗子来引出并发环境下带来的安全问题并详细分析其中的原因:

class Number {
    public int number = 0;
    public void addNum() {
        synchronized (Number.class) {
            this.number++;
        }
    }
}

public class ThreadSafetyTestDrive {
    private static Number number = new Number();

    public static void main(String[] args) throws InterruptedException {
        //创建两个线程对分别对Number对象中的num属性进行自增操作
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    number.addNum();
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    number.addNum();
                }
            }
        });
        //启动这两个线程
        thread1.start();
        thread2.start();
        //等待这两个线程执行结束后再在main线程中输出Number实例中的num的值
        thread1.join();
        thread2.join();
        System.out.println(number.number);
        //会发现输出的这个num的值在5w-10w之间,并不去顶具体是哪个值
    }
}

明明我们对同一个Number实例中的num实例进行了10w次自增操作,可为什么得到的值却不是10w?更过分的是,它还是一个每次程序运行不能确定的值!这是为什么呢?😂我们画张图来理解下这个过程。

2.2 线程安全问题出现的原因

  1. 线程是抢占式执行的,线程之间的调度充满了未知的随机性并且不受我们的控制,这是多线程并发环境下导致线程安全问题的万恶之源。
  2. 多个线程并发访问内存中的同一片内存空间,并且这片内存空间上操作不是原子性的,这就会出现多个线程同时修改同一片内存空间,造成不可预估的后果。
  3. 内存可见性。这是指编译器可能会对我们访问量高且需求量大的数据(Y)从内存中拷贝一份给放到CPU的寄存器当中进行读取,提升我们程序效率的一种机制。显然,这种机制在单线程环境中无疑是优异的,但是在多线程环境中,如果别的线程在某个时机修改了这个线程重复读取的这个值(Y),那么这个线程就不能及时的读取到被更新后的数据(Y')。这种在多线程环境中可能出现的问题就叫做内存可见性。由此导致的线程安全问题也是不可忽视的。
  4. 指令重排。这是指编译器会对我们的程序在保证逻辑不变的情况下进行某些操作的重排序以提高程序的执行效率,这就是所谓的指令重排,它也是编译器优化我们代码的一种机制。但是,在多线程环境下,编译器对“保证程序逻辑不变的情况下”进行操作的重排序是很难准确进行的。因此,这种激进方式的重排序很有可能会导致我们的程序也出现一些难以预知的效果。

下面这张图是对于由于内存可见性或者指令重排而导致线程安全问题的解释,原因2

就是我们在2.1中的引例,就不再画图了:

2.3 解决线程安全问题的方法 

1.使用synchronized关键字

被synchronized修饰的方法、方法块或者对象具有互斥的特性。当某个线程执行到某个对象的synchronized时,其他线程也执行到了同一个对象的synchronized时就会阻塞等待。同时synchronized关键字除了能够实现指令的原子性外,还能够保证内存的可见性。

2.使用volatile关键字

volatile关键字用于修饰成员变量,被volatile关键字修饰的成员变量能够实现内存的可见性,但是volatile关键字不能保证变量的原子性。

2.4 synchronized关键字详解

😄synchronized本质上是要修改指定对象的对象头也称为监视器锁(monitor lock)。因此synchronized必须要搭配一个对象来使用。synchronized对不同线程中的同一个对象之间具有竞态条件的原理如下:

 


😄synchronized的工作过程

  1. 获得互斥锁🔒。如果拿不到,则会处于阻塞状态
  2. 从主内存拷贝变量到自己的工作区域
  3. 执行代码并将更改后的共享变量的值刷新到主内存
  4. 释放互斥锁🔒

由上我们可以知道,synchronized也能够保证内存的可见性但是在验证内存可见性时,我遇到的下面的这个问题困扰了我很久:

  • 当我在一个线程中A中一直高速访问一个对象的成员变量,在某个时刻在另一个线程中B中修改了这个成员变量。启动A线程时run方法用了synchronized关键字修饰,按理来说,synchronized关键字能够实现内存可见性,那么它应当能够及时获取到成员变量被修改后的值,但是线程A仍然读取不到,这是为什么呢?

下面是出现这种问题的代码,thread-0线程指的是上面的A线程,main线程指的是上边的B线程,isQuit成员变量指的是上面的成员变量:

public class SynchronizedTestDrive {

    private static int isQuit = 0;

    public static void main(String[] args) {
        //创建一个新线程,不断访问我们的一个成员变量
        Thread thread = new Thread(new Runnable() {
            @Override
            public synchronized void run() {
                while (true) {
                    if (isQuit != 0) {
                        break;
                    }

                }
                System.out.println(Thread.currentThread().getName() + " 执行结束~");
            }
        }, "thread-0");

        thread.start();    //启动thread-0线程

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        isQuit = 1;             //在这里我将成员变量的值设置为了1,案例来说thread-0线程应该结束了。但是为什么程序一直不退出呢?

        System.out.println("main线程执行结束");
    }
}

分析问题:


😄synchronized的特性:

synchronized是可重入锁,具有可重入的特性。那么什么是可重入锁呢?

public class ReInLockTestDrive {
    static final Object object = new Object();
    public static void main(String[] args) {
        synchronized (object) {
            System.out.println("层次-1");
            synchronized (object) {
                System.out.println("层次-2");
            }
        }
    }
}

我们运行上面这段程序,发现并没有问题。按理来说外层的synchronized给Object对象加锁后,其他的程序就无法再次获得Object对象上的锁,但事实并不是这样,使用synchronized可以对同一个对象多次加锁。这就是synchronized的可重入性,我们常称它为可重入锁。

synchronized具有内存可见性。它具有刷新内存的功能,保证了内存的可见性。由于synchronized每次都从主内存加载变量到工作内存区并在执行完成后将工作内存区的内容刷新到主内存区,所以它是具有内存可见性的。


😄 synchronized的使用:

  1. synchronized修饰普通方法,相当于对方法中的当前对象上锁
    public class SynchronizedTestDrive {
        public synchronized void test() {
        }
    }
  2. synchronized修饰静态方法,相当于对当前类的类对象。值得注意的是,同一个类的所有对象都共享一个类对象,这相当于对该类的所有对象加锁,同一时刻,只能有一个对象获得执行指令上指令,其他对象排队获取,处于阻塞状态。
     

    public class SynchronizedTestDrive {
        public static synchronized void test() { }
    }
  3. synchronized修饰代码块,在代码块中指定要加锁的对象
     

    //这和synchronized直接修饰普通方法的效果是一致的
    public class SynchronizedTestDrive {
        public void test() {
            synchronized (this) {        //针对当前对象加锁🔒
            }
        }
    }
    
    //这和synchronized直接修饰静态方法的效果是一致的
    public synchronized class SynchronizedTestDrive {
        public void test () {
            synchronized (SynchronizedTestDrive.class) { //针对当前类对象加锁🔒
            }
        }
    }

2.5 volatile关键字详解

volatile关键字能够保证变量的内存可见性,但是不能保证对变量操作的原子性。它的工作过程如下:

  • 代码在改变volatile修饰的变量的时候
    ①先改变工作内存中的变量副本的值;②将工作内存中变量副本的值刷新到主内存中
  • 代码在加载volatile修饰的变量的时候
    ①从主内存中将volatile变量的最新值加载到线程的工作区域当中;②从工作区域中读取volatile变量的副本

这个关键字在上面已经介绍的很详细了,这里就不再写那么多啦。为成员变量加上volatile的实质是使得指令强制读取内存,虽然速度慢了,但是数据变得更加可靠!

3. wait方法和notify/notifyAll方法详解

1.wait方法

wait方法与sleep方法的功能类似,能够让当前调用的线程进入到等待状态。并且它需要与synchronized搭配使用,脱离synchronized的wait方法的调用会直接抛出监视器minitor状态异常。

方法说明
public final void wait() throws InterruptedException使调用线程进入永久等待状态,直到被notify或者notifyAll方法唤醒
public final native void wait(long timeout) throws InterruptedException使调用线程等待timeout秒,如果再这个时间段内没有线程唤醒它,则到timeout时刻时自动唤醒
public final void wait(long timeout,int nanos) throws InterruptedException和一个参数的方法类似,不过设置的等待时间更加精确
 

wait的工作过程:

  • 使当前线程进入到等待状态(实质上是加入到CPU的等待队列中)
  • 释放当前的锁
  • 当被notify/notifyAll方法唤醒时,会重新尝试获得当前锁,然后继续执行

体会下wait方法的使用

没有搭配synchronized使用的wait方法抛出了运行了异常:

 

在thread-0线程中使用wait使得当前线程进入等待状态,在main线程中使用notify方法结束thread-0线程的wait方法:

public class WaitAPITestDrive {
    private static final Object object = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //在这里获得object对象锁
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + " 开始");
                    try {
                        //在这里进入等待状态,失去object对象锁
                        object.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + " 结束");
                }
            }
        }, "thread-0");
        thread.start();    //启动线程

        Thread.sleep(1000);    //主线程休眠1s后进行thread-0线程的唤醒
        synchronized (object) {    //获得object对象锁
            object.notify();    //唤醒thread-0线程。thread-0线程尝试获取这个obejct对象锁,然后继续执行
        }
        System.out.println("main线程结束");
    }
}

wait方法使用的注意事项:

  • wait方法需要在synchronized修饰的方法或者代码块中才能起作用,否则会抛出IllegalMonitorStateException异常信息
  • wait方法的调用需要依赖指定的对象加锁对象,例如上面的例子中wait的执行是依靠object这个对象加减锁实现线程的暂停和被唤醒的。

2.notify方法(包括notifyAll方法)

方法说明
public final native void notify()唤醒等待当前对象锁的线程,具体的唤醒线程要取决于线程调度器,这个是随机的
public final native void notifyAll()唤醒所有等待该对象锁的线程,具体哪个等待线程先获取到对象锁是不能确定的

notify/notifyAll方法也需要与synchronized搭配使用,它用于唤醒可能等待该加锁对象的对象锁的其他线程,并使得这些线程重新获得对象锁,唤醒它们的执行。如果同时又多个线程在等待这个对象锁,则由线程调度器重新挑选出一个处于等待该对象锁的线程并唤醒,这个“挑选”是随机的,并没有先来后到的说法。😕

具体的唤醒栗子参照上面1.wait方法介绍中的第二个例子,这里就不再举例了。

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

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

相关文章

【LeetCode】145.二叉树的后续遍历

1.问题 给你一棵二叉树的根节点 root &#xff0c;返回其节点值的 后序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[3,2,1] 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[] 示例 3&#xff1a; 输入&#xff1a;roo…

Linux搭建我的世界MC服务器 - MCSM面板 【外网远程联机教程】

文章目录 1. 安装JAVA2. MCSManager安装3.局域网访问MCSM4.创建我的世界服务器5.局域网联机测试6.安装cpolar内网穿透7. 配置公网访问地址8.远程联机测试9. 配置固定远程联机端口地址9.1 保留一个固定tcp地址9.2 配置固定公网TCP地址9.3 使用固定公网地址远程联机 Linux使用MCS…

程序地址空间(上)

目录 &#xff1a; 1.C/C程序地址空间回顾&#xff08;C语言&#xff09; 2.通过一段代码&#xff08;引出进程虚拟地址空间&#xff09; 3.进程虚拟地址空间是什么 ------------------------------------------------------------------------------------------------------…

项目制作4

今天对文件录入,进行了测试,结构体录入还是非常的好用的 对地图也有了新的制作,但是地图上的鼠标操作遇到了问题 难以解决,一搞就是内存问题给我下坏了 哎我选择认了,用控制台来显示公告算了 for (int j 1;j < f->Points;j) fread(&f->drops[j], sizeof(drop…

微服务---Docker的基本使用

Docker 1.初识Docker 1.1.什么是Docker 微服务虽然具备各种各样的优势&#xff0c;但服务的拆分通用给部署带来了很大的麻烦。 分布式系统中&#xff0c;依赖的组件非常多&#xff0c;不同组件之间部署时往往会产生一些冲突。在数百上千台服务中重复部署&#xff0c;环境不…

虹科分享 | 基于流的流量分类的工作原理 | 网络流量监控

许多ntop产品&#xff0c;如ntopng、nProbe和PF_RING FT等都是基于网络流的。然而&#xff0c;并不是所有的用户都详细知道什么是网络流&#xff0c;以及它在实践中是如何工作的。这篇博客文章描述了它们是什么以及它们在实践中是如何工作的。 什么是网络流量 网络流是一组具…

C++11新特性(下)

文章目录 1. 可变参数模板1.1 empalce相关接口函数 2. lambda表达式2.1 C98中的一个例子2.2 lambda表达式语法2.3 函数对象与lambda表达式 3. 包装器3.1 function包装器3.2 bind 1. 可变参数模板 C11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板。相比…

Matlab与ROS---Action与Gazebo(六)

0. 简介 对于ROS1而言&#xff0c;其在Matlab当中相较于ROS2还有一些比较高级的用法&#xff0c;比如说我们接下来要说的Action和Gazebo仿真。 1. ROS Action ROS的Action行为模式当中也存在有一个客户端到服务器的通信关系。Action行为模式使用ROS主题将目标消息从客户机发…

【技巧】飞书多维表格零代码连接飞书多维表格,实现表单自动同步

飞书多维表格用户使用场景&#xff1a; 在公司日常工作中&#xff0c;各个部门使用飞书多维表格记录签订合同、文件审核、归档等事务&#xff0c;常需要行政人员辅助处理&#xff0c;将这些子表单的数据汇总到一个总表单中。但每个部门对应的事务较为复杂&#xff0c;子表单数量…

OpenCV例程赏析

OpenCV例程赏析 ①SITF特征检测匹配(目标查找)例程&#xff1a;…\opencv\sources\samples\python2\find_obj.py(asift.py) #!/usr/bin/env python ‘’’ Feature-based image matching sample. Note, that you will need the https://github.com/opencv/opencv_contrib r…

什么是内容交付网络?

内容交付网络&#xff08;CDN&#xff09;是一个全球分布的网络服务器或存在点&#xff08;PoP&#xff09;&#xff0c;其目的是提供更快的内容交付&#xff0c;内容被复制并存储在整个CDN中&#xff0c;因此用户可以访问存储在地理上离用户最近的位置的数据。这与仅在一个中央…

Flinkx/Datax/Flink-CDC 优劣势对比

Flinkx/Datax/Flink-CDC 优劣势对比_HiBoyljw的博客-CSDN博客 一、FlinkX简介 FlinkX是一款基于Flink的分布式离线/实时数据同步插件&#xff0c;可实现多种异构数据源高效的数据同步&#xff0c;其由袋鼠云于2016年初步研发完成&#xff0c;目前有稳定的研发团队持续维护&…

微前端解决方案

目录 微前端解决方案微前端的整体架构微前端部署平台 微前端解决方案 在理想的情况下&#xff0c;期望能达到&#xff0c;将一个复杂的单体应用以功能或业务需求垂直的切分成更小的子系统&#xff0c;并且能够达到以下能力&#xff1a; 子系统间的开发、发布从空间上完成隔离…

java学员学生综合测评管理系统

目 录 摘 要 I ABSTRACT II 第一章 绪论 1 1.1课题背景 1 1.2目的和意义 1 1.3开发工具及技术 2 1.3.1开发工具 2 1.3.2 JSP技术 2 1.4软硬件需求 3 第二章 系统分析 5 2.1可行性分析 5 2.1.1技术可行性 5 2.1.2经济可行性 5 2.1.3操…

GrassRouter多链路聚合通信系统保障公路网络稳定全面覆盖解决方案

近年来国内经济不断发展&#xff0c;城市道路交通能力迅速提高&#xff0c;各省市道路交通体系不断完善&#xff0c;促使高速公路运能得到极大提高&#xff0c;公路运输的通达性、舒适性得到明显提高。随着现代化高速公路的建设&#xff0c;新一代无线网络监控系统&#xff0c;…

Jmeter控制器 Logic Controller

控制器包含&#xff1a; 1.Loop Controller 作⽤&#xff1a;指定其⼦节点运⾏的次数&#xff0c;可以使⽤具体的数值&#xff0c;也可以使⽤变量    Forever选项&#xff1a;表示⼀直循环下去   如果同时设置了线程组的循环次数和循环控制器的循环次数&#xff0c;那循环…

大屏使用echart开发省市地图,并点击省获取市地图

1. 本文在基础上进行改进&#xff0c;后端使用若依后端 IofTV-Screen: &#x1f525;一个基于 vue、datav、Echart 框架的物联网可视化&#xff08;大屏展示&#xff09;模板&#xff0c;提供数据动态刷新渲染、屏幕适应、数据滚动配置&#xff0c;内部图表自由替换、Mixins注…

ZooKeeper的安装和配置过程

ZooKeeper的安装和配置过程 ZooKeeper服务器是用Java创建的&#xff0c;它需要在JVM上运行&#xff0c;所以需要使用JDK1.6及以上版本。 查看是否安装了Java环境: java -version没有安装的先去安装JDK&#xff1a;Linux 安装 JDK 官网下载zooKeeper 官网下载zooKeeper&…

基于 YOLOv8 的自定义数据集训练

图1.1&#xff1a;YOLOv8初始测试 YOLOv8&#x1f525;于 2023年1月10日由Ultralytics发布。它在计算机视觉方面提供了进展&#xff0c;带来了对我们感知、分析和理解视觉世界的巨大创新。它将为各个领域带来前所未有的可能性。 在速度、准确性和架构方面进行了相当大的改进。它…

vs的常用配置【以及vs常用的快捷键】

1、颜色设置 (1) 编译器的主题颜色设置 (2) 字体和颜色设置 (3) 字体大小 更快捷的修改字体大小方式&#xff1a;ctr鼠标滚轮 2、行号设置 默认就有&#xff0c;不用设置了 3、把解决方案资源管理器移动到左边 4、设置打开错误列表 5、自动保存-要手动使用快捷键 ctrs 代码…