wordpress4.8汉化/seo排名工具外包
《JavaEE篇》--多线程(1)
线程安全
线程不安全
我们先来观察一个线程不安全的案例:
public class Demo {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {//让count自增5W次for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() -> {//让count自增5W次for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();//如果没有join,线程还没执行完,就会打印countt1.join();t2.join();//预期结果应该是十万次System.out.println("count: " + count);}
}
两个线程同时对一个变量进行修改,按理来说最后的输出结果应该是十万,现在我们运行一下
可以以看到似乎并不像我们预期的那样,每个结果都不一样,而且有些结果和我们预期的相差很大。
上述代码如果放在单线程里肯定是对的,但是如果放在多线程里就会出现逻辑错误了
先要知道怎么回事我们要先了解一下count++这个操作。
Count++这个操作实际上,是分成三步进行的,站在cpu的角度上,count++是由cpu通过三条指令来实现的
- load 把数据从内存读到cpu寄存器上
- add 把寄存器中的数据+1
- save 把寄存器中的数据,保存到内存中
由于线程之间的调度顺序是随机的,就会导致在有些调度顺序下,就会出现上述逻辑错误,接下来我通过画图的方式详细讲解。
上述两种执行顺序是正常的执行顺序(t1线程执行完整个count++动作,之后t2线程再执行,或者t2先执行),但是由于线程的随机调度,可能当t1执行load和add的操作之后,这时t2插了进来完成自己的操作。就像这样
那么此时会产生什么结果呢?
可以看见此时编译器执行了两次count++操作但是实际的count只被加了一次,这还只是一种顺序,实际编译时两个操作执行count++又会有多少种呢?如果t1执行一次,t2执行两次,三次呢又会有多种?必然是我们无法知晓的。
结合上述讨论,就意识到了,在多线程中最困难的一点就是:线程的随机调度,使两个线程执行逻辑的先后顺序存在诸多可能,我们要保证在所有可能的情况下,代码都是正确的。
综上对于线程安全我们可以粗略的认为:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
线程不安全的原因
线程安全产生的原因主要有以下五点
- 操作系统中,线程的调度是随机的(抢占式执行)//主要原因
- 两个或多个线程对同一个变量进行修改
- 修改操作不是原子性的(比如count++就是非原子操作,需要多条指令才能完成操作)
- 内存可见性问题(之后讲到)
- 指令重排序(之后讲到)
原子性:原子性不一定只是一条java语句或者只是一条cpu指令,如果一个操作在执行的时候不会有其他操作干扰或插入,就可以说这个操作是具有原子性的,比如如果把刚刚的count++加上一把锁让编译器,必须执行完一个count++后才能执行下一个count++,这样不管t1,t2线程的执行顺序如何,都不会影响最后结果。
保证原子性这一点也和线程的抢占式调度密切相关,如果线程不是抢占式(随机)的,就算没有原子性也没有什么关系
那么我们如何保证线程是安全的?我们先暂时针对前三个问题进行解决,先看第一个,这是操作系统的特性我们不好对其进行修改, 那么再看后两个,如果我们保证操作是原子性的,这样不管对变量怎么修改,就都不会出错了。那么怎么保证原子性呢?最好的方法就是加锁
加锁
我们先来解决刚刚的代码
private static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {//让count自增5W次for (int i = 0; i < 50000; i++) {synchronized (locker){//当同时对一个对象加锁时,必须要等前一个代码块执行完,后一个代码块才能执行,就可以解决问题了count++;}}});Thread t2 = new Thread(() -> {//让count自增5W次for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});t1.start();t2.start();//如果没有join,线程还没执行完,就会打印countt1.join();t2.join();//预期结果应该是十万次System.out.println("count: " + count);}
运行结果:
可以看见现在的结果正是我们预期的
synchronized
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待,换言之,当两个synchronized同时对一个对象加锁(什么样对象一般无所谓只要是相同的就行),必须要等前一个synchronized代码块执行完,后一个代码块才能执行。
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
举个例子🌰:
如果A对B表白,并且B同意了此时就相当于A对对象B进行了加锁,此时C就不能再追求B了,只能等A和B分手后(解锁),B又回归单身后,此时C就可以光明正大的追求B了。
//synchronized用的锁是存在Java对象头里的,加锁就相当于对对象头进行了修改
可以粗略的理解成对象再内存中储存的时候,都有一块内存表示对象是否被锁,如果被锁则其他线程不能再次加锁,如果没被锁则可以加锁。
其他写法
这两种方法等价
//这两种方法等价synchronized public void add1(){this.count++;}public void add2(){synchronized (this){count++;}}
//这两种方法等价synchronized public static void add3(){}public static void add4(){synchronized (Count.class){}}
这两种方法也等价,上面的写法相当于下面的简化写法
Count.class叫做类对象,类对象在java进程中是唯一的,一般包含以下内容
- 类的属性有哪些,都是啥名字,类型,权限
- 类的方法有哪些,都是啥名字,类型,权限
- 类本身继承自哪个类,实现了什么接口
可重入
- 不可重入锁:只判断这个锁有没有被锁上,如果没被锁上就可以对其加锁,如果被锁上就要等待它解锁之后才可以加锁
- 可重入锁:不仅会判断这个锁有没有被锁上,还会判断这个锁是被谁锁上的,如果是被当前线程锁上的则可以连续加锁,并且不出现死锁
简单来说一个线程,连续针对一个一把锁,加锁两次,不会出现死锁,满足这个要求,就是可重入
不可重入锁
当一个代码块被锁上,此时如果在代码块内部再加上一把锁就会出现死锁,如下
如果synchronized是不可重复锁,针对上述代码,在第一次加锁时可以成功,此时locker已近是被加锁的状态了,当进行第二次加锁时,因为locker已经被加锁了所以第二把锁就会阻塞等待,要等到第一把锁解锁之后才能加成功。但是实际上,一旦第二把锁阻塞等待了,就会出现死锁,因为第一把锁要想解锁就要执行到(1)的位置,要想执行到(1)的位置就要让第二把锁成功加锁,由于第二次加锁导致代码阻塞了,就没办法执行到(1)的位置了也就没办法释放锁了。
//死锁的发生不一定是在一个方法,也可能会发生在方法调用之间
可重入锁
可重入锁是一种支持可重入机制的锁,当对一把锁连续加锁两次第二把锁不会出现阻塞(换言之,外层使用锁之后,内层任然可以使用),允许一个线程反复获得该锁,避免了死锁的发生,同时也提高了代码的简洁性的可读性,我们刚刚讲到的synchronized就是可重入锁
我们在对这个案例进行分析,因为synchronized是可重入锁,所以没有因为第二次加锁而死锁,但是当代码执行到(2)时,此时,锁(1)应不应该打开?又或者说,这里有N把锁,那么释放的时机应该如何?
为了解决这个问题,可重入锁设计了加锁次数,每加一次锁就加1,释放一次锁就减1,当计数为0时才真正释放锁,以此来保证所有的加锁过程都解锁了,其他线程才能访问。
在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.
- 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
关于死锁
1.一个线程针对一把锁,连续加锁两次,如果是不可重入锁,就会发生死锁
2.两个线程,两把锁,t1线程先获取锁A在获取锁B,t2线程先获取锁B在获取锁A(此时无论是不是可重入锁都会死锁),就相当于一个人把车钥匙锁在家里,把家里钥匙锁在车里
3.N个线程M把锁
关于N个线程M把锁有一个经典的模型--哲学家就餐问题
哲学家就餐问题
规则是这样的:
- 每个哲学家会做两件事1.思考人生,放下筷子啥也不干 2.吃面条,拿起左右两侧的筷子,开始吃面条
- 哲学家什么时候吃面条,什么时候思考人生是随机的
- 哲学家什么时候吃完面条,什么时候思考完人生也是随机的
- 哲学家正在吃面条时会拿起左右两则的筷子,如果相邻的哲学家也想吃面条就需要阻塞等待
基于上述规则通常情况下,是可以正常运行的,但是当处在极端情况下时,比如五位哲学家同时想吃面,又同时拿起左边的筷子,这时就会出现死锁的情况(这里哲学家就相当于一个线程,筷子就相当于是锁,拿起一个筷子相当于解开了一把锁)。
处理方法:
- 对哲学家进行限制,当哲学家确认左右两边的筷子都可以用时才会拿起筷子吃面(确认之后其他人不会再拿起),使拿起筷子这个操作成为一个原子性的,就可以避免死锁发生。
- 找到一位右撇子的哲学家(先拿起右边的筷子,其他人都是左撇子),目的是让这个哲学家拿筷子的顺序和其它人不一样,当其他哲学家都拿起左边的筷子时,此时这个哲学家右边就没有筷子了,就处于阻塞等待,此时就会有一个哲学家成功拿到两双筷子。
- 限制吃饭的人数,方法很简单,就是在一个时间点最多只能有4个哲学家进餐,这样4个让分5双筷子,一定能有人吃的到,就不会发生死锁。
- 规定一个哲学家只有在两个相邻的哲学家都不吃面的情况下,才可以吃面,这样就彻底解决了死锁问题,和第一种方法有点类似。
Volatile
内存可见性
线程之间的共享变量存在主内存(实际物理内存)中,而且每一个线程还有自己的工作内存(寄存器/cpu高速缓存),当线程要读取一个共享变量时,会把变量从主内存拷到工作内存中,再从工作内存读取数据,当线程要修改一个共享变量时,也会先修改工作内存中的副本,在同步到主内存中。这样可能就会导致线程1修改了共享变量a,但是主内存和线程2中数据更改不及时。
内存可见性会带来的问题
我们先来看段代码
正常来将当我们输入1时t1线程就结束了
但是实际上无论我们如何输入1,t1都没有结束,这是怎么回事?
计算机运行的程序,经常要访问数据,这些数据往往会存储在内存中(比如,定义一个变量,变量就会存储在内存中),当cpu使用这个变量的时候,就会把这个变量,先从内存中读出来,再放到cpu的寄存器中,最后在参与运算。cpu读取内存的操作是比较缓慢的,相对而言读寄存器就快的多了。
为了及解决上述问题,提高效率,此时编译器就可能对代码作出一些优化,把一些本来要读内存的操作,转化为读寄存器,减少读内存的次数,也就提高了程序的效率了
当编译器在处理isQuit == 0时会涉及到俩条指令1.load读取内存中的isQuit的值到寄存器中,2.通过cmp指令比较寄存器里的值是不是0,然后决定要不要循环。
由于循环执行的速度非常快,短时间内就会完成大量load和cmp操作。这时编译器/JVM就会发现,虽然进行了这么多次load,但是每次load的结果都一样,并且load操作非常消耗时间,比cmp操作消耗的时间多得多,于是编译器就做了一个大胆的就决定,只在第一次循环操作的时候,才读内存,后续都不在读内存了,而是直接从寄存器中取出isQuit的值,这个就是编译器的优化。
编译器是希望代码能够更高效,才在保证代码逻辑不变的情况下,对代码进行优化,但是,此时我们是由另外一个线程对isQuiti进行了修改,编译器没有做出正确的判断,以为没人修改,最终导致了bug,这个问题就是内存可见性问题。
这时我们可以利用Volatile来解决这个问题
在多线程的环境下,编译器有时会做出一些不准确的优化,此时就需要我们程序猿自己手动来矫正。我们可以通过Volatile关键字,来告诉编译器不要进行优化。我们直接在刚刚的代码上操作,直接在isQuit前加上Volatile就行。
运行结果
可以看到这时程序成功执行
wait和notify
wait():让当前线程等待阻塞
wait的工作过程是,释放当前的锁,使当前的线程进行等待,满足唤醒条件后,重新尝试获得这把锁(wait必须搭配synchronized使用,换言之当前线程必须得到锁,否则直接使用的化会抛出异常)
wait可以传参,表示最多等待的时间(ms)
notify():唤醒wait等待的线程
notify工作过程是,唤醒wait,然后解锁
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行 完,也就是退出同步代码块之后才会释放对象锁。
- 和wait一样必须搭配synchronized使用
notifyAll():可以一次唤醒所有的等待线程.
使用:
public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {System.out.println("wait 之前");synchronized (locker){try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("wait之后");});Thread t2 = new Thread(() -> {try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker){System.out.println("进行通知");locker.notify();}});t1.start();t2.start();}
wait和sleep的区别
不同点:
- wait 需要搭配 synchronized 使用. sleep 不需要.
- wait 是 Object 的方法 sleep 是 Thread 的静态方法.
- wait是为了让线程稍作等待,避免线程盲目争夺资源,sleep这时单纯让线程休眠一段是时间
- wait是用于线程之间的通信,sleep只是用于让线程阻塞一段时间
- 线程在wait时会释放锁,在sleep时不会
- wait线程处于WAITING状态,sleep时线程处于TIMED_WAITING状态
- wait被notify提前唤醒不会报出异常,sleep被提前唤醒会报异常
- 相同点:
- 都会让线程阻塞一段时间
- 都可以设置最大等待时间时间
以上就是博主对线程知识的分享,在之后的博客中会陆续分享有关线程的其他知识,如果有不懂的或者有其他见解的欢迎在下方评论或者私信博主,也希望多多支持博主之后和博客!!🥰🥰
下一篇博客博主将分享有关单例模式等知识,还希望多多支持一下!!!😊
相关文章:

《JavaEE篇》--多线程(2)
《JavaEE篇》--多线程(1) 线程安全 线程不安全 我们先来观察一个线程不安全的案例: public class Demo {private static int count 0;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() -> {//让count自增5W次…...

防爆智能手机如何助力电气行业保驾护航?
在电气行业的智能化转型浪潮中,防爆智能手机以其强大的数据处理能力、实时通讯功能及高度集成的安全特性,正成为保障电力网络稳定运行、预防安全隐患的得力助手。 防爆智能手机在电气行业中发挥着重要的保驾护航作用,主要体现在以下几个方面&…...

24.7.24数组|那几个课后得做的题
1、对长整形数据进行反转 2、对字符串进行反转 一、题目地址: 1. 实现一个函数atoi,使其能够将字符串转换整数 (Leetcode 8/中等). - 力扣(LeetCode) 2. 颠倒给定的32位无符号整数的二进制位(Leetcode 190/简单&…...

03Spring底层架构核心概念解析
为了感谢罕哥对我工作的帮助,特此记录下学习过程,期待成为和罕哥一样优秀的人 时间:2024.7.13 内容:spring源码课程3学习记录 一、BeanDefinition BeanDefinition表示Bean的定义,BeanDefinition中存在很多属性用来…...

Vue学习---vue 防抖处理函数,是处理什么场景
Vue防抖处理函数是用来处理在快速连续操作中,只执行最后一次操作的情况。 例如,在输入框输入时,我们可能希望只在用户完成输入后进行处理,而不是在每次键入时都处理。(n秒后触发一次) 以下是一个简单的Vue防抖处理函数的例子&am…...

力扣爆刷第166天之TOP100五连刷96-100(单词拆分、回溯、旋转数组)
力扣爆刷第166天之TOP100五连刷96-100(单词拆分、回溯、旋转数组) 文章目录 力扣爆刷第166天之TOP100五连刷96-100(单词拆分、回溯、旋转数组)一、24. 两两交换链表中的节点二、139. 单词拆分三、560. 和为 K 的子数组四、209. 长…...

2024在线PHP加密网站源码
源码介绍 2024在线PHP加密网站源码 更新内容: 1.加强算法强度 2.优化模版UI 加密后的代码示例截图 源码下载 https://download.csdn.net/download/huayula/89568335...

网络驱动移植(RTL8189)
1、把驱动放到内核文件夹中(linux/drivers/net/wireless),对应的驱动可以在网上下载 2、修改该目录下的Kconfig和Makefile文件 3、配置内核(make menuconfig) 配置支持IEEE 802.11,选中8189模块࿰…...

go语言中map学习
在 Go 语言中,map 是一种引用类型,这意味着它有以下特点: 内存结构: map 实际上是一个指向底层数据结构的指针。这个底层数据结构包含键值对的集合。 赋值与传参: 当你给一个变量赋值一个 map 时,或者将 map 作为函数参数传递时,实际上传递的是指针,而不是完整的数据结构副本。…...
【C#】| 与 及其相关例子
按位或(|) 按位或运算符 | 对两个数的每一位进行比较,如果两个数中至少有一个为 1,则结果位为 1;否则,结果位为0。 1010 (10 in decimal) | 1100 (12 in decimal) ------1110 (14 in decimal) 力扣相关…...

【数据结构 | 哈希表】一文了解哈希表(散列表)
😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀 🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C、数据结构、音视频🍭 🤣本文内容🤣&a…...

go创建对象数组
在 Go 语言中,可以使用字面量的方式创建结构体对象数组。以下是一个示例代码,展示了如何使用字面量创建一个结构体对象数组: package mainimport "fmt"// 定义一个结构体 type Person struct {Name stringAge intAddress Address…...

Golang | Leetcode Golang题解之第278题第一个错误的版本
题目: 题解: func firstBadVersion(n int) int {return sort.Search(n, func(version int) bool { return isBadVersion(version) }) }...

自动化网络爬虫:如何它成为提升数据收集效率的终极武器?
摘要 本文深入探讨了自动化网络爬虫技术如何彻底改变数据收集领域的游戏规则,揭示其作为提升工作效率的终极工具的奥秘。通过分析其工作原理、优势及实际应用案例,我们向读者展示了如何利用这一强大工具加速业务决策过程,同时保持数据收集的…...

软件测试---测试需求分析
课程目标 什么是软件测试需求 软件测试需求的必要性 如何对软件测试需求进行分析(重点) 课程补充 灰度测试(基于功能):先发布部分功能,然后看用户的反馈,再去发布另外一部分的功能更新。 A/B测…...

Android11 framework 禁止三方应用通过广播开机自启动-独立方案
之前的文章Android11 framework 禁止三方应用开机自启动记录了我调试Android11应用自启动限制的全过程,但是之前的方案感觉还能再研究,所以有了这一篇文章。 这一篇文章主要探讨Android11上,以广播来进行自启动的应用的限制,极个别…...

Node:解决Error: error:0308010C:digital envelope routines::unsupported的解决方法
问题描述 在使用vuepress搭建博客的时候,运行项目发现报错了,检查了node的版本是18,之前用的是16或14的版本,现在报:Error: error:0308010C:digital envelope routines::unsupported错误。 查找了一些资料࿰…...

spring boot(学习笔记第十四课)
spring boot(学习笔记第十四课) Spring Security的密码加密,基于数据库认证 学习内容: Spring Security的密码加密基于数据库认证 1. Spring Security的密码加密 如果用户的密码保存在数据库中是以明文保存,对于公司的安全将是灾难性的&…...

Android 11 Unable to start/bind service
今天在Android11上发现了一个的问题,如果目标Service的进程没有启动,那么无论是bindService还是startService都没有办法拉起指定的Service。 网上查了很多资料如下: 1.目标Service 设置 android:exported"true" 2.目标Service需要声明自定义权…...

走难而正确的路并持之以恒
走难而正确的路并持之以恒 接近八月,台风频繁。气象台说台风“格美”今夜将至,往粤北走,而留在粤东的将是持续的高温。高温的广州,这几晚的天空惊喜不断,成片的火烧云,站在猎德大桥观望,丹红的凤…...

规范:Redis规范
在公司项目中,redis属于高频使用,在使用中,我们遇到了各种各样的redis问题,于是针对自身情况梳理了一个redis使用规范。 一、键名设计 1、key名设计 1. 禁止包含特殊字符(比如空格、换行、单双引号以及其他转义字符) 2. 建议以…...

比较 WordPress 、 Baklib 和 BetterDocs
对于希望管理其产品和服务的在线文档或知识库以支持其客户和员工的组织来说,市场上有太多的平台和工具。一些组织使用 WordPress 作为 Web 内容管理,并打算使用可用的插件。如果您是这样的组织之一,正在考虑使用广泛使用的 WordPress 插件之一…...

Redis 哨兵搭建
Redis哨兵(sentinel)搭建 7.2.5 文章目录 一、单节点哨兵1. 环境介绍2. 环境前准备工作3. 安装 Redis 7.2.54. redis 配置修改并且启动4.1 修改配置文件4.2 编写启动脚本 5. 开启主从5.1 开启5.2 主库实例查看主从信息 6. 创建sentinel的配置文件并启动6.1 创建配置文件6.2 启…...

HackTheBox--Knife
Knife 测试过程 1 信息收集 端口扫描 80端口测试 echo "10.129.63.56 knife.htb" | sudo tee -a /etc/hosts网站是纯静态的,无任何交互功能,检查网页源代码也未发现任何可利用的文件。 检查页面请求时,请求与响应内容࿰…...

Linux_实现TCP网络通信
目录 1、实现服务器的逻辑 1.1 socket 1.2 bind 1.3 listen 1.4 accept 1.5 read 1.6 write 1.7 服务器代码 2、实现客户端的逻辑 2.1 connect 2.3 客户端代码 3、实现服务器与客户端的通信 结语 前言: 在Linux下,实现传输层协议为TCP…...

正则表达式与文本三剑客之grep
目录 前言 一、grep命令 二、基础正则表达式常见元字符 2.1、特殊字符 2.2、定位符 2.3、非打印字符 三、元字符操作实例 3.1、查找特定字符 3.2、利用中括号“[]”来查找集合字符 3.3、查找行首“^”与行尾字符“$” 3.4、查找任意一个字符“.”与重复字符“*” 3.…...

微信小程序开发:项目程序代码构成
✨✨ 欢迎大家来访Srlua的博文(づ ̄3 ̄)づ╭❤~✨✨ 🌟🌟 欢迎各位亲爱的读者,感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢,在这里我会分享我的知识和经验。&am…...

【云原生】Kubernetes微服务Istio:介绍、原理、应用及实战案例
✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,…...

【Docker】Docker-consul容器服务自动发现与注册
目录 一.Consul概述 1.解决了什么问题 2.什么叫微服务或者注册与发现 3.consul的模式 4.相关命令 二.consul 部署 1.consul服务器部署 2.部署docker容器 3.Nginx负载均衡器 3.1.安装启动nginx 3.2.配置nginx负载均衡 3.3.创建配置consul complate模板文件 3.4.添加…...

Go 1.22 remote error: tls: handshake failure
Golang 1.22 remote error: tls: handshake failure 1.22之前运行下面代码是没有错误 package mainimport ("crypto/tls""fmt""net/http" )func main() {http.DefaultTransport.(*http.Transport).TLSClientConfig &tls.Config{InsecureS…...