java中线程安全问题及解决方法、线程状态、线程间通信(线程等待唤醒机制)

news2024/11/15 2:00:19

线程安全

概述:

多线程访问了共享数据,此时会产生冲突(如:在多个线程中执行售卖货物的业务,要求是某个货被某个线程售卖后,其他线程应该不再可以售卖此个货,但是默认被某个线程售卖后,其他线程还是会售卖此货物,这里不合理,不过有解决的方法),这里的冲突指线程安全问题,这个问题是可以避免(始终保证一个线程在执行任务,当前线程任务执行完后才可以执行其他线程的任务),下面通过卖电影票实现线程安全问题:

实现Runnable接口的方式创建一个卖票线程类:

// 实现Runnbale接口的方式创建一个多线程实现类:
public class RunnableTmplClass implements Runnable {
    // 定义一个多个线程共享的数据(票):
    private int ticket = 10;
    // 重写run方法设置卖票任务:
    @Override
    public void run() {
        // 判断票是否存在,存在的话在售卖:
        for (int i = 0; i <= ticket; i++) {
            if (ticket > 0) {
                System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
                ticket--;
            }
        }
    }
}

创建三个线程实例售卖票:

// 创建3个线程售卖票:
public class SellTicketTest {
    public static void main(String[] args) {
        // 创建Runnable接口的实现类对象:
        RunnableTmplClass rt = new RunnableTmplClass();
        // 创建三个线程:
        Thread t1 = new Thread(rt);
        Thread t2 = new Thread(rt);
        Thread t3 = new Thread(rt);
        // 开启三个线程:
        t1.start();
        t2.start();
        t3.start();
    }
}

打印结果:
请添加图片描述
执行原理:请添加图片描述
解决线程安全问题

解决线程安全问题可以使用线程同步,线程同步一共有三种方式:同步代码块、同步方法、锁机制

同步代码块解决线程安全问题:

// 解决线程安全的第一种方式:使用同步代码块(把同步代码块锁住,只让一个线程在同步代码块中执行)
// 注意:锁对象可以是任意对象、但是多个线程保证应该使用同一个锁对象
public class RunnableTmplClass implements Runnable {
    private int ticket = 10;
    // 在run外面创建一个公共的锁对象(同步锁,对象监视器):
    Object obj = new Object();
    @Override
    public void run() {
        for (int i = 0; i <= ticket; i++) {
            // 创建同步代码块:将会出现线程安全的代码放到同步代码块中即可:
            synchronized (obj) {
                if (ticket > 0) {
                    System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
                    ticket--;
                }
            };
        }
    }
}

请添加图片描述
同步方法解决线程安全:

// 解决线程安全的第二种方式:使用同步方法,把访问了共享数据的代码抽取出来放到一个方法中,此方法前面添加一个修饰符:synchronized,方法体里面写访问了共享数据的代码:
// 注意:还可以在方法体内写synchronized代码块解决线程安全问题
public class RunnableTmplMethod implements Runnable {
    private /*static*/ int ticket = 10;
    @Override
    public void run() {
        for (int i = 0; i <= ticket; i++) {
            cellTicket();
            // cellTicketSecond();
        }
    }
    // 定义一个同步方法,第一种方式:
    public /*static ,这里static可以加上,提前定义的变量也是静态的才可以访问得到变量*/ synchronized void cellTicket() {
        if (ticket > 0) {
            System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
            ticket--;
        }
    };
    // 定义一个同步方法,第二种方式:
    public void cellTicketSecond() {
        synchronized (this) {
            if (ticket > 0) {
                System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
                ticket--;
            }
        };
    };
}

lock锁解决线程安全问题:

使用lock锁解决线程安全问题比synchronized先进些。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 解决线程安全的第三种方式:使用lock锁
// 使用步骤:1.在成员位置创建一个ReentrantLock对象 2.在可能会出现安全问题的代码前调用lock方法获取锁 3.在可能会出现安全问题的代码后调用unlock方法释放锁
public class RunnableTmplLock implements Runnable {
    private int ticket = 10;
    // 创建锁对象:
    Lock lk = new ReentrantLock();
    @Override
    public void run() {
        // for (int i = 0; i <= ticket; i++) {
        //     // 调用锁:
        //     lk.lock();
        //     if (ticket > 0) {
        //         System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
        //         ticket--;
        //     };
        //     // 释放锁:
        //    lk.unlock();
        // }

        // 下面方式为比较安全的,无论是否报错,都会释放锁
        for (int i = 0; i <= ticket; i++) {
            // 调用锁:
            lk.lock();
            if (ticket > 0) {
                try {
                    System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");// 没有处理线程安全前,这里打印的结果是:某个票可能被多次售卖,某个票可能一次也没有售卖
                    ticket--;
                } finally {
                    lk.unlock();
                }
            };
        }
    }
}

线程状态

线程状态概述:

线程的状态这里可以理解为状态的生命周期,也就是指从状态的创建到状态的结束的过程。状态依次可分为6种状态:new Therad()新建阶段—Runnable运行阶段(这里还有一种没有抢占到CPU的情况,称为blocked阻塞状态,阻塞状态和运行状态是可以相互切换的)—Terminated死亡状态—Timed_waiting休眠状态—waiting无限等待状态
请添加图片描述
休眠状态案例:

public class SleepStatic implements Runnable {
    private int ticket = 10;
    @Override
    public void run() {
        for (int i = 0; i <= ticket; i++) {
            if (ticket > 0) {
                try {
                    // 调用Thread类的sleep对程序进入休眠状态休眠2000毫秒在进入其他状态
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程:" + Thread.currentThread().getName() + "正在售卖第:" + ticket + "号票。");
                ticket--;
            }
        }
    }
}

无限等待状态(线程之间的通信):

一个正在无限期等待另一个线程执行一个特别的唤醒动作的线程处于这一状态。

无限等待状态的执行过程:有一个消费者线程需要某种需求,此时消费者会调用wait方法进入无限等待状态,生产者线程开始处理需求,当结果被生产者处理好后可以调用notify方法通知唤醒消费者可以进行消费了。

import java.util.Random;

// 创建一个消费者线程:告知生产者线程需要的结果种类和数量,调用wait方法,放弃cpu执行权,进入Waiting无限等待状态
// 创建一个生产者线程:处理消费者需要的结果,当结果处理好后,调用notify方法唤醒消费者来消费结果。
// 注意: 消费者线程和生产者线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个执行;同步使用的锁对象必须是唯一的,只有锁对象才能调用wait和notify方法
public class WaitNotifyStatic {
    public static void main(String[] args) {
        // 创建一个随机数变量
        final int[] num = {0};
        // 创建一个唯一的锁对象:
        Object obj = new Object();

        // 1.创建一个消费者线程:
        new Thread() {
            @Override
            public void run() {
                // 使用同步技术保证消费者线程和生产者线程只能有一个执行:
                synchronized (obj) {
                    // 向生产者发起业务需求:
                    System.out.println("A:消费者要求生产者处理业务,生成一个随机数:");
                    try {
                        // 调用wait方法进入无限休眠状态(这里会抛异常,使用try/catch处理即可):
                        System.out.println("B:消费者调用wait方法开启无限等待状态中");
                        obj.wait(); // 里面可以传入一个毫秒值,此时和sleep大概一样,当传入的时间小于生产者处理业务所用时间时,此时消费者会提前醒来进入runnable/blocked状态
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    // 生产者唤醒消费者后执行的代码:
                    System.out.println("E:消费者被生产者唤醒开始消费生产者所处理的结果:" + num[0]);
                };
            };
        }.start(); // 调用start方法执行消费者线程

        // 2.创建一个生产者线程:
        new Thread() {
            @Override
            public void run() {
                try {
                    // 生产者需要花费5秒钟处理业务(同样会抛异常,使用try/catch处理即可):
                    Thread.sleep(5000);
                    Random randoms = new Random();
                    num[0] = randoms.nextInt(100);
                    System.out.println("C:生产者花费了一段时间处理结果,生成一个随机数:" + num[0]);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 在同步代码块中唤醒消费者,继续执行消费者中wait之后的代码:
                synchronized (obj) {
                    System.out.println("D:生产者调用notify方法唤醒消费者:");
                    obj.notify(); // 当有多个消费者线程时,notify只能随机唤醒一个等待消费者线程,而obj.notifyAll可以唤醒所有等待线程
                };
            };
        }.start(); // 调用start方法执行消费者线程
    }
    // 提示:依次打印了a-e的结果,且执行完B后等待了一段时间后再开始执行后面代码
}

等待唤醒机制(线程间通信)

线程间通信概念: 多个线程处理同一个资源(这里往往指某个整套业务),但是处理的动作(线程的任务)却不同。在多个线程并发执行时,默认情况下cpu是随机切换线程的,但是有的时候我们需要让他们有规律的执行,此时多个线程之间就要进行一些协调通信,来完成多个线程处理某个整套业务。

保证线程间通信是有效利用资源: 多个线程间处理同一份数据时,避免对同一共享变量的争夺,我们可以通过某种手段(比如判断某个数据是否存在,存在时执行哪个线程,不存在时执行哪个线程)使各个线程能有效的利用资源,这种手段被称为等待唤醒机制。

等待唤醒中的方法:也就是上面用到的wait、notify、notifyAll。

当调用wait方法后,当前线程进入wait set中,等待别的线程执行notify将线程从wait set中释放出来从新进入到调度队列ready queue中,进入wait set中 此时不会消耗CPU,也不会去竞争锁。

notify: 选取所通知对象的wait set中等待时间最久的一个线程释放。

notifyAll:释放所通知对象的wait set中的全部线程。

注意:哪怕只通知了一个等待的线程,被通知的线程也不会立即恢复执行,因为它中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试获取锁(可能面临其他线程的竞争),获取锁成功后才能在当初调用wait方法之后的地方恢复执行。

下面是一个用户买包子的案例:

包子类:

// 1.创建一个包子类:
public class BaoZiClass {
    // 包子皮:
    String pi;
    // 包子馅:
    String xian;
    // 包子状态:是否有包子,用于线程间通信状态的判断
    boolean flag = false;
}

包子铺类:

// 2.创建一个包子铺类,用来生产包子的线程:当包子实例flag有包子时,包子铺调用wait方法进入等待状态;当flag没有包子时,包子铺生产包子,当包子生产好后修改flag状态为有并唤醒吃货线程吃包子
// 注意:包子铺线程和包子线程之间是互斥关系,两者只能有一个同时执行,因此需要使用同步技术

// 因为包子铺是一个线程,所以可以继承Thread类,并重写里面的run方法设置线程任务:
public class BaoZiPuClass extends Thread{
    // 定义一个包子变量:
    private BaoZiClass bz;

    // 使用包子铺带参数构造方法,为包子变量赋值:
    public BaoZiPuClass(BaoZiClass bz) {
        this.bz = bz;
    }

    // 重写run方法生产包子:
    @Override
    public void run() {
        // 定义一个序号,用于生产两种包子:
        int count = 0;

        // 使用循环让包子铺一直生产包子:
        while (true) {
            // 使用同步代码块解决线程安全问题:
            synchronized (bz) {
                // 如果有包子,调用wait方法进入等待,否则生产包子:
                if (bz.flag == true) {
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                };

                // 生产两种类型的包子:
                if (count % 2 == 0) {
                    bz.pi = "薄皮";
                    bz.xian = "粉丝馅";
                } else {
                    bz.pi = "厚皮";
                    bz.xian = "猪肉线";
                };

                System.out.println("A:包子铺正在生产第"+count+"个包子:"+bz.pi+","+bz.xian);
                count++;

                // 为了模拟真实环境,设置生产包子花费的时间为2秒钟:
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                // 当休眠2s后包子生产好了,修改包子的状态为有:
                bz.flag = true;

                // 唤醒吃货线程:
                System.out.println("B:包子铺已经生产好包子了,开始唤醒吃货吃包子:");
                bz.notify();
            };
        }
    };
}

吃货类:

// 3.创建一个吃货类(消费者)线程:对包子的状态进行判断,有包子的话吃,没有的话调用await方法等待。
public class ChiHuoClass extends Thread {
    // 设置一个包子变量:
    private BaoZiClass bz;
    // 使用构造方法为包子变量赋值:
    public ChiHuoClass(BaoZiClass bz) {
        this.bz = bz;
    };

    // 重写run方法设置吃包子的任务:
    @Override
    public void run() {
        // 使用死循环一直吃包子:
        while(true){
            // 使用同步代码块解决线程安全问题,这里锁对象和包子铺中的锁对象为同一个对象:
            synchronized (bz) {
                // 如果没有包子,调用wait进入等待状态
                if (bz.flag == false) {
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                // 开吃包子:
                System.out.println("C:吃货正在吃:"+bz.pi+","+bz.xian+"的包子");

                // 修改包子状态:
                bz.flag = false;

                // 唤醒包子铺生产包子:
                System.out.println("D:吃货唤醒包子铺生产包子:");
                bz.notify();

                System.out.println("-----------------------------");
            }
        }
    };
}

测试吃货吃包子:

// 4.测试吃货买包子及包子铺生产包子等业务:
public class TestClass {
    public static void main(String[] args) {
        // 创建一个包子:
        BaoZiClass bz = new BaoZiClass();

        // 创建一个包子铺,并开启线程:
        new BaoZiPuClass(bz).start();

        // 创建一个吃货,并开启线程:
        new ChiHuoClass(bz).start();

        // 提示打印结果为:包子铺生产一个包子,吃货就吃掉一个包子
    }
}

提示:本文图片等素材来源于网络,若有侵权,请发邮件至邮箱:810665436@qq.com联系笔者删除。
笔者:苦海

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

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

相关文章

JVM 教程

jvm教程jvm概述前言JVM 定义JVM 的作用查看自己的 JVMJVM&#xff0c;JRE 和 JDK 联系小结JVM 整体架构目标JVM 整体架构类加载子系统运行时数据区执行引擎小结JVM 常用参数配置IntelliJ IDEA 添加运行参数JVM 参数&#xff1a;跟踪垃圾回收JVM 参数&#xff1a;跟踪类的加载与…

手把手代码实现五级流水线CPU——第二篇:分支预测流水线

系列文章目录 第三篇&#xff1a;流水线控制逻辑 第一篇&#xff1a;初级顺序流水线 文章目录系列文章目录一、流水线硬件结构二、流水线各阶段的实现实现原理一、流水线硬件结构 取指阶段 PC增加器&#xff1a;用来计算下一条指令的地址valP 译码阶段 一次译码操作读出俩个寄…

学习vue的准备工作

一、前提&#xff1a; 1、vscode安装&#xff1a; https://blog.csdn.net/m0_55400356/article/details/1260267332、node.js安装&#xff1a; 已安装 16.0 或更高版本的 Node.js&#xff1b; https://www.runoob.com/nodejs/nodejs-install-setup.html3、安装vue&#xff…

hadoop之ranger权限配置(二)

文章目录一、编译ranger&#xff08;node12&#xff09;二、安装前环境准备&#xff08;node12&#xff09;三、安装RangerAdmin&#xff08;node12&#xff09;(root)五、Ranger Hive-plugin&#xff08;node10&#xff09;六、Ranger Hdfs-plugin&#xff08;node10、11&…

ansible远程控制及其相关操作

1.控制主机和受控主机通过root用户通过免密验证方式远程控住受控主机实施对应&#xff08;普通命令&#xff0c;特权命令&#xff09;任务&#xff08;以下所有结果均见截图&#xff09; (1)控住主机--server通过主机名匹配对应连接的受控主机 [rootserver ~]#vim /etc/hosts …

一本通 1267:【例9.11】01背包问题(详细代码+严谨思路+清晰图片)

经典01背包问题 这里给你3种方法 目录 DFS 思路&#xff1a; 代码&#xff1a; DFS记忆化 思路&#xff1a; 代码&#xff1a; 动态规划 思路&#xff1a; 代码&#xff1a; DFS 时间复杂度 &#xff1a;O(2^n) 思路&#xff1a; DFS求出所有选法&#xff0c;再用…

Git Stash、Git Merge、Git Rebase、Git Revert

GIT 日常使用总结git stashgit mergegit rebase将多个commit合并成一个commitgit revert撤销修改撤销commit 但是不撤销add 的内容撤销addGIT Rebase 补充说明&#xff1a;git stash git stash&#xff1a;将修改后的代码存储到本地的一个栈结构&#xff0c;将工作区和暂存区恢…

37-Vue之ECharts高级-交互API

交互API前言全局ECharts对象echartsInstance对象前言 本篇来学习下ECharts中交互API的使用 全局ECharts对象 全局 echarts 对象是引入 echarts.js 文件之后就可以直接使用的 init &#xff1a;初始化ECharts实例对象&#xff0c;使用主题 registerTheme&#xff1a;注册主题…

Avatar和虚拟场景如何影响用户VR社交行为

对于VR社交来讲&#xff0c;虚拟场景&#xff08;社交环境&#xff09;、Avatar&#xff08;虚拟化身&#xff09;是两个重要的元素&#xff0c;一个代表了人们在VR中互动的空间&#xff0c;而另一个则代表他们在这个空间中所扮演的角色。现阶段&#xff0c;Avatar有多种形态&a…

已解决import tensorflow.contrib.layers as layers导包错误

已解决W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library ‘cudart64_110.dll’; dlerror: cudart64_110.dll not found I tensorflow/stream_executor/cuda/cudart_stub.cc:29] Ignore above cudart dlerror if you do not ha…

opencv-python常用函数解析及参数介绍(八)——轮廓与轮廓特征

轮廓与轮廓特征前言1.获取轮廓通过膨胀与腐蚀获得轮廓通过梯度获取轮廓通过边缘检测获取轮廓2.寻找轮廓参数及作用对比3.轮廓特征前言 在前面的文章中我们已经学会了使用膨胀与腐蚀、使用梯度、使用边缘检测的方式获得图像的轮廓&#xff0c;那么在获得轮廓后我们可以对图像进…

​赛分科技冲刺科创板上市:拟募资8亿元,复星、高瓴为股东​

近日&#xff0c;苏州赛分科技股份有限公司&#xff08;下称“赛分科技”&#xff09;在上海证券交易所递交招股书&#xff0c;准备在科创板上市。本次冲刺上市&#xff0c;赛分科技计划募资8亿元&#xff0c;将用于20万升/年生物医药分离纯化用辅料、研发中心建设项目&#xf…

《收获,不止Oracle》读书笔记一

当今时代 技术人员&#xff0c;真正的差距其实在意识 1.忽略了知识的重点 20%的知识&#xff0c;解决80%的问题 2.从未考虑知识落地 知识要落地&#xff0c; 要思考应用的场合。 学习任何技术都是一样的&#xff0c;没有思考过你所学的某项技术有什么用&#xff0c;没有想…

P1827 [USACO3.4] 美国血统 American Heritage

题目描述 农夫约翰非常认真地对待他的奶牛们的血统。然而他不是一个真正优秀的记帐员。他把他的奶牛 们的家谱作成二叉树&#xff0c;并且把二叉树以更线性的“树的中序遍历”和“树的前序遍历”的符号加以记录而 不是用图形的方法。 你的任务是在被给予奶牛家谱的“树中序遍历…

计算机网络 | 湖科大教书匠

一、因特网概述 1、网络、互联网和因特网 网络是由若干结点和连接这些结点的链路组成 多个网络还可以通过路由器互联起来&#xff0c;这样就构成了一个覆盖范围更大的网络&#xff0c;互联网 因此&#xff0c;互联网是网络的网络&#xff08;Network of Networks&#xff0…

hadoop+ranger+kerberos页面权限配置(四)

一、原理介绍 hdfs 指令测试&#xff1a;hdfs dfs -mkdir /ranger 原理&#xff1a;根据路径进行文件夹操作赋权。一旦指定文件夹权限&#xff0c;则该用户可以操作该文件夹及该文件夹底下的子文件夹。 yarn 指令测试&#xff1a;hadoop jar /home/hadoop/module/hadoop-3.2.2…

跨平台数据库管理器DbGate

本文软件由网友 zxc 推荐&#xff1b;隔了很久才开始写&#xff0c;又隔了很久才想起来发 &#x1f602; 什么是 DbGate &#xff1f; DbGate 是跨平台的数据库管理器。支持 MySQL、PostgreSQL、SQL Server、MongoDB、SQLite 等的数据库管理器。能够在 Windows、Linux、Mac 下运…

Z函数(扩展KMP)

1,定义 z函数存储字符串s(长度n&#xff0c;下标从0开始&#xff09;与其所有后缀s[i,n-1](0<i<n-1)的最大公共前缀LCP的值&#xff08;一般默认z[0]0,有时是n) 2,思路 叫他扩展KMP是有原因的&#xff0c;因为思想相近&#xff0c;我们求取z[i]&#xff0c;尝试利用前…

20230102单独编译原厂RK3588开发板的开发板rk3588-evb1-lp4-v10的Android12的内核

20230102单独编译原厂RK3588开发板的开发板rk3588-evb1-lp4-v10的Android12的内核 2023/1/2 20:52 《RK3588_Android12_SDK_Developer_Guide_CN.pdf》 原厂的开发板rk3588-evb1-lp4-v10单独编译内核的方式&#xff1a; cd kernel-5.10 export PATH../prebuilts/clang/host/linu…

redis的常见命令

Redis 所有的 key&#xff08;键&#xff09;都是字符串。在谈基础数据结构时&#xff0c;我们讨论的是存储值的数据类型&#xff0c;主要包括常见的 5 种数据类型&#xff0c;分别是&#xff1a;String、List、Set、Zset、Hash 数据结构介绍 Redis 是一个 key-value 的数据库…