多线程初阶(二)

news2024/12/26 12:31:51

目录

前言:

synchronized

解析

可重入和不可重入问题

解析

Java中线程安全类

死锁问题

解析

解决死锁问题

解析

内存可见性

解析

volatile关键字

解析

wait,notify

解析

小结:


前言:

    针对上篇文章讲到的线程安全问题,我们需要保证一些指令的原子性,在代码中可以通过加锁实现。针对于加锁,这个是有一定的开销的,还有可能导致死锁问题。因此在加锁的时候要慎重考虑。

synchronized

    1)修饰普通方法是把锁加到当前引用对象上。

    2)修饰静态方法是把锁加到类对象上。

    3)修饰代码块,可以指定加到哪个对象上。

注意:

    如果两个线程针对同一个对象加锁,就会出现锁竞争/冲突,一个线程能够获得到锁,先到的线程先获得锁。另一个线程则需要阻塞等待,直到上一个线程解锁(方法执行完),这个线程就可以回到就绪队列,才能够获取到锁。

    这里加锁虽然在方法上修饰,但实际加锁都是加到对象上面的。只有两个线程针对同一个对象加锁,才会出现锁冲突。如果针对不同对象加锁,则都会获取到锁,不会产生阻塞等待。

class Cumsum {
    public int a = 0;
    synchronized public void add() {
        a++;
    }
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        Cumsum cumsum = new Cumsum();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                     cumsum.add();
                }

            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                    cumsum.add();
                }
            }
        });

        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(cumsum.a);

    }
}

解析

    上篇讲述到这段代码,不加锁运行结果是有bug的,加了锁之后就正确了。不加锁出bug的原因在上片文章有讲述到,就是典型的线程安全问题。那么为什么加了锁之后代码就是正确呢?

    首先这里有两个线程t1和t2,main线程会阻塞等待,这两个线程并发执行。如果第一个线程首先获取到锁,这个锁是加到cumsum对象上的,当第二个线程在尝试获取到这个对象的锁,就会产生阻塞等待(对象是同一个)。直到上一个线程释放了这个对象的锁,这个线程才可以获取这个锁成功。获取锁成功后读取到a的值肯定是save过的,即就是正确的数值。

可重入和不可重入问题

    如果一个线程在一个方法里尝试针对同一个对象加锁两次,第一次加锁成功后,第二次在尝试对这个方法加锁,就会产生阻塞等待。即就会阻塞在这个方法里,第一次加的锁没有办法释放,程序就会一直阻塞在这里,产生死锁问题。

    针对给一个对象加锁两次产生的死锁问题,在java里很可能会写出这样的代码,因此synchronized就设计为可重入锁。对于这样死锁现象,可重入锁就不会产生阻塞等待,就会放过它,即代码可以正常执行。对于这样原因产生死锁问题,即就是不可重入锁。

class Add {
    public static int a = 0;
    synchronized public  void add() {
        a++;
    }
    synchronized public void add2() {
        synchronized (this) {
           a++;
       }
    }
}
public class ThreadDemo11 {

    public static void main(String[] args) throws InterruptedException {
        Add add2 = new Add();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                    add2.add();
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 5000; i++) {
                    add2.add2();
                }
            }
        });
        //执行一次a++需要,load,add,save(都内存数据到寄存器,寄存器值++, 寄存器值写回内存)
        //由于两个线程是并发执行的,这些指令会随机组合(抢占式执行,随意调度),就会产生线程安全问题(不同的顺序,结果就会产生差异)
        //第二个线程读取的值是在第一个线程保存后读取的1,就会加2次(线程安全)
        //两次读取的值都为0,则最终只加1. 在一次线程切换中,另一个线程可能会执行多次三步流程(线程不安全)
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(Add.a);
    }
}

解析

    如果第一个线程先执行,会给add2对象加锁成功。这个时候第二个线程在尝试对于这个对象加锁就会产生阻塞等待。当第一个线程释放锁之后,第二个线程就会针对于这个对象加锁成功,执行代码块的时候又会针对这个对象加锁第二次,由于synchronized是可重入锁,即在这里不会产生死锁问题,即代码就会正常执行。

Java中线程安全类

    Vector  HashTable  ConcurrentHashMap  StringBuffer 这些集合类中都内置了synchronized锁,多线程中线程是安全的。String类由于不可修改性,即天然就是线程安全的。

    AyyayList  LinkedList  HashMap  TreeMap  HashSet  TreeSet  StringBuilder 这些集合类中在线程安全问题需要手动加锁。

死锁问题

    上面说的在一个线程里给一个对象加两次锁,如果锁是不可重入锁,那么就会产生死锁。如果线程1先获取到锁A,被调度走,线程2先获取到锁B,再尝试获取锁A,就会阻塞等待,线程1调度回来获取锁B,也会阻塞等待。这个时候两个线程都在等对方释放锁,程序就会卡着不动了,产生了死锁问题。多个线程多把锁,如果每个线程都获取到锁,并且都在等对方释放锁,那么每个线程都会卡着不动,产生死锁问题。

    死锁问题的核心就是循环等待,想要解决死锁问题,那么就需要打破这种循环等待。

public class ThreadDemo12 {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o1) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2) {
                        System.out.println("aaaa");
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o2) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o1) {
                        System.out.println("bbbbb");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

解析

    线程1先个o1对象加锁,然后sleep(),线程2给o2加锁,然后sleep()。接下来线程1尝试获取o2的锁,线程2尝试获取o1的锁,即两个线程都会阻塞等待,产生死锁。

解决死锁问题

    给锁编号,约定获取锁的顺序,从小到大或者从大到小。任意线程在加锁的时候都遵循这样的规则,就可以打破循环等待的问题,那么死锁问题也就解决了。

public class ThreadDemo12 {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o1) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2) {
                        System.out.println("aaaa");
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (o1) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2) {
                        System.out.println("bbbbb");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

 解析

    线程1先获取o1再获取o2的锁,线程2也遵循这样的规则。调整了获取锁的顺序后,可以清楚看见代码正常执行了。

内存可见性

    如果针对于同一个变量即读又写,那么就会涉及内存可见性问题。实质上是编译器优化导致的bug。

    对于一个变量的修改,首先需要读取内存的数据到寄存器中,然后在寄存器中修改这个变量,最终写回到内存中。编译器优化可能会认为这个变量是不可变的,即在每次读数据的时候,只读取寄存器中的值,而不是修改后内存中的数据。这就导致读的数据就是修改之前的。

    一个线程针对于一个变量进行修改操作,同时另一个线程针对这个变量读取操作。此时读取到的值不一定是修改后的值,这个线程没有感知到这个变量的变化。

class Counter {
    //不能修饰局部变量
    //局部变量在不同的线程里占用不同的栈空间,意味这就是不同的变量(每个线程都有自己的栈空间)
    public int flag = 0;
}
public class ThreadDemo16 {
    public static void main(String[] args) {

        Counter counter = new Counter();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (counter.flag == 0) {

                }
                System.out.println("aaaa");
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Scanner scanner = new Scanner(System.in);
                counter.flag = scanner.nextInt();
            }
        });
        t1.start();
        t2.start();
    }
}

解析

    当t2线程修改掉flag值为2时,t1线程在while中死循环。因为t1线程读取到的值是没有修改的flag。这就是编译器优化导致认为flag是不可变的,即每次都是读取寄存器中的值,而不是t2线程修改后内存中的值。 

volatile关键字

    解决内存可见性,使用volatile关键字。声明这个变量是可变的,即告诉编译器在每次读取数据时,需要读取内存中的数据,而不是寄存器中的数据。这个时候编译器就不会随便优化了。

class Counter {
    //不能修饰局部变量
    //局部变量在不同的线程里占用不同的栈空间,意味这就是不同的变量(每个线程都有自己的栈空间)
    volatile public int flag = 0;
}
public class ThreadDemo16 {
    public static void main(String[] args) {

        Counter counter = new Counter();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (counter.flag == 0) {

                }
                System.out.println("aaaa");
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Scanner scanner = new Scanner(System.in);
                counter.flag = scanner.nextInt();
            }
        });
        t1.start();
        t2.start();
    }
}

 解析

    当给flag加上volatile关键字声明是可变的之后,while循环后的语句顺利打印了,说明t1线程读取到了t2线程修改后的值。因为voiatile修饰后,就会认为这个变量是可变的,即每次都会同步内存中的数据,即也就解决了内存可见性问题。

wait,notify

    由于线程的抢占式执行,随机调度。wait可以让线程主动放弃cpu的调度,进入阻塞队列。让其他线程可以被调度,可以控制线程的调度时机。当使用wait主动放弃cpu的时候,需要其他线程通过notify来唤醒该线程,进入就绪队列。wait和notify都是Object类下的方法。

    wait主动放弃cpu调度的机制,首先会释放锁,然后线程阻塞等待。那么在释放锁的时候需要有锁,即需要先得到锁然后在释放锁,进入阻塞队列。为什么这样设定呢?释放锁之后其他线程可以给这个对象加锁,就不会导致这个对象一直被加锁。wait不加任何参数就是死等。某个线程调用wait方法,就会进入阻塞队列(无论是哪个对象),此时就处在WAITING状态。

    notify通知线程唤醒机制。再唤醒线程也需要获得锁才可以唤醒线程。即首先需要获取锁,然后调用notify方法,唤醒线程进入就绪队列。notify只能唤醒同一个对象调用wait所阻塞的线程,如果有多个线程都在阻塞,则随机唤醒一个。notifyAll可以全部唤醒,一起进入就绪队列。这里的notify唤醒wait不会有任何异常。

public class ThreadDemo17 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t1前");
                synchronized (object) {
                    try {
                        object.wait(); //不加任何参数就是死等
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t1后");
            }
        });

        //notify只能唤醒同一个对象上的等待线程
        //如果和wait对象不一致,则不生效
        //多个线程wait的时候,notify随机唤醒一个,notifyAll全部唤醒,一起竞争锁
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t2前");
                synchronized (object) {
                    object.notify();
                }
                System.out.println("t2后");
            }
        });
        t1.start();
        Thread.sleep(500);
        t2.start();
    }
}

解析

    需要保证先启动t1线程通过synchronized先给object加上锁,然后通过wait方法释放锁,使该线程阻塞等待。当t2线程执行的时候,也是通过synchronized先给object加上锁,然后object对象调用notify方法通知t1线程,进入就绪队列。可以看见代码的执行顺序也是这样。这里wait和notify方法的调用对象需要一致,才能明确具体通知哪一个线程。

小结:

    与大家共勉歌德的名言:志向和热爱是伟大行为的双翼。 

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

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

相关文章

VSCode\\VS2017下CPP环境的配置

VSCode下C环境配置一些问题VSCode下配置C环境&#xff1a;VSCode与boost总结&#xff1a;坑位待填&#xff1a;VSCode中3个json文件的作用&#xff1a;环境配置出现的问题以及解决VS2017 配置 C环境VS配置boost库包含项目中的自定义的.hpp文件&#xff0c;.h文件VSCode下配置C环…

公众号网课题库接口

公众号网课题库接口 本平台优点&#xff1a; 多题库查题、独立后台、响应速度快、全网平台可查、功能最全&#xff01; 1.想要给自己的公众号获得查题接口&#xff0c;只需要两步&#xff01; 2.题库&#xff1a; 题库&#xff1a;题库后台&#xff08;点击跳转&#xff09;…

4.验证面试高频问题整理(附答案)

目录 Q76.package如何使用 Q77.如何在子类中调用父类中的方法 Q78.bit[7:0]和byte有什么区别 Q79.类中的方法和类外的方法有什么区别 Q80.如何将类中的方法定义在类外 Q81.modport的用途是什么 Q82.struct和union的异同 Q83.$rose和posedge区别 Q84.如何在fork...join结…

[附源码]Python计算机毕业设计Django人事管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

js对象易混淆知识

js对象易混淆知识 __proto__ vs prototype __proto__和constructor属性是对象所独有的。 __proto__属性的作用就是当访问一个对象的属性时&#xff0c;如果该对象内部不存在这个属性&#xff0c;那么就会去它的__proto__属性所指向的那个对象&#xff08;父对象&#xff09;…

三菱FX5U PLSV指令-可变速度输出

三菱FX5U PLSV指令-可变速度输出,程序如下 该指令用于输出带旋转方向输出的变速脉冲。只支持CPU模块 *1 只能使用Y。 *2 输出模式为CW/CCW时&#xff0c;请指定CCW轴。使用Y时&#xff0c;只能指定本轴的SIGN输出或通用输出。 *3 不能使用T、ST、C 以上是指定FX3操作数得情况…

JVM总结全

虚拟机 HotSpot 默认虚拟机 JRockit HotSpot融合了JRockit jdk8初步融合完成 没有解释器&#xff0c;只有编译器 IBM J9 结构图 类加载子系统Q 1.类加载器 ​ 启动类加载器&#xff08;引导类加载器&#xff09;Bootstrap ClassLoader ​ 加载java 核心类库&#xff0c;…

QT + FFmpeg 5.x + x264 + x265 + SDL2 音视频播放器

QT FFmpeg 5.x x264 x265 SDL2 音视频播放器 使用了QT的QML设计界面,人机交互; 使用了FFmpeg 5.x x264 x265 SDL2 完成了音视频的解析到播放; 阅读了ffplay的源码,用到了ffplay的核心思想. 想熟悉ffmpeg和ffplay的朋友,都可以参考学习. 代码自取: https://github.c…

秒杀实现技巧

需求分析 “秒杀”这个词在电商行业中出现的频率较高&#xff0c;如京东或者淘宝平台的各种“秒杀”活动&#xff0c;最典型的就是“双11抢购”。 “秒杀”是指在有限的时间内对有限的商品数量进行抢购的一种行为&#xff0c;这是商家以“低价量少”的商品来获取用户的一种营…

Golang原理分析:切片(slice)原理及扩容机制

《Go语言精进之路》切片相关章节学习笔记及实验。 1.切片原理 说切片之前&#xff0c;先看看Go语言数组。Go数组是一个固定长度的、容纳同构类型元素的连续序列&#xff0c;因此Go数组类型具有两个属性&#xff1a;长度及类型&#xff1a; var a [1]int var b [2]byte var c …

【Web安全】文件上传漏洞

目录 1. 文件上传漏洞概述 1.1 FCKEditor文件上传漏洞 1.2 绕过文件上传检查功能 2. 功能还是漏洞 2.1 Apache文件解析 2.2 IIS文件解析 2.3 PHP CGI路径解析 2.4 利用上传文件钓鱼 3. 设计安全的文件上传功能 1. 文件上传漏洞概述 文件上传漏洞是指用户上传了一个…

R语言学习笔记——入门篇:第四章-基本数据管理

# R语言R语言学习笔记——入门篇&#xff1a;第四章-基本数据管理 文章目录一、示例二、创建新变量三、变量的重编码四、变量的重命名4.1、交互式编辑器4.2、函数编程五、缺失值5.1、重编码某些值为缺失值5.2、在分析中排除缺失值六、日期值6.1、将日期值转换回字符型变量6.2、…

使用 Learner Lab - 在 Lambda 建立 Pillow 层,进行 S3 的图片镜相操作

使用 Learner Lab - 在 Lambda 建立 Pillow 层&#xff0c;进行 S3 的图片镜相操作 AWS Academy Learner Lab 是提供一个帐号让学生可以自行使用 AWS 的服务&#xff0c;让学生可以在 100 USD的金额下&#xff0c;自行练习所要使用的 AWS 服务&#xff0c;如何进入 Learner La…

[论文阅读] 颜色迁移-Automated Colour Grading

[论文阅读] 颜色迁移-Automated Colour Grading 文章: Automated colour grading using colour distribution transfer, [paper], [matlab代码], [python代码] 1-算法原理 本文算法分为2个大步骤, 首先使用IDT(Iterative Distribution Transfer)方法得到初步的结果, 这个结果…

优雅的springboot参数校验(二)

7. 集合校验 有这样一种场景&#xff0c;前端请求后端接口时&#xff0c;需要传递的是一个数组&#xff0c;数组的元素是一个对象&#xff0c;并且希望后台收到参数后可以对数组集合中的元素元素对象的属性进行校验&#xff0c;如果后台直接以List的来接收参数&#xff0c;约束…

[附源码]计算机毕业设计基于springboot的云网盘设计

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

linux网络编程之tcp

相关函数 int socket(int domain,int type,int protocol);参数&#xff1a; domain&#xff1a; AF_INET AF_INET6 AF_UNIX,AF_LOCAL AF_NETLINK AF_PACKET type&#xff1a; SOCK_STREAM: 流式套接字&#xff0c;唯一对应于TCP SOCK_DGRAM:数据报套接字&#xff0c;唯一对应着…

【第七章 MySQL体系结构、存储引擎、InnoDB、MyISAM、Memory、存储引擎特点及选择】

第七章 MySQL体系结构、存储引擎、InnoDB、MyISAM、Memory、存储引擎特点及选择 1.MySQL体系结构&#xff1a; ①连接层&#xff1a; 最上层是一些客户端和链接服务&#xff0c;包含本地sock 通信和大多数基于客户端/服务端工具实现的类似于TCP/IP的通信。主要完成一些类似于连…

[Power Query] 日期和时间处理

Power Query查询编辑器为日期和时间数据提供了强大而快捷的处理方式 例1: 从日期中提取年、月份、日、季度、周、天等信息 数据源 步骤1:将数据源导入到Power BI Desktop&#xff0c;单击【转换数据】选项&#xff0c;进入Power Query查询编辑器界面 步骤2:选中"日期&qu…

【概念】数据仓库和数仓建模

数据仓库 数仓主要特征 面向主题&#xff1a;每个需求和表都属于一个主题&#xff0c;可以用主题来对数仓的表分门别类集成性&#xff1a;将异构数据源&#xff0c;比如MySQL和服务器埋点日志&#xff0c;统一转换成结构化的hive表数据存储到ODS层非易失性&#xff1a;对历史…