Java中的多线程和线程安全问题

news2024/11/24 19:53:56

线程

线程是操作系统进行调度的最小单位。一个进程至少包含一个主线程,而一个线程可以启动多个子线程。线程之间共享进程的资源,但也有自己的局部变量。多线程程序和普通程序的区别:每个线程都是一个独立的执行流;多个线程之间是并发执行的。

在这里插入图片描述

多线程的实现方法

继承Thread类

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    public MyThread() {
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(this.getName() + ":" + i);
        }
    }
}

public class Text {
    public static void main(String[] args) {
        //实例化对象  创建线程
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread("线程t2");
        t1.start();
        t2.start();  //启动线程

        for (int i = 0; i < 5; i++) {
            System.out.println("hello main");
        }
    }
}

使用lambda表达式创建线程

public class Demo1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println("t1线程");
            }
        });

        Thread t2 = new Thread(() -> {
            while (true) {
                System.out.println("t2线程");
            }
        });
        t1.start();
        t2.start();

        while (true) {
            System.out.println("hello main");
        }
    }
}

start()和run()的区别

start()是启动一个分支线程 是一个专门用来启动线程的方法 而run()是一个普通方法和main方法是同级别的 单纯调用run方法是不会启动多线程并发执行的.

public class Demo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+ ":我还存活");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + ":我即将死去");
        });

        System.out.println(Thread.currentThread().getName() + ": 状态" + thread.getState());

        //启动线程
        thread.start();
        boolean flg = thread.isAlive();  //是否存活
        System.out.println(Thread.currentThread().getName() + ": 状态:" + thread.getState());
    }
}

上面的代码涉及到的一些线程的方法
在这里插入图片描述
还有获取线程状态的方法是getState()

sleep()方法

该方法就是让线程休眠 括号里面写休眠的时间 单位是毫秒级别 下面通过代码来描述

public class Demo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5;i++) {
                System.out.println("hello t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        
        t1.start();
        t2.start();
    }
}

通过上面的代码 就可以控制两个线程没执行一遍就休眠一秒钟 再继续执行下一遍。

join方法

多线程的join方法就是用来等待其他线程的方法 就是谁调用该方法谁就等待 join()这种是死等 ,当然也可以有时间的等 过了这个时间就不等了
就类似舔狗 有写舔狗 舔自己的女神 可能 会一直舔 舔到死 那种 有些就是有原则的舔 ,可能就在固定的时间内舔 ,过了这个时间段就坚决不舔。

代码实现如下:

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t1 = new Thread(() -> {
            //t1等t2
            try {
                Thread.sleep(500);
                t2.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            for (int i = 0; i < 5; i++) {
                System.out.println("t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        System.out.println("main end");
    }
}

import java.time.Year;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("t1线程在执行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t1.join(3000);
        System.out.println("main线程over");
    }
}

运行结果:
在这里插入图片描述
通过代码和运行截图可以看出 main线程在等了3秒之后就结束了 而t1线程还没执行完。
join方法的底层原理:
join方法的工作原理基于Java的内置锁机制。当你调用join方法时,当前线程会尝试获取目标线程对象的锁,并在目标线程执行完毕后释放锁。在这个过程中 ,当前调用的线程就会被阻塞等待,直到目标线程结束执行并释放锁,当前线程才能执行。

线程的状态

在Java官方的线程状态分类中 ,一共给出6种线程状态。
在任意一个时间点 ,一个线程就有且仅有一种状态

6种状态如下

NEW:创建好线程但是还没启动

RUNNABLE该状态是已经调用了start()方法 是可以工作的状态 但这种状态的线程有两种情况 :一种是正在执行 还要一种就是在等待CPU分配执行时间。

BLOCKED: 该状态就是线程被阻塞了,在等待别的线程的锁

WAITING:这种状态就是无限期等待 CPU不会给他分配执行时间 这种要等别的线程来唤醒。

TINED_WAITING: 这种就是有限期德等待,在一定时间之后就会被系统自动唤醒。

TERMINATED:工作完成了 线程已经执行结束了 。

线程状态的转换

在这里插入图片描述

线程的安全问题

那什么叫做线程安全呢
就是多线程环境下代码运行的结果是符合问你预期的,即在单线程环境应该的结果,则表示该线程是安全的 ,否则该线程就是不安全的。

线程不安全的原因

线程的调度是随机的 这是线程不安全的罪魁祸首
随机调度使一个程序在多线程环境下,执行顺序存在很多变数
多线程是一个神奇的东西

下面我们通过一个代码来看看什么是线程的不安全 也就是有bug 和预期效果不符。

public class Demo6 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

运行结果:
在这里插入图片描述
在这里插入图片描述
结果不符合预期 预期结果是100000 但是却输出小于10万的数,且每次运行的结果都不一样 为什么呢?
1、count++这个操作,站在cpu指令的角度来说,其实是三个指令。
load :把内存中的数据,加载到寄存器中
add:把寄存器中的值 + 1
save :把寄存器中的值写回到内存中

2、两个线程并发执行的进行count++
因为多线程的执行是随机调度,抢占式执行的模式

相当于某个线程执行指令的过程中,当她执行到任何一个指令的时候都有可能被其他线程把他的cpu资源抢占走。

综上所述,实际并行执行的时候,两个线程执行指令的相对顺序就可能存在无数种可能。

在这里插入图片描述
除了上面的两种可能还有无数种可能。

出现线程不安全的原因:
还是那句话:1、线程在系统中是随机调度的

2、在上面那个代码中 ,多个线程同时修改同一个变量就会出现这种线程不安全的问题

3、线程针对变量的修改操作,不是“原子”的
就像上面count++这种代码 就不是原子操作 因为该操作涉及到三个指令。
但有些操作,虽然也是修改操作 ,但是只有一个指令,是原子的。
比如直接针对 int / double进行赋值操作(在cpu上就只有一个move操作)
相当于来说 ,就是某个代码操作对应到一个cpu指令就是原子的 如果是多个那就是原子的。

那如何解决 该问题呢
那必须得从原因入手

线程调度是随机的这个我们无法干预
我们可以通过一些操作 把上诉非原子操作,打包成一个原子操作
那就是给线程加锁 下面我们举个例子:
就比如上厕所 现在厕所里面就一个坑位 现在来了两个人A和B 现在A先进去上厕所了 结果 A还没上完 B就冲了进去 这显然是不科学的 。在现实生活中,一般我们上厕所都会锁上门 。A进去上厕所把门给锁上,这时B要是也想进去上厕所就得等待A上完厕所解锁出来 这时B才能进去接着上厕所。

:本质上也是操作系统提供的功能 通过api给到应用程序 ;在Java中JVM对于这样的操作系统又进行了封装。

synchronized对象锁(可重入锁)

在Java中 我们引入synchronized 关键字
synchronized()括号里面就是写锁的对象

锁对象的用途,有且仅有一个 ,就是用来区分 两个线程是否针对同一个对象加锁
如果是 那就会出现锁竞争 /互斥 就会引起阻塞等待
如果不是,就不会出现锁竞争 ,也就不会出现阻塞。

下面给你们看看加上锁之后的代码

public class Demo6 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

运行的结果:
在这里插入图片描述
加锁之后 明显线程就没有bug了变安全了

加上锁之后 当t1进入count操作的时候 ,如果t2想进去执行 就会阻塞等待 因为 现在锁在t1的手里

还有 一种嵌套加锁 就是在第一个锁的基础上再加一个锁 就相当于 你要获取第二个锁得先执行完第一个锁 要想执行完第一个锁 ,得获取到第二个锁 ,这就相互矛盾了 就产生死锁看了

但是实际上 对于synchronized是不适用的 这个锁在上面这种情况下不会出现死锁 但是这种情况在C++和Python中就会出现死锁。

synchronized没有出现上诉情况是因为自己内部进行了特殊的处理(JVM)
每个锁对象里,会记录当前是哪个线程持有这个锁。
当针对这个对象加锁操作时,就会先判定一下,当前尝试加锁线程是否是持有锁的线程
如果不是就阻塞 否则就直接放行 不会阻塞。

场景二: ;两个线程 两把锁
现在有线程t1 和t2 以及锁A和锁B 现在这两个线程都需要获取这两把锁 ‘拿到锁A后 不释放锁A ,继续去获取锁B 就相当于 先让两个线程分别拿到一把锁,然后去尝试获取对方的锁。

举个例子: 疫情期间,现在广东的健康吗崩了 程序猿赶紧来到公司准备修bug 被保安拦住了
保安: 请出示健康吗 才能上楼
程序猿:我得上楼修复bug才能出示健康码

就这样 如果两个人互不相让 就会出现僵住的局面。
类似的 还有 钥匙锁在车里 ,而车钥匙锁屋子里了 这样也是僵住了

下面我们通过代码来实现一下 该情况:

public class Demo8 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker2) {
                    System.out.println("t1获取到两把锁");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker1) {
                    System.out.println("t2获取到两把锁");
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

运行结果:
在这里插入图片描述
你会发现现在运行起来什么都没有
t1尝试针对locker2加锁 就会阻塞等待 等待t2释放locker2 ,而t2尝试针对locker1加锁 也会阻塞等待等待t1 释放locker1.

这就相当于两个互相暗恋的人 你喜欢我 我也喜欢你 谁都在等对方 但是没人主动 说出来 终究是会错过。

针对这个问题 我们可以不用使用嵌套锁 ,但是也可以规定获取锁的顺序 比如说 t1和t2
线程规定好 先对locker1 加锁 再对locker2加锁 。这样就不会出现死锁的情况了。

产生死锁的四个必要条件

1.互斥条件
每个资源不能同时被两个或更多个线程使用。
2、不可剥夺性
一旦一个线程获得了 资源,除非该线程自己释放 ,否则其他线程不难强行剥夺这些资源。
3、请求和保持条件
一个线程因请求资源而阻塞时,必须保持自己的资源不放。如果一个线程在请求资源之前就释放了已获得的资源,那么就不会发生死锁现象。
4循环等待条件
如果存在一个资源等待链 ,即P1正在等待P2释放的资源 ,P2正在等待P3释放的资源 ,以此类推,最后Pn又在等待P1释放的资源。

以上四个条件必须同时满足,才能产生死锁现象 在实际开发中我们应该合理设计代码 避免死锁的发生。

在上面这四种产生死锁的条件中 前面两个是线程的基本特征 ,我们无法干预 ,最好解决死锁的方法就是破除条件3或者条件4 条件3 要破解 就需要 不要写锁嵌套 ,那如果非要写成锁嵌套怎么办 ,那就是破解第四个条件 当代码中有多个线程获取多把锁的情况 ,我们就需要统一规定好加锁的顺序 ,这样就能有效的避开死锁的现象。

内存可见性 引起的线程安全问题

下面我们通过一个代码来展示这个线程安全问题:

import java.util.Scanner;

public class Demo1 {
    private  static int count = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
                ;
            }
            System.out.println("t1 执行结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数 :");
            count = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

运行结果:
在这里插入图片描述
你会发现虽然修改了count的值 但是程序并没有结束 这就和我们预期的出现了差错 出现bug了 那是为什么呢?

在那个while循环里面 会执行 load和cmp这两个指令
**load 😗*从内存读取数据到寄存器
cmp:(比较,同时产生跳转) 条件成立,就继续执行程序 条件不成立,就会跳到另一个地址来执行

当前的循环旋转速度很快 短时间内会有大量的load 和 cmp 反复执行的效果 load执行消耗的时间 会比 cmp 多很多
这样JVM 就会因为load执行速度慢 而每次load的结果都是一样的 JVM 就会干脆 把上面的load操作给优化了 只\有第一次执行load才是真的在进行load 后续再执行到相对应的代码,就不再真正的load了,而是直接读取已经load过的寄存器的值了
当我们在while循环体里面加入一些IO操作 程序运行就要正确了
这又是为什么呢
因为如果循环体里面有IO操作 就会使循环体的旋转速度大幅度降低 ,因为IO操作比load操作要慢得多 所以JVM也就不会再去优化load操作 ,而IO操作是不会被优化的.
内存可见性问题说到底是由于编译器优化引起的,优化掉load操作之后 ,使得t2线程的修改没有被t1线程感知到.
JVM在什么时候优化 什么时候不优化 这也是不确定的
那我们该怎么解决该内存可见性问题呢?

volatile

我们会引入volatile关键字
这个关键字的作用就是告诉编译器不要触发上述优化 volatile关键字是专门针对内存可见性的场景来解决问题的.

关于线程安全问题还有一些内容 我们下篇内容讲解 本篇内容就到此结束了 谢谢大家的浏览 !!!

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

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

相关文章

大模型面试准备(九):简单透彻理解MoE

节前&#xff0c;我们组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学&#xff0c;针对大模型技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何备战、面试常考点分享等热门话题进行了深入的讨论。 合集在这…

C++list的模拟实现

为了实现list&#xff0c;我们需要实现三个类 一、List的节点类 template<class T> struct ListNode {ListNode(const T& val T()):_pPre(nullptr),_pNext(nullptr),_val(val){}ListNode<T>* _pPre;ListNode<T>* _pNext;T _val; }; 二、List的迭代器…

双网卡环境概率出现DNS解析错误

测试环境 VMware Rocky Linux 9 虚拟机, 双网卡(eth0和eth1)配置如下&#xff1a; eth0 10.206.216.27/24 DNS 10.204.16.18 eth1 192.168.1.27/24 DNS 192.168.1.1问题描述 手动配置eth1的DNS后&#xff0c;网络不通&#xff0c;通过抓包发现是eth1的DNS server配置有误…

【JavaWeb】Day29.SpringBootWeb请求响应——请求(二)

请求响应 4.数组集合参数 数组集合参数的使用场景&#xff1a;在HTML的表单中&#xff0c;有一个表单项是支持多选的(复选框)&#xff0c;可以提交选择的多个值。 4.1 数组 数组参数&#xff1a;请求参数名与形参数组名称相同且请求参数为多个&#xff0c;定义数组类型形参即…

springboot简历系统

摘 要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;简历系统当然也不能排除在外。简历系统是以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;采用…

速通汇编(三)寄存器及汇编mul、div指令

一&#xff0c;寄存器及标志 AH&ALAX(accumulator)&#xff1a;累加寄存器BH&BLBX(base)&#xff1a;基址寄存器CH&CLCX(count)&#xff1a;计数寄存器DH&DLDX(data)&#xff1a;数据寄存器SP(Stack Pointer)&#xff1a;堆栈指针寄存器BP(Base Pointer)&#…

Vue3+Vite Nginx部署 跨域

打包项目 webstorm打开项目之后&#xff0c;在Terminal执行打包命令 pnpm run build:prod 复制到Nginx 打包完成之后,生成的包在根目录dist&#xff0c;把dist目录拷贝到Nginx放网站目录下&#xff1a;\nginx-1.25.2\html\divided &#xff0c;dist改名了divided 修改配置…

力扣---网络延迟时间---迪杰斯特拉,弗洛伊德floyd

首先推荐博客&#xff1a;图论最短路径专题&#xff08;力扣743、5888&#xff09;_力扣 最短路径-CSDN博客 迪杰斯特拉算法&#xff1a; 太久没有做图论的题了&#xff0c;&#xff0c;临时抱佛脚。。 这道题可以转化为max{点x到点k的距离}。因为带权图&#xff08;权值为正…

[超详细]3种方法判断一个数是否为质数(Python)

(发现好多博客对第三种进阶方法说的不明白&#xff0c;至少我是没完全看明白。后面结合自己的理解应该算是弄懂了&#xff0c;供大家参考&#xff0c;欢迎纠正。) 方法一&#xff1a;最暴力&#xff0c;最简单&#xff0c;也最耗时O(n) 思想&#xff1a;由素数的定义&#xf…

arcgis 无法编辑元素的解决办法(无法删除元素或者缺失值替换)

打开“编辑器”中&#xff0c;“开始编辑”即可进行元素编辑&#xff0c;也可进行缺失值替换 &#xff08;其他方式&#xff1a;选中图层&#xff0c;右击点击开始编辑&#xff09; 在元素编辑状态下无法删除变量&#xff0c;可以删除元素 元素编辑结束后 点击“编辑器”&…

深入剖析Spring WebFlux:从MethodHandler到反射获取请求信息的源码之旅

文章目录 前言一、获取请求执行的类、方法信息二、获取请求url变量三、获取请求处理数据总结 前言 最近想写一个代办事项后台服务&#xff0c;底层&#xff0c;选型WebFlux。在操作层面上&#xff0c;针对部分操作&#xff0c;想在不侵入业务代码的前提下&#xff0c;记录操作…

使用 Seq2Seq 模型进行文本摘要

目录 引言 1 导入数据集 2 清洗数据集 3 确定允许的最大序列长度 4 选择合理的文本和摘要 5 对文本进行标记 6 删除空文本和摘要 7 构建模型 7.1 编码器 7.2 解码器 8 训练模型 9 测试模型 10 注意 11 整体代码 引言 文本摘要是指在捕捉其本质的同时缩短长文本的…

主从复制与读写分离

前言&#xff1a; 在企业应用中&#xff0c;成熟的业务通常数据量都比较大&#xff0c;单台MySQL在安全性、高可用性和高并发方面 都无法满足实际的需求&#xff1f; 配置多台主从数据库服务器以实现读写分离 目录 一 主从复制的工作原理 ①MySQL的复制类型 ②主从复制过…

Netty组件优化之FastThreadLocal

ThreadLocal:CSDNhttps://mp.csdn.net/mp_blog/creation/editor/132995427 Netty中的FastThreadLocal是对Java中的FastThreadLocal的优化主要是为了解决ThreadLocal中线性查找 带来的性能下降同时实现快速查找和赋值 FastThreadLocal构建这里的index代表一个编号&#xff0c;从…

【Web应用技术基础】CSS(4)——背景样式

第1题&#xff1a;背景颜色 .html <!DOCTYPE html> <html><head><meta charset"utf-8"><title>Hello World</title><link rel"stylesheet" href"step1/CSS/style.css"> </head><body>&…

预训练大模型最佳Llama开源社区中文版Llama2

Llama中文社区率先完成了国内首个真正意义上的中文版Llama2-13B大模型&#xff0c;从模型底层实现了Llama2中文能力的大幅优化和提升。毋庸置疑&#xff0c;中文版Llama2一经发布将开启国内大模型新时代。 作为AI领域最强大的开源大模型&#xff0c;Llama2基于2万亿token数据预…

[机器学习]练习闵可斯基距离

闵可斯基距离&#xff08;Minkowski distance&#xff09;是一种用于衡量向量空间中两点之间距离的方法。它是曼哈顿距离和欧几里得距离的一般化形式。闵可斯基距离使用一个参数 p 来调整计算方法&#xff0c;其中 p 是一个大于 0 的实数值。 在二维空间中&#xff0c;闵可斯基…

二. CUDA编程入门-Stream与Event

目录 前言0. 简述1. 执行一下我们的第九个CUDA程序2. Stream是什么3. Streams实验(单流vs多流)4. 如何隐藏延迟(memory)5. 如何隐藏延迟(kernel)6. 如何隐藏延迟(kernelmemory)7. 代码分析总结参考 前言 自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》&#xff0c;链接。记…

HWOD:提取不重复的整数

一、题目 1、描述 输入一个int型整数&#xff0c;按照从右向左的阅读顺序&#xff0c;返回一个不含重复数字的新的数字。保证输入的整数最后一位不是0 2、数据范围 1< n <10^8&#xff1b; 3、输入 输入一个int型整数 4、输出 按照从右向左的阅读顺序&#xff0c…

vue3 视频播放功能整体复盘梳理

回顾工作中对视频的处理&#xff0c;让工作中处理的问题的经验固化成成果&#xff0c;不仅仅是完成任务&#xff0c;还能解答任务的知识点。 遇到的问题 1、如何隐藏下载按钮&#xff1f; video 标签中的controlslist属性是可以用来控制播放器上空间的显示&#xff0c;在原来默…