【线程安全篇】

线程安全之原子性问题

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关键字提供了内存屏障的规则

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嗨!很高兴回答你关于Python游戏脚本入门的问题。在Python中,多线程是一种并发处理的技术,它允许程序同时执行多个线程,从而提高程序的运行效率和响应能力。在游戏开发中,多线程可以用于处理游戏中的多个任务或实现并行计算。 要在Python中使用多线程,可以使用内置的`threading`模块。下面是一个简单的示例,演示如何在Python中创建和启动多个线程: ```python import threading def task(): # 这里是线程要执行的任务 print("Hello, I'm running in a thread!") # 创建线程对象 thread1 = threading.Thread(target=task) thread2 = threading.Thread(target=task) # 启动线程 thread1.start() thread2.start() ``` 在上面的示例中,我们首先定义了一个`task`函数,这是线程要执行的具体任务。然后,我们使用`threading.Thread`类创建了两个线程对象`thread1`和`thread2`,并将`task`函数作为参数传递给它们。最后,我们调用`start`方法来启动这两个线程。 多线程的执行是并发的,所以你可能会看到输出信息交替出现。在实际的游戏开发中,你可以利用多线程来处理不同的游戏逻辑、计算复杂的物理模拟或者处理网络通信等任务,从而提升游戏的性能和玩家体验。 但是需要注意的是,多线程编程需要注意线程之间的同步和资源竞争问题。在游戏开发中,你可能需要使用锁和同步原语来确保线程之间的安全操作。 希望这个简单的介绍对你有所帮助!如果你有任何其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值