java多线程之线程安全(重点,难点)

这篇具有很好参考价值的文章主要介绍了java多线程之线程安全(重点,难点)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

由于操作系统中,线程的调度是抢占式执行的,或者说是随机的,这就造成线程调度执行时,线程的执行顺序是不确定的,虽然有一些代码在这种执行顺序不同的情况下也不会运行出错,但是还有一部分代码会因为执行顺序发生改变而受到影响,这就会造成程序出现Bug,对于多线程并发时会使程序出现bug的代码称作线程不安全的代码.

本质原因: 线程在系统中的调度是无序的/随机的(抢占式执行)

1. 线程不安全的原因:

序号 线程不安全的原因
1 抢占式执行(罪魁祸首)
2 多个线程同时修改同一个变量
3 修改操作不是原子的
4 内存可见性
5 指令重排序

多线程不安全的原因主要分为一下三种:

1.原子性

  • 多行指令,如果指令前后有依赖关系,不能插入其他影响自身线程执行结果的指令

2.可见性

  • 系统调用CPU执行线程内,一个线程对共享变量的修改,另一个线程能够立刻看到

3.有序性

  • 程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序)

1.1 抢占式执行

我们通过下面的代码来进行讲解:

class Demo{
    private static int count;
	//
    public static void countAdd(){
        count++;
    }
	//返回count
    public static int getCount() {
        return count;
    }
}

public class ThreadDemo11 {

	public static void main(String[] args) throws InterruptedException {
	        Thread t1 = new Thread(()->{
	            for (int i = 0; i < 50000; i++) {
	            //执行50000次count++
	                Demo.countAdd();
	            }
	        });
	        Thread t2 = new Thread(()->{
	            for (int i = 0; i < 50000; i++) {
	            //执行50000次count++
	                Demo.countAdd();
	            }
	        });
	        //执行t1线程
	        t1.start();
	
	        //执行t2线程
	        t2.start();
	
	        //等待t1,t2线程执行完
	        t1.join();
	        t2.join();
	
	        //打印此时的count值
	        System.out.println(Demo.getCount());
    }

此时我们可以看到,线程 t1 和线程 t2 分别对count进行50000次的自增,那么我们最后打印的值应该是100000吧~

此时我们运行看一下结果:

java 常见的线程安全问题,线程安全,java,jvm,开发语言

java 常见的线程安全问题,线程安全,java,jvm,开发语言
java 常见的线程安全问题,线程安全,java,jvm,开发语言

大家会发现,每次的运行结果都不一样,并且没有一次的值是正确的,到底是为什么呢?

小鱼给大家画图解释下:

java 常见的线程安全问题,线程安全,java,jvm,开发语言

  • load: 从内存中将值读取到cpu寄存器中

  • add: 将cpu寄存器的值进行+1

  • save: 将寄存器的值读取到内存中

此时这里的结果错误,就是因为count++不是原子的而造成.

为什么这么说呢? 为了方便大家理解,我们用两个cpu内核来举例.内两个圆圆的东西就是 t1 的工作内存和 t2 的工作内存.

工作内存包含:cpu寄存器和缓存…

java 常见的线程安全问题,线程安全,java,jvm,开发语言

此时我们可以观看到,我们是在执行完 t1 线程的count++之后再去执行 t2 的count++的.此时我们的count是2,是正确的~~

但是呢,由于线程的随即调度,我们可能会存在这种情况:

java 常见的线程安全问题,线程安全,java,jvm,开发语言

由于线程是抢占式执行的,所以可能会存在,当 t1 线程刚进行 load 之后, t2 就也进行了 load,就是上图中左侧线程执行的顺序.当然,类似于这种插队式的组合方法多的数不清,上图运行的结果是count=1,但是我们的count已经自增两次了啊,应该是2的,此时出现的错误,就是线程安全的问题.

下面是一部分可能出现的随机排列情况:

java 常见的线程安全问题,线程安全,java,jvm,开发语言

1.2 多个线程修改同一个变量

上述的多线程安全问题就是因为多个线程对同一变量进行修改造成的.

大家看下面的表格:

原因 安全性
一个线程修改一个变量 安全
多个线程修改一个变量 不安全
多个线程读取多个变量 安全
多个线程修改多个不同变量 安全

1.3 修改操作不是原子的

原子性: 不可分割的最小单位.

java 常见的线程安全问题,线程安全,java,jvm,开发语言

通过上述过程我们知道,正因为有些操作不是原子的,导致两个或多个线程的指令排序存在更多的变数,自然就引发线程不安全的问题.

关于内存可见性,和指令重排序在后面讲到…

那我们如何解决上述的线程安全问题呢?

我们通过count++ 举例,如何让count++ 变成原子的呢?

我们可以通过加锁的方式将count++ 变成原子的.

锁(synchronized)

锁的核心操作分为两个:

(1) 加锁 : 当我们进入这个房间之后,别人就无法进入这个房间.

(2) 解锁 : 只有我们打开门,释放锁之后,别人才可以进入这个房间.

如果我进入这个房间之后,还有别人想要进入,就需要等我释放锁,走出这个房间才可以进入,别人在门口等待我的过程称为"阻塞".

当然,由于线程是抢占式执行的,所以如果我想进入这个房间就需要和那些都想进入这个房间的人进行争抢,当我进入这个房间之后,此时一群人只能在外面等着我,不能干别的事情,当我出了这个房间之后,他们又会开始新一轮的争抢,如果我还有进入房间的需要,我也会再次和他们一起争抢.

锁(synchronized) 这个关键字还有一个参数需要传进去,这个参数的类型需要是object或者它的子类

1.一个锁对应一个锁对象.

关于这个锁对象的用途,这里通过生活中的例子讲解…

我们假设有两个老师,A,B他们都是教英语的,学校规定,这个学期英语课只有50节,工资按照你们每个人上课的次数决定,由于上课的教室只有一个,所以A,B老师先到先得.因为老师上课的时候不能被打扰,所以规定,这个教室一次只能进入一个老师,假设A老师在讲课,那么B老师如果想讲课的话只能等着,且不能做别的事情,只能等A老师讲完课并且出门之后,再次和A老师竞争上课的资格.

上面涉及到的知识点有:

(1) A,B老师先到先得(抢占式执行)(也可以称为锁竞争)

(2) 学校规定,一次只能进去一个老师(加锁)

(3)如果A老师讲课,B老师如果想讲课的话只能等着(阻塞)

(4) 只能等A老师讲完课并且出门之后(执行完锁内的代码块,并释放锁)

也可以用代码举例:

我们这里假设两个老师讲的课程一模一样,这50节课的内容从来没变.

class Teacher{
    static Object object = new Object();
    public static void AttendClass(){
        //此时两个进程都调用这个方法,为了防止老师讲课被打扰
        //对这个讲课进行加锁
        //此时这个锁的参数,也可以称为锁对象
        //我们可以将锁对象理解为教室,
        synchronized (object){
        //课程
            System.out.println("ABCDEFG....");
            ThreadDemo14.size--;

        }
    }
}
public class ThreadDemo14 {
   static int size = 50;
    public static void main(String[] args) {
        Thread A = new Thread(()->{
            while (size >= 0)
                    Teacher.AttendClass();
        }) ;
        Thread B = new Thread(()->{
            while (size >= 0)
                    Teacher.AttendClass();
        }) ;

        A.start();
        B.start();
    }
}

上述讲的例子是只在教室里面讲英语(只有一个锁,且只有一个锁对象).下面这个例子是不仅仅是讲英语…

2.多个锁对应一个锁对象.

依旧是先语言表达一下:

A,B两个老师,A英语老师,B语文老师,但是呢,只有一个教室可以用来上课,此时A,B老师就会为了这个教室的使用权而争抢,没有抢到的老师只能等着,依旧是50节课,谁上的课多谁工资高!

代码举例:

class Teacher{
    static Object object = new Object();
    public static void EnglishClass(){
    //注意这里
        synchronized (object){
            System.out.println("ABCDEFG...."+ThreadDemo14.size);
            ThreadDemo14.size--;

        }
    }

    public static void ChineseClass(){
    //注意这里
        synchronized (object){
            System.out.println("鹅鹅鹅,曲项向天歌...."+ThreadDemo14.size);
            ThreadDemo14.size--;

        }
    }
}
public class ThreadDemo14 {
   static int size = 50;
    public static void main(String[] args) {
        Thread A = new Thread(()->{
            while (size >= 0)
                    Teacher.EnglishClass();
        }) ;
        Thread B = new Thread(()->{
            while (size >= 0)
                    Teacher.ChineseClass();
        }) ;

        A.start();
        B.start();
    }
}

上述代码和之前的代码的区别就是,我上了两个锁,但是锁对象一样(多个锁,但是只有一个锁对象),这是什么意思呢? 意思就是,虽然我们讲的是不同的课程(锁的代码块不一样),但是这个锁对象一样,意味着我们需要在同一个教室讲课,一次只能有一个老师讲课.

更通俗点理解就是:不管你多少个老师,不管你教什么,只要是同一个教室(锁对象一样)那么,你就只能等这个教室空出来才能用…在此期间

2.多个锁对应多个锁对象.

但是呀,如果多个老师争抢一个教室,那么一定会引发不满的,所以个,学校就多建造了几个教室,那么这些老师就可以在不同的教室上课了…

class Teacher{
//教室1
    static Object classroom1 = new Object();
  //教室2
    static Object classroom2 = new Object();
    public static void EnglishClass(){
    //此时锁对象不同,意味着在不同的教室
        synchronized (classroom1){
            System.out.println("ABCDEFG...."+ThreadDemo14.size);
            ThreadDemo14.size--;

        }
    }

    public static void ChineseClass(){
    //此时锁对象不同,意味着在不同的教室
        synchronized (class2room){
            System.out.println("鹅鹅鹅,曲项向天歌...."+ThreadDemo14.size);
            ThreadDemo14.size--;

        }
    }
}

此时我们大概了解锁对象的意思了,科学一点的解释如下:

总结:
(1) 多个代码块使用了同一个同步监视器 ( 锁 ) , 锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中任何一个代码块.

(2) 多个代码块使用了同一个同步监视器 ( 锁 ) , 锁住一个代码块的同时,也锁住了所有该锁的所有代码块,但是没有锁柱使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块.

4. 找出代码错误

你也为锁这块讲完了吗?

天真! 上面还有一处错误! 不知道同学发现了没有…

我们来运行下两个英语老师的那个例子:
java 常见的线程安全问题,线程安全,java,jvm,开发语言

咦,怎么打印出来了-1,这是为什么呢?

有一种情况我们没有考虑到,就是…

java 常见的线程安全问题,线程安全,java,jvm,开发语言
java 常见的线程安全问题,线程安全,java,jvm,开发语言
java 常见的线程安全问题,线程安全,java,jvm,开发语言

那么怎么解决呢?

java 常见的线程安全问题,线程安全,java,jvm,开发语言
此时的运行结果:

java 常见的线程安全问题,线程安全,java,jvm,开发语言
可能这个方法不是最优的,因为每次进入锁又需要进行一次if(),这些都是时间上的开销!!!

5. 锁的另一种用法

我们可以用synchronized来修饰方法,如果是用锁来修饰方法的话,我们不需要给这个锁设置参数,如果这个方法是静态的,那么这个锁的对象就是类名(该方法所处的类).class,如果是非静态的方法,那么谁调用这个方法,谁就是锁对象,也就是this.

代码如下:

非静态方法:

   public  void func1(){
        synchronized (this){
            
        }
    }
    //两者等价
    synchronized public void func2(){
        
    }

静态方法:

class A1{
    public static void func1(){
        synchronized (A.class){

        }
    }
    //两者等价
    synchronized public void func2(){

    }

}

1.4 内存可见性

什么是内存可见性呢? 大家看下面的代码,猜猜运行结果!

public class ThreadDemo15 {
    static boolean flog = false;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (!flog){
                //什么都不打印
            }
            //循环执行结束
            System.out.println("循环执行结束");
        });

        t1.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flog = true;
        System.out.println("flog改为true");
    }
}

两个选项:

A. 打印完flog之后就程序结束
B. 打印完flog之后程序没有结束

现在请看运行结果…

java 常见的线程安全问题,线程安全,java,jvm,开发语言

正确答案是B,为什么呢?

小鱼为大家解答.

我们通过一个例子来解释:

一天呢,玉帝派给孙悟空一个任务,要求他一直看着唐僧,看他会不会怀孕…
这孙悟空一听,男的会怀孕? 那不可能啊…

于是玉帝每次问孙悟空,孙悟空看都不看唐僧一眼就说没怀孕,不知过了多久,唐僧因为喝了女儿国的水,怀孕了!!! 就在唐僧怀孕之后,玉帝问孙悟空,你师傅怀孕了嘛?孙悟空依旧说不屑的回答:“没有”,殊不知唐僧孩子都快生出来了…

我们刚才的程序为什么会在我修改flog之后也一直在继续执行呢?

是因为,while(flog != false) 需要两步,从内存中读取flog的值到自己的寄存器,再将寄存器的值和false比较,由于呢~读内存(load)的操作很是麻烦,所以编译器就自作主张,想要优化这个代码,于是就不再去内存中读取了,直接将自己寄存器的值和false进行比较,但是这一不读取就出现了意外,代码内心: 你小子看我一眼啊,我都变了,我都变成true了,你丫还在循环!!!

上面出现线程安全的主要原因就是编译器优化,因为读内存的操作比读寄存器要慢几千倍,所以编译器为了运行效率,擅自做了决定.

所谓内存可见性就是在多线程的情况下,编译器对于代码优化,产生了误判,从而引起的一系列Bug,进而导致咱们的代码bug了.

解决内存可见性引发的线程安全问题(volatile)

我们对比这个代码:

public class ThreadDemo15 {
    static boolean flog = false;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (!flog){
                //什么都不打印
                //加入了一个时间限制
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //循环执行结束
            System.out.println("循环执行结束");
        });

        t1.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flog = true;
        System.out.println("flog改为true");
    }
}

运行结果:

java 常见的线程安全问题,线程安全,java,jvm,开发语言

这个代码有sleep,加上sleep循环执行速度就变得很慢,当循环次数下降了,此时load不再是负担,编译器就没必要优化了.

但是我如果我就想让循环空转,并且还不能出错,该怎么处理?

咱么可以让编译器针对这个场景暂停优化.

如何做到呢?

有请接下来的主角: volatile

我们通过使用volatile关键字修饰变量,此时该变量就会禁止编译器优化,能够保证每次都是从内存中重新读取数据.

java 常见的线程安全问题,线程安全,java,jvm,开发语言

volatile是一个类型修饰符,作用是作为指令关键字,一般都是和const对应,确保本条指令不会被编译器的优化而忽略。

1.5 指令重排序

volatile还有一个用处就是禁止指令重排序.

指令重排序也是编译器优化的策略,调整了代码的执行顺序,让程序更高效.

前提: 保证代码逻辑不变,并且调整之后的结果要和之前是一样的.

关于指令重排序,小鱼给大家举个例子吧…

妈妈今天让小鱼去菜市场买菜,把买菜的清单列给了小鱼.
java 常见的线程安全问题,线程安全,java,jvm,开发语言

小鱼发现,如果按照清单上的顺序购买,比较浪费时间.

java 常见的线程安全问题,线程安全,java,jvm,开发语言

小鱼于是呢,就想换个路线…

java 常见的线程安全问题,线程安全,java,jvm,开发语言

当小鱼买完西红柿之后呢,妈妈给小鱼打电话,问小鱼买完西红柿了嘛,小鱼说买完了,妈妈说,那就抓紧回家吧~~

此时小鱼就到了家里,妈妈看到小鱼手里的西红柿陷入了沉思…

怎么只有西红柿,别的菜呢? 然后小鱼就挨打了,因为妈妈以为他是按照清单上的顺序去买的,当看到小鱼买完西红柿之后以他都买好了,就让他回来了,结果今天只能吃凉拌西红柿了…

我们也可以用代码来解释:

class Student {
	//成员变量
    static Student s;
    //成员方法
    public static Student getS() {
        return s;
    }
	//
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            s = new Student();
        });
        Thread t2 = new Thread(()->{
            if(s != null) {
                s.getClass();
            }
        });
		t1.start();
		t2.start();
    }
}
s = new Studnet();

这和new的过程可以大体分为三部分.

1.申请内存空间
2.调用构造方法
3.把对象的引用赋值给 s

如果是在单线程的环境下,1,2,3的指令可以发生重排序,1先执行,2和3谁先谁后都可以.

在单线程情况下,这种优化并不会出现什么问题,但是在多线程情况下就不好说了…

java 常见的线程安全问题,线程安全,java,jvm,开发语言
此时呢,为了避免指令重排序产生的线程安全问题,我们需要将文章来源地址https://www.toymoban.com/news/detail-824317.html

   volatile static Student s;//进行volatile修饰

到了这里,关于java多线程之线程安全(重点,难点)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • MYSQL中的锁(面试难点重点)

    首先说一下 这个加锁是个啥子过程呢 我们拿一条记录举例,这个记录就放在这,没人操作它,他就没生成锁结构, 直到有个事务操作它了,然后给它才生成了个锁结构,锁结构两个参数 trx(生成该锁的事务) is_waiting(正在等待就是:true 没在等待就是 false) (锁里面很多参数 这里这

    2024年02月16日
    浏览(50)
  • Java多线程之线程安全问题

    我们知道操作系统中线程程的调度是抢占式执行的, 宏观上上的感知是随机的, 这就导致了多线程在进行线程调度时线程的执行顺序是不确定的, 因此多线程情况下的代码的执行顺序可能就会有无数种, 我们需要保证这无数种线程调度顺序的情况下, 代码的执行结果都是正确的

    2023年04月17日
    浏览(46)
  • java基础之线程安全问题以及线程安全集合类

    当多个线程同时访问同一个临界资源时,原子操作可能被破坏,会导致数据丢失, 就会触发线程安全问题 临界资源: 被多个线程同时访问的对象 原子操作: 线程访问临界资源的过程中不可更改和缺失的操作 互斥锁 每个对象都默认拥有互斥锁, 该锁默认不开启. 当开启互斥锁之后

    2024年01月18日
    浏览(59)
  • Java中的多线程——线程安全问题

    作者:~小明学编程   文章专栏:JavaEE 格言:热爱编程的,终将被编程所厚爱。 目录 多线程所带来的不安全问题 什么是线程安全 线程不安全的原因 修改共享数据 修改操作不是原子的 内存可见性对线程的影响 指令重排序 解决线程不安全的问题 synchronized 互斥 刷新内

    2024年02月03日
    浏览(81)
  • 【Java】线程安全问题

    在之前的文章中,已经介绍了关于线程的基础知识。 我的主页: 🍆🍆🍆爱吃南瓜的北瓜 欢迎各位大佬来到我的主页进行指点 一同进步!!! 我们创建两个线程t1和t2,对静态变量count执行++操作各50000次。 我们的预期结果是100000。但是当两个线程分别执行++操作时最后的结果

    2024年04月10日
    浏览(46)
  • c#期末复习题重点难点题

    2. (单选题, 9分)在.NET中,.NET Framework由( )组成。 A. FCL和CLR   -开发库和运行环境 B. ADO.NETASP.NET  -数据操作和web框架 C. CLS和CTS  -语法规范和类型规范  即所有语言和语法规范 和 各语言间的类型互操作性规范 D. Winform和ASP.NET  -控件库和web框架 解析: 正确答案是A. FCL和CL

    2023年04月25日
    浏览(45)
  • java 线程安全问题 三种线程同步方案 线程通信(了解)

    线程安全问题指的是,多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题。 下面代码演示上述问题,先定义一个共享的账户类: 在定义一个取钱的线程类 最后,再写一个测试类,在测试类中创建两个线程对象 某个执行结果: 为了解决前面的线程安全问题,

    2024年02月09日
    浏览(45)
  • java线程安全问题及解决

    当我们使用多个线程访问 同一资源 (可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程 只有读操作 ,那么不会发生线程安全问题。但是如果多个线程中对资源有 读和写 的操作,就容易出现线程安全问题。 案例: 火车站要卖票,我们模拟火车站的卖票

    2024年02月15日
    浏览(38)
  • 前端中学习 HTML 需要注意的重点难点

    学习HTML(Hypertext Markup Language)是进入Web开发世界的第一步,因为它是用于创建网页结构的基本语言。HTML用标记(tag)来描述文档的结构和内容,是一种标记语言。在学习HTML的过程中,有一些关键的概念和技能需要注意,以建立牢固的基础。 1. 理解HTML的基本结构 HTML文档的

    2024年01月23日
    浏览(29)
  • Java多线程【状态与安全问题】

    线程状态 说明 NEW 安排了工作, 还未开始行动 RUNNABLE 可工作的. 又可以分成正在工作中和即将开始工作 BLOCKED 这几个都表示排队等着其他事情 WAITING 这几个都表示排队等着其他事情 TIMED_WAITING 这几个都表示排队等着其他事情 TERMINATED 工作完成了 1.NEW状态:安排了工作, 还未开始

    2023年04月09日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包