【线程安全篇】

news2024/12/24 8:47:09

线程安全之原子性问题

x++ ,在字节码文件中对应多个指令,多个线程在运行多个指令时,就存在原子性、可见性问题
在这里插入图片描述

赋值

多线程场景下,一个指令如果包含多个字节码指令,那么就不再是原子操作。因为赋值的同时,读到的x的值可能已经发生变化,被其他线程修改了。

	x = 10;     //原子操作,只有一个操作,10赋值给x,之后写入内存
	y = x;      //非原子操作,1、先从内存读x的值  2、x的值赋值给y,再写入内存
	x++;        //非原子操作,同上

count++

模拟多个线程count++,最终count不一定等于1000。

public class Demo{ 
	private static int count=0; 
	public static void inc(){ 
		try { 
			Thread.sleep(1); 
		} catch (InterruptedException e) { 
			e.printStackTrace(); 
		} 
		count++; 
	} 
	
	public static void main(String[] args) throws InterruptedException {
		for(int i=0;i<1000;i++){ 
			new Thread(()->Demo.inc()).start(); 
		} 
		Thread.sleep(3000); 
		System.out.println("运行结果"+count); 
	} 
}

线程安全之可见性问题

多个线程访问同一变量,一个线程修改了该变量的值,其他线程能立刻看到修改的最新值。

CPU缓存不一致问题

计算机核心组件:CPU、内存、I/O设备
计算速度对比:CPU > 内存 > I/O设备

为了提升计算性能,CPU从单核升级到了多核,以及超线程技术。但后两者的处理性能并没有跟上。为了平衡三者的速度差异,做了很多优化:

1.CPU增加了高速缓存,很好的解决了CPU和内存的速度矛盾。
2.操作系统增加了进程、线程。通过CPU时间片切换最大化提高CPU录用率。
3.编译器指令优化。

CPU高速缓存

线程是CPU调度的最小单元。

主内存 、总线 、CPU多级缓存
CPU先在L1找数据,L1没有去L2,L2没有去L3,L3没有去内存找。
CPU计算时,直接从缓存中读取数据,计算完成后再写入缓存中,最后再把缓存中的数据同步到内存。
在这里插入图片描述

缓存不一致问题

每个CPU拥有自己的缓存,如果同一数据在不同缓存中,缓存值不一样,就存在缓存不一致的问题。
解决方案: 总线锁、缓存锁

总线锁

当一个CPU要对共享变量操作时,在总线上发出LOCK#信号,锁住CPU和内存的通信,锁住期间,其他CPU不能操作缓存了该数据内存地址的缓存。
总线锁开销比较大,所以这种机制显然不合适。

缓存锁

基于缓存一致性协议

缓存一致性协议(MESI)

M(Modify)
被修改的。该数据只在当前CPU的缓存中有,且与主内存不一致。
E(Exclusive)
独占的。该数据只在当前CPU缓存中,且没有被修改过。
S(Shared)
共享的。该数据被多个CPU缓存,且各缓存中的数据与主内存一直。
I(Invalid)
失效的。当前CPU中缓存的该数据失效。

在这里插入图片描述
i=1,该CPU独占且与内存数据一致,此时处于E状态,如果i变成了2,则状态变为M。
在这里插入图片描述
CPU只能从缓存中读取M、E、S状态的数据,I状态的数据要到内存中读取。
CPU可以直接写M、E状态的数据。S状态的数据,需要先将其他CPU中缓存行设置为无效才能写。

Store Bufferes

CPU0对缓存中的共享变量写入时,先发送一个失效的消息给到缓存了该共享变量的CPU,并且要等到它们的确认回执。这个过程中,CPU0处于阻塞状态。为了避免浪费资源,所以引入Store Bufferes

在这里插入图片描述
1.CPU0将数据写入Store Bufferes中,同时发送invalidate消息给CPU1,之后就可以继续处理其他指令。
2.CPU1收到invalidate消息后,将要修改的变量i放入invalidate queue(失效队列中),并且给一个ACK应答。
3.CPU0收到CPU1的invalidate acknowledge之后,将Store Bufferes中的数据存储至缓存行(cache line),最后再从缓存行同步到内存。

内存屏障(memory barrier)

内存屏障就是把Store Bufferes中的指令写入内存,内存屏障之前的内存访问操作先于其后的操作完成。保证共享变量对其他线程的可见性。
在这里插入图片描述
写屏障(store memory barrier)
store之前的所有已经存储在Sotre Bufferes中的数据同步到内存。即将Sotre Bufferes中的a==1同步到内存后,才能执行后面的b=1。
读屏障(load memory barrier)
load之后的读操作,都在load屏障之后执行。配合store屏障,使得store之前的写操作对load之后的读操作是可见的。
全屏障(full memory barrier)
full前的读写操作同步到内存后,才能执行full之后的操作。

重排序问题

为了提升性能,编译器和CPU会对指令做重排序,源码到最终执行,会经过三种重排序
在这里插入图片描述注:2、3属于CPU重排序

JMM(Java Memory Model)

JMM定义了共享内存中,多线程的读写操作规范。实现了将共享变量存储到内存、从内存中取出共享变量的底层细节。从而解决CPU多级缓存、处理器优化、指令重排序导致的内存访问问题。保证了并发场景下的可见性。
缓存一致性问题,有总线索、缓存锁,缓存锁基于MESI协议。
指令重排序问题,硬件层面提供了内存屏障。
JMM在此基础上提供了volatile、final等关键字,来解决可见性、重排序问题。

内存屏障分4类

在这里插入图片描述

HappenBefore

如果前一个操作的结果需要另一个操作时可见,那么这两个操作之间必须存在happens-before关系。这两个操作可以是同一个线程,也可以是不同线程。

1、程序顺序规则(as-if-serial语义)==
单个线程中的代码顺序不管怎么变,对于结果来说是不变的。
依赖问题,如果两个指令存在依赖关系,不许重排序。
1 happenns-before 2,3 happens-before 4
2、volatile变量规则
volatile修饰的变量,写操作一定happens-before读操作。
2 happens-before 3

3.传递性规则
如果1 happenns-before 2,3 happens-before 4,那么1 happenns-before 4。
4.Start规则
线程A 中ThreadB.start()操作happenns-before线程B中的任意操作。

public StartDemo{
int x=0;
Thread t1 = new Thread(()->{
// 子线程中,x==10
});
x = 10; // 此处对共享变量 x 修改,此操作对于子线程可见。
t1.start(); // 主线程启动子线程
}

5.join规则
线程A中ThreadB.join(),那么线程B的所有操作happenns-before线程的ThreadB.join()操作。

public StartDemo{ 
	int x=0; 
	Thread t1 = new Thread(()->{ 
		// 子线程中,x==10 
		x=100;	//修改X
	});  
	x = 10; 	// 此处对共享变量 x 修改,此操作对于子线程可见。
	t1.start(); 	// 主线程启动子线程
	t1.join();	//子线程的修改,在主线程执行t1.join()之后皆可见。X==100
} 

6.监视器锁的规则
解锁happenns-before下一个加锁。

synchronized (this) { // 此处自动加锁 
	if (this.x < 12) { // x 是共享变量, 初始值 =10 
		this.x = 12; 
	}
} 		// 此处自动解锁

线程A中x = 12,那么线程B拿到锁之后,能看到x == 12。

Synchronized

synchronized可以解决线程原子性问题,synchronized块之间的操作具备原子性。
Java SE 1.6优化了synchronized,引入了偏向锁、轻量级锁,减少获得锁、释放锁带来的性能开销。

public class Demo{ 
	private static int count=0; 
	public static void inc(){ 
		synchronized (Demo.class) {	//基于Demo对象的生命周期来控制锁粒度
			try { 
				Thread.sleep(1); 
			} catch (InterruptedException e) { 
				e.printStackTrace(); 
			} 
			count++; 
		}
	} 
	public static void main(String[] args) throws InterruptedException {
		for(int i=0;i<1000;i++){ 
			new Thread(()->Demo.inc()).start(); 
		} 
		Thread.sleep(3000); 
		System.out.println("运行结果"+count); 
	} 
}	

对象锁

synchronized 修饰方法

在这里插入图片描述
在这里插入图片描述

synchronized 修饰代码块 this/Synchronized_demo.this

在这里插入图片描述
在这里插入图片描述

多线程跑同一个对象

在这里插入图片描述
在这里插入图片描述

全局锁/类锁

synchronized 修饰 static 方法

各线程之间 抢锁
在这里插入图片描述
在这里插入图片描述

synchronized 修饰代码块 Synchronized_demo.class

各线程之间 抢锁
在这里插入图片描述

在这里插入图片描述

对象

对象的存储布局

在这里插入图片描述

对象头

包含了Mark Word、class指针、数组的长度(对象为数据时才有)

Mark Word(自身运行时数据)记录了对象和锁有关的信息。
32位操作系统为例:
在这里插入图片描述

synchronized 锁升级

所以在JDK1.6之后,synchronized中,锁存在4种状态:无锁、偏向锁、轻量级锁、重量级锁,锁状态由低到高不断升级。

偏向锁

大部分情况下,锁总被同一个线程多次获得,所以引入偏向锁。
对象头中存储线程ID,从而避免同一个线程再次进入、退出时获取锁、释放锁的操作。
如果多个线程竞争该锁,那么偏向锁就是一种累赘,可通过JVM参数UseBiasedLocking 来设置开启或关闭偏向锁。

偏向锁获取逻辑
1.获取锁对象的Mark Word,判断是否处于可偏向状态。
(biased_lock=1且 ThreadID 为空,则表示可偏向)
2.如果是可偏向状态,则通过CAS操作,把当前线程ID写入锁对象的Mark Word。
1)CAS成功,则获得偏向锁
2)CAS失败,说明偏向锁被其他线程占有,当前锁存在竞争,则撤销偏向锁,升级成轻量级锁。
3.如果是已偏向状态,则检查锁对象的Mark Word中的ThreadID 与当前线程的 ThreadID 是否相等。
1)如果相等,则无需再获得锁。
2)如果不相等,说明当前锁偏向于其他线程,要么重新偏向,要么撤销偏向锁,升级成轻量级锁。

偏向锁撤销逻辑
1.如果原获得偏向锁的线程同步代码块执行完了,那么锁对象设置成无锁状态,再重新偏向。
如果没有执行完,则在一个安全点停止拥有锁的线程A,修复锁记录和Mark Word,使其变成无锁状态,再唤醒线程A,将当前锁升级成轻量级锁。

轻量级锁

升级为轻量级锁之后,对象的Mark Word也会相应的变化。

轻量级锁的加锁逻辑
1.线程在自己的栈帧中创建锁记录LockRecord。
2.将锁对象 对象头中的MarkWord复制到线程刚刚创建的LockRecord。
3.将锁记录中的owner指针指向锁对象。
将锁对象对象头的MarkWord替换为指向锁记录的指针。
在这里插入图片描述
在这里插入图片描述
轻量级锁的解锁
锁释放逻辑其实就是获得锁的逆向逻辑。
通过CAS操作把线程栈帧中的LockRecord替换回到锁对象的MarkWord中,如果成功,表示没有竞争。如果失败,表示当前锁存在竞争,膨胀称为重量级锁。

自旋锁

轻量级锁在加锁的过程中,使用了自旋锁。
当一个线程来竞争锁时,会原地循环等待,直到锁被释放后,该线程直接获得锁,所以轻量级锁适用于同步代码块执行很快的场景。
自旋必须要一定的条件限制,否则不断循环,反而消耗CPU资源。默认情况下,自旋次数10次,可以通过preBlockSpin修改。
JDK1.6之后,引入自适应自旋锁,可根据前一次自旋时间以及锁拥有者的状态来觉得自旋次数。

重量级锁

当轻量级锁膨胀到重量级锁,未抢到锁的线程只能被挂起阻塞,等待被唤醒。
重量级锁是依赖对象内部的monitor锁来实现的,而monitor锁又依赖操作系统的MutexLock(互斥锁),所以重量级锁又称互斥锁。
当线程要去执行一段被synchronize修饰的方法或代码块时,需要先获得被synchronize修饰的对象的monitor监视器(monitorenter),获取失败,线程进入同步队列,变成blocked状态,直到锁被释放之后,当前线程会被唤醒,重新尝试对monitorenter的获取。

synchronized的执行过程
  1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁 。
  2. 如果不是,则使用CAS将当前线程的ID替换Mard Word里的线程ID,如果成功则表示当前线程获得偏向锁,置偏向标志位1 ,如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
  3. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁 ,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
  4. 如果自旋成功则依然处于轻量级,如果自旋失败,则升级为重量级锁。

sleep

Thread.sleep(1000)
阻塞1秒,期间不释放锁

wait

wait()阻塞当前线程,释放锁,并把当前线程放入等待队列,等待被唤醒。
wait()前提是必须先获得锁,这样才能释放锁,一般配合synchronized 关键字使用,即一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。

public class ThreadA extends Thread{
    private Object lock;
    public ThreadA(Object lock) {
        this.lock = lock;
    }
    
    @Override
    public void run() {
        synchronized (lock){
            System.out.println("start ThreadA");
            try {
                lock.wait(); //实现线程的阻塞,并且释放锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end ThreadA");
        }
    }
}

notify

notify()是将锁交给含有wait()方法的线程,让其继续执行下去,所以必须先持有锁

public class ThreadB extends Thread{
    private Object lock;
    public ThreadB(Object lock) {
        this.lock = lock;
    }
    @Override
    public void run() {
        synchronized (lock){
            System.out.println("start ThreadB");
            lock.notify(); //唤醒被阻塞的线程
            System.out.println("end ThreadB");
        }
    }
}

notifyAll

notifyAll()唤醒等待队列里的线程,等待队列并没有资格竞争锁,而是线程被移到同步队列后,再竞争锁。

jion

主线程合并子线程。join底层是使用wait()来实现,所以会释放锁。

Volatile

Vloatile遵循HappenBefore规则,能保证新值在修改后立即同步回主内存,每次使 用前从主内存刷新。
普通变量无法保证这一点,因为普通的共享变量修改后,什么时候同步写回主内存是不确定的,其他线程读取时,内存中可能还是原来的旧值。

public class App {
    public volatile static boolean stop=false;
    public static void main( String[] args ) throws InterruptedException {
        Thread t1=new Thread(()->{
            int i=0;
            while(!stop){ //condition 不满足
                i++;
            }
            System.out.println(i);
        });
        t1.start();
        Thread.sleep(10);
        stop=true; //true 主线程设置stop为true,对子线程可见。
    }
}

final关键字提供了内存屏障的规则

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

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

相关文章

智慧工地AI视频分析系统 opencv

智慧工地AI视频分析系统通过pythonopencv网络模型图像识别技术&#xff0c;智慧工地AI视频分析算法自动识别现场人员穿戴是否合规。本算法模型中用到opencv技术&#xff0c;OpenCV基于C实现&#xff0c;同时提供python, Ruby, Matlab等语言的接口。OpenCV-Python是OpenCV的Pyth…

研讨会回顾 | Perforce版本控制工具Helix Core入华十年,携手龙智赋能企业大规模研发

2023年2月28日&#xff0c;龙智联合全球领先的数字资产管理工具厂商Perforce共同举办Perforce on Tour网络研讨会&#xff0c;主题为“赋能‘大’研发&#xff0c;助力‘快’交付”。 作为Perforce Helix Core产品在中国地区的唯一授权合作伙伴&#xff0c;龙智董事长何明女士为…

六、GoF之工厂模式

设计模式&#xff1a;一种可以被重复利用的解决方案。 GoF&#xff08;Gang of Four&#xff09;&#xff0c;中文名——四人组。 《Design Patterns: Elements of Reusable Object-Oriented Software》&#xff08;即《设计模式》一书&#xff09;&#xff0c;1995年由 Eric…

Sidecar-详解 JuiceFS CSI Driver 新模式

近期发布的 JuiceFS CSI Driver v0.18 版本中&#xff0c;我们提供了一种全新的方式访问文件系统&#xff0c;即 JuiceFS 客户端以 Sidecar 方式运行于应用 Pod 中&#xff0c;且客户端与应用同生命周期。 这个全新的功能将帮助用户在 Serverless Kubernetes 环境中使用 Juice…

【Python每日一练】总目录(不断更新中...)

Python 2023.03 20230303 1. 两数之和 ★ 2. 组合总和 ★★ 3. 相同的树 ★★ 20230302 1. 字符串统计 2. 合并两个有序链表 3. 下一个排列 20230301 1. 只出现一次的数字 2. 以特殊格式处理连续增加的数字 3. 最短回文串 Python 2023.02 20230228 1. 螺旋矩阵 …

基于k3s部署KubeSphere

目录相关文档准备工作安装K3S安装KubeSphere相关文档 k3s官网&#xff1a;https://docs.k3s.io/zh/quick-start k3s所有版本查看&#xff1a;https://github.com/k3s-io/k3s/tags kubesphere文档&#xff1a;https://kubesphere.io/zh/docs/v3.3/quick-start/minimal-kubesp…

2023爱分析·RPA软件市场厂商评估报告:容智信息

目录 1. 研究范围定义 2. RPA软件市场分析 3. 厂商评估&#xff1a;容智信息 4. 入选证书 1. 研究范围定义 RPA即Robotic Process Automation&#xff08;机器人流程自动化&#xff09;&#xff0c;是一种通过模拟人与软件系统的交互过程&#xff0c;实现由软件机器人…

【python+selenium自动化测试实战项目】全面、完整、详细

今天整理一下实战项目的代码共大家学习。 不说废话&#xff0c;直接上项目 项目简介 项目名称&#xff1a;**公司电子零售会员系统 项目目的&#xff1a;实现电子零售会员系统项目自动化测试执行 项目版本&#xff1a;v1.0 项目目录 项目环境 本版 python 36 pip insat…

Linux开放的端口太多了?教你一招找出所有开放的端口,然后直接干掉!

基于服务器安全性维护的目的&#xff0c;查看所有开放的端口是通常采取的第一步&#xff0c;从中检查出可疑或者不必要的端口并将其关掉。关于查看开放的端口&#xff0c;方法不止一种&#xff0c;比如lsof 命令&#xff0c;还可以使用 ss 命令。 查看开放的端口 今天我们就介…

分布式缓存 Memcached Linux 系统安装

1.Memcached简介 Memcached是一个开源、高性能&#xff0c;将数据分布于内存中并使用key-value存储结构的缓存系统。它通过在内存中缓存数据来减少向数据库的频繁访问连接的次数&#xff0c;可以提高动态、数据库驱动之类网站的运行速度。 Memcached在使用是比较简单的&#…

Clip:学习笔记

Clip 文章目录Clip前言一、原理1.1 摘要1.2 引言1.3 方法1.4 实验1.4.1 zero-shot Transfer1.4.2 PROMPT ENGINEERING AND ENSEMBLING1.5 局限性二、总结前言 阅读论文&#xff1a; Learning Transferable Visual Models From Natural Language Supervision CLIP 论文逐段精读…

只需4步,让OKA40i-C开发板的Linux系统拥有中文显示

如果你试着在Linux系统里面输入中文&#xff0c;那么将会有一片乱码呈现在你面前&#xff0c;这是因为Linux系统的默认语言是英文。但是如果可以显示中文的话&#xff0c;那么在使用过程中的便利程度一定会大大提升。今天小编就通过飞凌嵌入式的OKA40i-C开发板来为大家演示让Li…

极狐GitLab DevSecOps 为企业许可证安全合规保驾护航

本文来自&#xff1a; 小马哥 极狐(GitLab) 技术布道师 开源许可证是开源软件的法律武器&#xff0c;是第三方正确使用开源软件的安全合规依据。 根据 Linux 发布的 SBOM 报告显示&#xff0c;98% 的企业都在使用开源软件&#xff08;中文版报告详情&#xff09;。随着开源使用…

微电影广告有哪些传播优势?

微电影广告是在基于微电影的模式下发展而来的&#xff0c;是伴随着当下快节奏、碎片化的生活方式而诞生的新兴广告表现形式。微电影广告凭借其具备的独特传播优势以及时代特征成为广大企业主塑造企业品牌形象的主要方式。那么&#xff0c;微电影广告究竟有哪些传播优势&#xf…

Ubuntu22.04安装、配置、美化、软件安装、配置开发环境

Ubuntu22.04安装、配置、美化、软件安装、配置开发环境 一、Ubuntu、Windows11&#xff08;10&#xff09;双系统安装 因为ubuntu的安装网上的教程特别多了&#xff0c;所以这里不做赘述&#xff0c;推荐使用小破站这个up主的教程&#xff1a;Windows 和 Ubuntu 双系统从安装到…

计算机系统的基本组成 第一节

一、计算机系统 计算机系统是指&#xff1a;电子数字通用、计算机系统 由硬件和软件两个子系统组成 硬件是保存和运行软件的物质基础 软件是指挥硬件完成预期功能的智力部分 重点&#xff1a; 计算机系统部件 五个 1、数据运算部件&#xff1a;完成对数据的运算处理功能…

Docker 配置WebSSH

1、基于DockerHub Centos镜像 docker pull centos:centos7 2、 Centos镜像存在的一个自身问题&#xff1a;启动后的容器内部无法使用systemctl命令 Failed to get D-Bus connection: Operation not permitted ## docker run -dit eeb6ee3f44bd /bin/bash ## 切勿忘记宿主机防…

【测绘程序设计】——平面坐标转换

测绘工程中经常遇到平面坐标转换——比如,北京54(或西安80)平面坐标转换成CGCS2000平面坐标、工程独立坐标系平面坐标转换成CGCS2000平面坐标等,常用转换模型包括:①三参数法(2平移+1旋转);②四参数法(赫尔默特法,2平移+1旋转+1尺度);③六参数法(仿射变换法,2平移…

00后看完这个直接拿下字节offer,软件测试100+道面试题

软件测试与质量保证-软件测试部分练习题 1单选(2分) 软件测试用例主要由输入数据和_________两部分组成。 A.预期输出结果2.00/2.00 B.测试计划 C.以往测试记录分析 D.测试规则 2单选(2分) 与设计测试用例无关的文档是_________。 A.项目开发计划2.00/2.00 B.源程序 …

Ansys的电磁场分析和系统电路仿真软件Electronics 2023版本下载和安装教程

目录前言一、工具安装二、使用配置总结前言 ANSYS Electromagnetics Suite或ANSYS Electronics Suite是几个功能强大的程序的集合&#xff0c;用于仿真系统的电磁。ANSYS电磁套件还公开了行业分支机构和专业目录&#xff0c;并有标志。例如&#xff0c;在设备中&#xff0c;机…