JAVA多线程和并发编程(三)- JAVA多线程信息共享

article/2025/10/9 13:20:22

        我们通常希望多个线程之间有信息的通信,而不是每个线程各自run方法执行完就结束了。那么多个线程间如何通信呢?

        Java中多线程通常通过共享变量进行信息共享。

        1)使用static变量共享信息,该方法适用于通过继承Thread类创建线程的方式。

        2)通过同一个Runnable实例的成员变量来共享信息,该方法适用于通过实现Runnable接口创建线程的方式。

        JDK原生库不支持线程间点对点发送消息(类似C/C++的MPI并行库, MPI是一个信息传递应用程序接口,至今仍然是高性能计算的主要模型。MPI支持线程0向线程1发送一条消息,或者线程0向所有线程群发一条消息)。

一、使用static变量共享信息

public class ThreadMsgShareTest {public static void main(String[] args) {new Thread1().start();new Thread1().start();new Thread1().start();new Thread1().start();}
}class Thread1 extends Thread {private static int tickets = 100; // 所有线程一起卖100张票public void run() {while(tickets > 0) {System.out.println(Thread.currentThread().getName() + " is " +"selling ticket : " + tickets);tickets --;}}
}

        如上代码中, Thread1定义了一个静态变量,则所有Thread1实例共享同一个tickets变量。在main函数中,创建了4个不同的Thread1对象,启动四个线程,这些线程共同拥有100张票。

        程序运行结果如下:

Thread-0 is selling ticket : 100
Thread-0 is selling ticket : 99
Thread-0 is selling ticket : 98
Thread-0 is selling ticket : 97
Thread-0 is selling ticket : 96
Thread-3 is selling ticket : 100
Thread-3 is selling ticket : 94
Thread-3 is selling ticket : 93

... 省略,4个线程共卖了103张票,说明出现了信息同步问题

        可以看到线程0和线程3都卖了第100张票,说明存在信息同步问题。

        如果将Thread1的静态变量改成普通的成员变量,则tickes将成为每个线程对象各自的成员变量,即每个线程都拥有100张票,如下代码所示:

public class ThreadMsgShareTest {public static void main(String[] args) {new Thread1().start();new Thread1().start();new Thread1().start();new Thread1().start();}
}class Thread1 extends Thread {
//    private static int tickets = 100;private int tickets = 100; // 每个线程卖100张票public void run() {while(tickets > 0) {System.out.println(Thread.currentThread().getName() + " is " +"selling ticket : " + tickets);tickets --;}}
}

        该代码运行后,每个线程独立售卖100张票,四个线程一共卖了400张票。

二、使用同一个Runnable实例的成员变量共享信息

public class RunnableMsgShareTest {public static void main(String[] args) {Runnable1 runnable1 = new Runnable1();new Thread(runnable1).start();new Thread(runnable1).start();new Thread(runnable1).start();new Thread(runnable1).start();}
}class Runnable1 implements Runnable {private int tickets = 100;@Overridepublic void run() {while(tickets > 0) {System.out.println(Thread.currentThread().getName() + " is " +"selling ticket : " + tickets);tickets --;}}
}

        如上代码中,虽然Runnable1类中的tickets是普通成员变量,但同一个Runnable实例可以在多个线程对象间共享,如上main函数中,虽然创建了4个线程对象,但它们的target都是同一个Runnable1对象,所以四个线程操作的tickets都是同一个实例的变量,从而达到信息共享的效果。

        运行结果如下:

Thread-1 is selling ticket : 100
Thread-3 is selling ticket : 100
Thread-2 is selling ticket : 100
Thread-2 is selling ticket : 97
Thread-0 is selling ticket : 100
Thread-2 is selling ticket : 96
Thread-3 is selling ticket : 98
Thread-3 is selling ticket : 93

... 省略,四个线程共卖了103张票,说明出现了信息同步问题

        如上运行结果说明,四个线程确实共享了同一个tickets变量,由于没有进行共享资源的同步控制,因此出现了信息共享问题。

        如上代码中,如果将main函数的代码改成每个线程对象都拥有不同的runnable实例,则每个线程都独立售卖100张票,不存在tickets信息的共享。

通过如上两种方式,虽然实现了信息共享,但却带来了多线程操作同一资源出现的信息不一致问题。下面介绍如何解决信息不一致的问题。

三、信息不同步的原因 - 工作缓存副本

        JVM中每个线程都有一个工作缓存,线程会从主存中加载操作数到工作缓存中生成一个副本,CPU对副本进行运算操作后,将结果写入工作缓存,最后数据才从工作缓存中刷新到主存中。

        线程的运行都是依赖工作缓存的,线程1对工作缓存中副本的修改,对线程2和线程3是不可见的,它们的工作缓存中还是原来的值,这就出现了数据不一致的问题。

四、实现多线程信息同步的方案

方案一:volatile 关键字,实现内存可见性

        解决工作缓存副本问题,用于保证多线程对共享变量操作时的可见性。用volatile关键字修饰的变量,如果在工作缓存中被修改,会立即刷新到主存,且同步失效其他线程工作缓存中该变量的值。即volatile关键字修饰的变量如有改变,会及时通知给所有线程

        注意:volatile关键字修饰的变量只能进行原子操作,适用于修饰flag=true/false这种标记型字段,volatile对复合操作无效。

volatile底层原理

        如果将使用volatile修饰的代码和未使用volatile修饰的代码都编译成汇编语言,会发现,使用volatile修饰的代码会多出一个lock前缀指令。

        lock前缀指令相当于一个内存屏障,内存屏障的作用有以下三点:

        ①重排序时,不能把内存屏障后面的指令排序到内存屏障前

        ②使得本CPU的cache写入内存

        ③写入动作会引起其他CPU缓存或内核的数据无效,相当于修改对其他线程可见。

        上文卖票的示例中,由于tickets--是复合操作,因此对tickets变量增加volatile修饰,仍然解决不了多线程同步问题。

public class RunnableMsgShareTest {public static void main(String[] args) {Runnable1 runnable1 = new Runnable1();new Thread(runnable1).start();new Thread(runnable1).start();new Thread(runnable1).start();new Thread(runnable1).start();}
}class Runnable1 implements Runnable {// volatile对多线程中有复合操作的变量无效private volatile int tickets = 100;@Overridepublic void run() {while(tickets > 0) {System.out.println(Thread.currentThread().getName() + " is " +"selling ticket : " + tickets);tickets--;}}
}

        运行结果:

Thread-0 is selling ticket : 100
Thread-0 is selling ticket : 99
Thread-0 is selling ticket : 98
Thread-0 is selling ticket : 97
Thread-0 is selling ticket : 96
Thread-3 is selling ticket : 100
Thread-2 is selling ticket : 100

... 省略,多个线程售卖第100张票,仍然有多线程同步问题

         下面的代码中用volatile修饰布尔型变量,实现多线程信息同步:

public class VolatileTest {public static void main(String[] args) throws InterruptedException {Runnable2 runnable2 = new Runnable2();new Thread(runnable2).start();Thread.sleep(10);runnable2.flag = false;System.out.println("main thread existing");}
}class Runnable2 implements Runnable {public volatile boolean flag = true;@SneakyThrows@Overridepublic void run() {while (flag) {}System.out.println("sub thread existing");}
}

        运行结果:

main thread existing
sub thread existing

        可以看到,子线程初始运行时flag=true,于是进入while循环。之后main线程修改了flag=false,子线程立即看到了该变化,退出了while循环。

        如果将上述代码中flag去掉volatile修饰,则子线程会进行死循环,不会退出,运行结果如下:

main thread existing

        该运行结果说明,主线程已经退出了,内存中的flag已经为false,但子线程中的flag是使用其工作缓存中的值,该值仍然为true,因此子线程一直在运行,没有退出。

 方案二:synchronized 关键字 对代码块/函数加锁,实现指定代码段的互斥访问。

        互斥:某个线程运行一个代码段(关键区),其他线程不能同时运行这个代码段。互斥是同步的一种特例。互斥的关键字是synchronized.

        同步:多个线程的运行,必须按照某种规定的先后顺序来运行。

        synchronized关键字修饰的代码块/函数,同一时刻只能一个线程访问。

        synchronized加大性能负担,但是使用简便。

        synchronized如果修饰代码段,则必须加锁在某一个对象上,只要是一个非空的对象都可以。所有线程要执行关键区的代码,必须先抢到这把锁。

        代码示例:上文卖票的示例中,抽取一个卖票的函数,用synchronized修饰,实现多线程正确访问共享变量

public class SynchronizedTest {public static void main(String[] args) {Runnable1 runnable1 = new Runnable1();new Thread(runnable1).start();new Thread(runnable1).start();new Thread(runnable1).start();new Thread(runnable1).start();}
}class Runnable1 implements Runnable {private int tickets = 100;@SneakyThrows@Overridepublic void run() {while(true) {sale();Thread.sleep(10); // 为了方便线程阻塞后切换另一个线程if(tickets <= 0) {break;}}}private synchronized void sale() {if(tickets > 0) {System.out.println(Thread.currentThread().getName() + " is " +"selling ticket : " + tickets);tickets--;}}
}

        运行结果:

Thread-0 is selling ticket : 100
Thread-3 is selling ticket : 99
Thread-2 is selling ticket : 98
Thread-1 is selling ticket : 97
Thread-0 is selling ticket : 96
Thread-3 is selling ticket : 95
Thread-2 is selling ticket : 94
Thread-1 is selling ticket : 93
Thread-0 is selling ticket : 92
Thread-1 is selling ticket : 91
Thread-2 is selling ticket : 90
Thread-3 is selling ticket : 89

... 四个线程按顺序售卖了第100 到第1张票。


http://chatgpt.dhexx.cn/article/1huCrwN4.shtml

相关文章

Java多线程的知识点

&#x1f331;&#x1f331;友友们大家好 我是你们的小王同学啊 今天给大家带来的是 java多线程的知识点 希望大家能支持小王 喜欢就给个三连吧 你们的三连是我制作的动力&#xff01;&#x1f497;&#x1f497; 小王的gitee&#xff1a;小王同学&#x1f370; 小王的github&a…

java线程调度

线程调度分为两种形式 1 分时调度模型: 所有线程轮流获得CPU使用权&#xff0c;平均分配每一个线程的CPU时间片 2 抢占式调度模型: 优先让优先级更高的线程使用CPU&#xff0c;如果线程优先级相同 那机会随机分配优先级 给优先级高的线程更多一些的时间片 而java的分配模式 是…

java多线程(详)

目录 一,什么叫线程&#xff1f; 那我们要先了解什么叫进程&#xff0c;线程依赖于进程而存在的。 二.多线程的创建 方式一&#xff1a;继承Thread类 方式二&#xff1a;实现Runnable接口 方式三&#xff1a;JDK 5.0新增&#xff1a;实现Callable接口 三种方式的比…

Java线程、Java多线程详细介绍

目录 一、进程和线程的区别 1.1 进程 1.2 线程 二、并发和并行 2.1 并行 2.2 并发 2.3 监控线程的执行情况 三、创建方式 3.1 继承Thread类 思考&#xff1a;为什么不直接通过对象调用start&#xff08;&#xff09;方法&#xff1f; 3.2 实现Runnable接口 …

【java】java多线程及线程池详解

目录 前言线程是什么&#xff1f;多线程是什么&#xff1f;多线程的作用和好处以及缺点守护线程和用户线程并发和并行的区别 一.线程的状态和常用方法1.线程各种状态转化图2.线程相关常用方法有① wait()② sleep(long timeout)③ join()④ yield()⑤ notify()和notifyAll() 3.…

Java线程池(超详细)

文章目录 1. 线程池概念2. JUC线程池架构3. Executors创建线程的4种方法4. 线程池的标准创建方式5. 向线程池提交任务的两种方式6. 线程池的任务调度流程7. ThreadFactory&#xff08;线程工厂&#xff09;8. 任务阻塞队列9. 调度器的钩子方法10. 线程池的拒绝策略11. 线程池的…

Java多线程超详解

引言 随着计算机的配置越来越高&#xff0c;我们需要将进程进一步优化&#xff0c;细分为线程&#xff0c;充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。 那么话不多说&#xff0c;今天本帅将记录自己线程的学习。 程序&#xff0c;进程&#xff0c;线程的…

java多线程(超详细)

1 - 线程 1.1 - 进程 进程就是正在运行中的程序&#xff08;进程是驻留在内存中的&#xff09; 是系统执行资源分配和调度的独立单位 每一进程都有属于自己的存储空间和系统资源 注意&#xff1a;进程A和进程B的内存独立不共享。 1.2 - 线程 线程就是进程中的单个顺序控制…

JAVA线程

一、线程相关概念 &#xff08;一&#xff09;程序、进程和线程的区别 程序 程序是含有指令和数据的文件&#xff0c;被存储在磁盘或其他的数据存储设备中&#xff0c;也就是说程序是静态的代码。 进程 进程是程序的一次执行过程&#xff0c;是系统运行的基本单位&#xf…

Java 线程 基础知识总结

线程基础 很不严谨的说&#xff0c;线程是什么&#xff1f;线程就是为了让很多个东西并发执行&#xff0c;大大的提高程序执行的效率啊 三个非常重要的概念&#xff1a; 程序&#xff1a;一组写好了的静态代码块&#xff08;就我们写的那些代码玩意&#xff09;进程&#xf…

Java多线程(超详解)

目录 1. 线程简介 1.1 程序 1.2 进程 1.3 线程 1.4 多线程 1.5 普通方法调用和多线程 2. 线程创建 2.1 继承Thread类 2.2 实现Runnable接口 2.3 实现Callable接口&#xff08;了解&#xff09; 2.4 网图下载 2.4.1 通过继承Thread类实现网图下载 2.4.2 通…

java 线程详解

一、线程的基本概念 一个程序最少需要一个进程&#xff0c;而一个进程最少需要一个线程。关系是线程–>进程–>程序的大致组成结构。所以线程是程序执行流的最小单位&#xff0c;而进程是系统进行资源分配和调度的一个独立单位。 一个线程就是在进程中的一个单一的顺序…

JAVA多线程详解(超详细)

目录 一、线程简介1、进程、线程2、并发、并行、串行3、进程的三态 二、线程实现1、继承Thread类2、实现Runnable接口3、实现Callable接口&#xff08;不常用&#xff09; 三、线程常用方法1、线程的状态2、线程常用方法 四、多线程1、守护&#xff08;Deamon&#xff09;线程2…

Java多线程(超详细!)

1、什么是进程&#xff1f;什么是线程&#xff1f; 进程是:一个应用程序&#xff08;1个进程是一个软件&#xff09;。 线程是&#xff1a;一个进程中的执行场景/执行单元。 注意&#xff1a;一个进程可以启动多个线程。 eg. 对于java程序来说&#xff0c;当在DOS命令窗口中…

count/count if函数的基本用法

count函数&#xff0c;用来计算单元格的数的个数&#xff0c;只是用来计数&#xff0c;并且只有只记录数子的个数&#xff0c;文本的个数是不被记录的。 但是很少会用到单纯的count函数&#xff0c;往往在工作中计数是带有条件的。就会用到countif函数 COUNTIF函数需要注意的点…

EXCEL COUNTIF()的一些奇特的用法

文章目录 前言一、统计第几次重复二、统计不重复的数量三、通配符模糊统计四、防止重复录入五、忽略错误值或空值统计六、重复值填充背景色总结 前言 日常工作中需要度娘很多知识点或者方法&#xff0c;但每次用了就忘&#xff0c;下次遇到就需要继续度娘&#xff0c;故在此记…

Excel多条件计数——COUNTIFS【获奖情况统计】

问题描述 当前&#xff0c;我们需要对表格中的获奖情况进行统计 奖励级别&#xff1a;院级、校级、国家级、国际级奖励内容&#xff1a;特等奖、一等奖、二等奖、三等奖、优胜奖 功能要求 对所有奖励级别进行统计根据级别&#xff0c;计算内容数量 当有人的选项内容如下时 …

如何在Microsoft Excel中使用COUNTIF函数

COUNTIF 是一个 Excel 函数,用于对满足单个条件的区域中的单元格进行计数。COUNTIF可用于计算包含日期、数字和文本的单元格。COUNTIF 中使用的条件支持逻辑运算符(>、<、<>、=)和通配符(*、?)进行部分匹配。 例如,我们想计算包含 Google或 Facebook 的单元…

COUNT函数的使用

一、问题描述 今天在随手练习sql的时候&#xff0c;发现count查出来的数量和实际的数量不对&#xff0c;下面是我查询的sql 我想看看suitName字段一共有多少种数据 SELECTCOUNT(DISTINCT suitName) AS suitNameNum FROMrace_goods 得到的结果是suitName条数是22条&#xff0c…

countif是什么意思,如何运用?

countif是什么意思&#xff1f;countif函数是excel中对指定区域中符合指定条件的单元格计数的一个函数&#xff0c;简单来说就是算出某个参数的数量。那么&#xff0c;countif函数具体是如何运用的呢&#xff1f;小编分为两种情景为大家总结了操作步骤。 情景一&#xff1a;计算…