volatile与synchronized原理

永远的循环demo

 public class test {
        static   boolean stop = false; //停止标记

        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                stop = true; // volatile 的写
                System.out.println("thread: " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));

            });
            System.out.println("start " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
            t.start();
            foo();
        }

        private static void foo() {
            while (true) {
                boolean b = stop; // volatile 的读
                if (b) {
                    break;
                }
            }
            System.out.println("end " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        }
    }

执行在上面的这段代码,永远无法打印出“foo 方法中的 end” ,想要知道为什么,那么就必须要了解一下JMM

加加减减出现了数据计算错误的情况demo


第四种可能demo

在这里插入代码片

在揭秘出现这些问题的原因之前必须要明白两个事实1、写的代码未必就是实际运行的代码;2、代码的编写顺序未必就是实际的执行顺序;那为什么会这样呢?在多线程下在多线程下一个cpu总占用cpu是不合理的,任务调度器会让线程分时使用CPU,编译器以及硬件层面都会做成层层优化,提升性能
了解一下编译器优化:
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;在保证代码最终执行结构一致的前提下对没有依赖的变量进行优化执行行顺序以获得更好的性能
在这里插入图片描述

了解一下指令流水线优化

了解一下缓存优化

这些都不是重点

早期计算机的内存

其实早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的处理速度要远远的高于计算器的存储速度,处理结束等待存储完成显然这个是不合理的,所以不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲。将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了,基于高速缓存解决了处理器与缓存之间的矛盾,但是这又引发了另外一个问题“缓存一致性”;借用网上的图
在这里插入图片描述在多个处理器的计算器中每个计算器都有自己的一个高速缓存来解决处理快和存储慢的矛盾问题,但是因为他们又共同使用了同一块主内存多个高速缓存需要将自己的缓存内的内容同步到主内存导致了缓存一致性的问题;

JMM

JMM的规定

java内存模型描述了java程序在多线程下各种变量(线程共享变量)的访问规则以及在JVM中将变量存储到内存和从内存中读取变量的这样的底层细节,JMM有这样的规定,所有的共享变量都存储在主内存,线程的工作内存保留了主内存的工作副本,线程对变量的所有的操作读和写必须在工作内存中完成,不能直接读写主内存的变量,不同线程之间也不能访问对方工作内存中的变量,线程中变量的同步在主内存中转完成,工作内存和主内存之间的关系如下
在这里插入图片描述就是工作内存和主内存这样的关系,导致了多线程下读写共享变量会出现如如下接个问题1、原子性问题-线程切换导致的指令交错;2、可见性有序性问题-jit编译优化,cpu缓存优化;解决这些问题的关键在于java内存模型,解决的手段就是使用同步方法synchronized,ReentrantLock(解决原子性、可见性、有序性问题),volatile(解决可见性问题,有序性问题,思考为什么不能解决原子性问题)

JMM的规则

内存模型就是多线程下对共享变量的一组读写规则,符合这个套读写规则就可以保证多线程下的有序性 可见性 以及原子性
规则一:RaceCondition规范 ,在多线程下 没有依赖关系的代码,在执行共享变量的读写操作(至少一个线程写),并不能保证以实际编码的顺序执行,这称为发生了竞态条件,想要保证执行顺序方法有很多比如加锁synchronization,使用volatile都可以,竞争是为了更好的性能,并不能因为麻烦而不去使用;

假设有共享变量x,线程1执行
			r.r1=y;
			r.r2=x;
		线程2执行
			x=1;
			y=1;

	最终可能出现的结果可能是  r1==1,r2==0;
执行的实际顺序可能如下:
	y=1;
	r.r1=y;
	r.r2=x;
	x=1;
这就是说明了在多线程下,没有依赖关系的代码并不能保证代码的执行顺序

规则2—>Synchronization Order 同步动作一个线程中代码的执行顺序
若要保证多线程下的每个线程的执行顺序按编写顺序执行,那么必须使用Synchronization actions来保证,这些SA有1、lock,unlock-synchronization;2、volatile 方式读写共享变量可以保证可见性防止重排序,但不能保证原子性 ;3、varHandle方式读写变量jdk9引入;

用volatile修饰共享变量 y,线程1执行
		r.r1=y
		r.r2=x;
线程2执行
		x=1;
		y=1;
最终的结果就不可能出现 r1==1而r2==0;

Synchronization Order 同步动作保证的是一个线程中代码的执行顺序,并不是阻止多线程切换

线程1执行
	synchronized(lock){
		r1=x; //1处
		r2=x;//2处
	}
线程2执行
	synchronized(lock){
		x=1;
	}

并不是说 //1处与2处之间不能切换到线程2 ,只是即使切换到了线程2,因为线程2不能拿到lock锁,导致阻塞,执行权又会伦到线程1

规则3–>Happens-Before规则,线程切换时代码的顺序和可见性
若是变量读写时发生线程切换(比如线程1写入x,切换至线程2,线程2读取x)这些边界上如果有action1先于action2发生,那么代码可以按照确定的顺序执行,这种称为HappensBefre规则;可以理解为如果action1先于action2发生,那么action1之前的共享变量对象action2可见,并且按照代码的编写顺序执行;
具体规则:
1)线程的起懂和运行边界
2)线程的结束和 join 边界
3)线程的打断和得知打断边界
4)unlock 与 lock 边界
5)volatile write 与 volatile read 边界
规则4–>安全发布
若要安全的构造对象,并将其共享使用,需要使用final或者volatile修饰,并且避免this溢出的情况发生,使用stattic静态成员变量可以安全的发布,但是不属于懒加载的方式

例如:
	class student{
		int  x1;
		volatile int x2;         
		public Holder ( int v){
		}
	}
讲这个对象作为全局使用  
Holder f;
现在有两个线程一个去创建这个对象,一个去使用这个对象

同步动作

内存屏障

一共有四种内存屏障,具体的实现与cpu的架构有关
LoadLoad屏障:防止B的Load重排到A的Load之前
		if(A){
			LoadLoad
			return B
		}
		read(A);
		LoadLoad
		read(B)
		A==true时,再去获取B ,否则可能会因为重排的问题导致B的值相对A来说是过期的

LoadStore屏障:防止B的写被重拍到A的读之前
StoreStore屏障:防止A的写被重排到B的写的后面,

		A=x;
		storeStore
		B=true;
		意义在与B为true之前,其他的线程别想看到A的修改

storeLoad(*)屏障:线程间的屏障,屏障前的改动都同步到主内存,屏障后的写获取到最新的数据,防止屏障前所有的写的操作被重新排序到屏障后的任何操作

storeLoad屏障

Volatile的本质

定义一个变量共享变量x用volatile 修饰实际上加上了三组屏障,蓝色的代表线程1 ,红色的代表线程2;y=10是写的操作,在写操作x=10的时候,在前面加上storestore的屏障保证x=10前面的所有的写的操作无法越过屏障,后面加上storeload屏障保证之前的写不能够越过屏障之后的读也不能越过屏障,将数据写到主内存后面的获取到的是最新的数据;读取x变量的时候相当于加上了loadload和loadStore屏障,相当于r1=x之后的读操作不能越过屏障,r1=x之后的写不能越过屏障;加上了屏障之后就保证了代码的有序性,当然也有例外
在这里插入图片描述在这里插入代码片

volatile的有序性

public class TestOrderingPartial {
    @JCStressTest
    @Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
    @State
    public static class Case1 {
        int x;
        int y;

        @Actor
        public void actor1() {
            x = 1;
            y = 1;
        }

        @Actor
        public void actor2(II_Result r) {
            r.r1 = y;
            r.r2 = x;
        }
    }

执行上面的代码可能会出现如下几种情况
在这里插入图片描述出现 ‘00’,‘11’,“01”的情况,但是还有两种意外就是出现r1=1,r2=0的情况的情况,将用volatile修饰就不会出现r1=1,r2=0的情况,具体原理如下图,

 public static class Case1 {
        int x;
        volatile int y;

在这里插入图片描述使用了volatile修饰了y,相当于在y=1前加了loadstore和storestore屏障,保证了前面读和写不可以越过屏障,后面加上了storeload保证了在上面的写不会排到后面,下面的读不会拍到前面,确保了线程切换时的有序性;

在这里插入图片描述
用volatile修饰了x,红色的线程中 r.r2=x,对于x的读限制了下面的代码的读和写无法越过屏障,但是限制不了r.r1=y向下走动,又由于内存屏障保证了线程内的代码的有序性(除了storeLoad对线程有影响保证有序),但是阻止不了线程间的指令切换如下图,先去读取r.r2=x;在去执行,这样照成了结果是r2=0,r1=1;

在这里插入图片描述
总结 :对于volatile的使用,volatile要用来修饰最先读取,最后写的变量;在同一个线程中volatile修饰的变量由于laodload与loadstore屏障的作用,保证了该变量的前面所有的读和写无法越过被修饰的变量(同一个线程中的有序性),由于storeoad屏障保证了将数据写到主内存,后续的读无法越过屏障,从而获取到最新的数据(切换线程时候的可见性);

volalatile的使用场景

volatile一般都是与cas一起使用保证原子性,可见性和有序性;
atomicinterger

第一个demo中添加volatile修饰解决可见性问题

 public class test {
        static  volatile  boolean stop = false; //停止标记

        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                stop = true; // volatile 的写
                System.out.println("thread: " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));

            });
            System.out.println("start " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
            t.start();
            foo();
        }

        private static void foo() {
            while (true) {
                boolean b = stop; // volatile 的读
                if (b) {
                    break;
                }
            }
            System.out.println("end " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        }
    }

解决可见性和线程内有序性的本质是在volatile修饰的变量做读的操作加上了内存屏障写的操作也加上了内存屏障

Synchronized的本质

在这里插入图片描述
流程:synchronized本质是monotorcenter(根据lock对象找到monitor锁)和monitorexit(根据lock对象做锁的释放),底层的过程为thread1根据表面的java对象lock获取到monitor锁,实际上是系统提供的monitor锁只是根据这个对象找到系统提供的monitor锁的地址,根据monitor地址就可以找到monitor对象,当然首次访问就会创建这个monitor对象,Owner是锁的持有者,刚开始是空的,thread1进入monitor对象判断是否为空,如果是空的就持有owner,那么thread1就成为锁的持有者,thread2进入monitor对象的时候,发现owner中已经有thread1会尝试获取锁,如果尝试几次失败后就会进入EntryList,并且将线程状态改为阻塞状态,thread3进来同样会尝试获取到owner对象,如果失败就会进入EntryList中,并且EntryList是一个链表结构,这就是整个加锁的过程,解锁的过程,当thread1执行到monitorexit(lock)将owner置为空,这时唤醒thread2恢复运行,获取owner锁;

有锁VS无锁

synchronized悲关锁,如果一个线程执行时间比较长,那么其他线程都因为获取不到锁而陷入阻塞,阻塞的线程保存线程中代码如局部变
量等的执行状态,当被唤醒的时候还需要将所有的代码状态全部都恢复,引起线程上下文切换,成本高;atomicInteger实现乐观锁并非加锁而是所有所有的线程全部都继续执行遇到锁的状态下“等”一会继续执行重复这个过程,减少了悲关锁引起的上下文切换成本高的问题,
但是受到cup的核数限制,适用于于短频快的比如说计数器
1、synchronized 更为重量,申请锁、锁重入都要发起系统调用,频繁调用性能会受影响
2、synchronized 如果无法获取锁时,线程会陷入阻塞,引起的线程上下文切换成本高
3、如果数据的原子操作时间较长,仍应该让线程阻塞,无锁适合的是短频快的共享数据修改操作主要用于计数 器、停止标记、或是阻塞前的有限尝试

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值