当前位置: 首页 > 后端技术 > Java

Java锁(一):volatile、synchronized底层实现原理详解

时间:2023-04-02 00:42:25 Java

一、锁的基础知识锁的种类锁客观上分为悲观锁和乐观锁。乐观锁:乐观锁是一种乐观的思想。认为少写多读,并发写的可能性比较低。读数据的时候相信别人不会修改,所以读的时候不会加锁,但是写的时候会时不时的去判断这段时间别人有没有更新过数据。方法是先读取当前版本号,然后锁定操作。写入完成后,读取最新版本号,记录版本号。如果版本号相同,则成功。如果失败,则重复读-比较-写操作。Java中的乐观锁基本都是通过CAS操作实现的,java.util.concurrent.atomic包下的原子变量。CAS(compareandswap)比较交换是一个原子更新操作,比较当前值和传入值是否相同,如果相同则更新,否则失败。悲观锁:悲观锁是悲观的思想,认为写很多遇到并发的可能性很大,每次拿到数据都认为是被别人修改了,所以每次读写都会加锁,这样其他人想要读写数据就会被阻塞(blocked),直到获得锁。Java中的悲观锁是syschronized,AQS框架下的锁先尝试CAS乐观锁获取锁。如果获取不到,就会变成悲观锁,比如ReentrantLock。Java中的锁Java中的锁机制主要有两种:syschronized关键字修饰java.util.concurrent.Lock,Lock是一个接口,还有很多实现类比如ReentrantLock。2.易失性可见性publicclassVolatileTest{publicstaticvoidmain(String[]args){finalVTvt=newVT();线程thread01=新线程(vt);Threadthread02=newThread(newRunnable(){@Overridepublicvoidrun(){try{Thread.sleep(3000);}catch(InterruptedExceptionignore){}vt.sign=true;System.out.println("vt.sign=truenotifywhile(!sign)isover!");}});thread01.开始();thread02.start();}}类VT实现Runnable{publicbooleansign=false;@Overridepublicvoidrun(){while(!sign){}System.out.println("Youarebad");}}上面的代码是两个线程同时操作一个变量,程序希望在对线程Thread01vt.sign=true进行sign操作时,线程Thread02输出youarebad。其实这段代码永远不会输出你不好,而是一直在死循环。为什么是这样?接下来,我们将逐步解释验证。我们将sign关键字添加到volatile关键字中。publicvolatilebooleansign=false;这次它会输出你的坏。volatile关键字是Java虚拟机提供的最轻量级的锁同步机制。它作为修饰符出现,用于修改变量,不包括局部变量,以确保对所有线程可见。没有volatile关键字修饰时内存变化没有volatile关键字修饰时,Thread01对变量进行操作,Thead02取不到最新值。有volatile关键字时内存变化修改volatile关键字时,Thread01对变量进行操作时,会强制刷新变量变化到主内存,Thread02取值时,会过期自己的sign值内存,从主存中读取最新的。有序性底层volatile关键字通过lock指令实现可见性。lock指令相当于一个内存屏障,保证了以下三点:将处理器的缓存写入主存。重新排序时,后面的指令不会在内存屏障之前重新排序。如果是写操作,其他内存设备中对应的内存将失效。综上所述,volatile关键字会在内存运行时控制修改后的变量主动刷新值到主内存,而JMM会先过期线程对应的CPU内存设置,并从内存中读取最新的值。volatile关键字是为了防止通过内存屏障进行指令重排。读写时,在前后加一个Storebarrier的volatilememorybarrier,保证reorder时,memorybarrier后面的指令不会排在memorybarrier之前。volatile解决不了原子性。如果需要解决原子性,就需要synchronized或者lock。三、如何使用synchronized知识大??纲synchronized关键字主要有以下三种使用方式:修改实例方法、作用于当前实例加锁、在进入同步代码前获取当前实例的锁。公共类SynchronizedTest实现Runnable{privatestaticinti=0;publicsynchronizedvoidgetI(){if(i%1000000==0){System.out.println(i);}}publicsynchronizedvoidincrease(){i++;得到我();}@Overridepublicvoidrun(){for(intj=0;j<1000000;j++){increase();}System.out.println(i);}publicstaticvoidmain(String[]args){ExecutorServiceexecutorService=Executors.newCachedThreadPool();同步测试synchronizedTest=newSynchronizedTest();executorService.execute(synchronizedTest);executorService.execute(synchronizedTest);executorService.shutdown();}}最后结果输出:1000000155662320000002000000上记代码中,创建两个线程同时操作同一个共享资源i,increase()和get()方法添加synchronized关键字,表示当前线程的锁是一个实例对象,因为传入的线程都是synchronizedTest对象实例,所以最后的结果肯定会输出2000000。如果我们换一种方式,传入不同的对象,代码如下:publicstaticvoidmain(String[]Args){ExecutorServiceexecutorService=Executors.newCachedThreadPool();同步测试synchronizedTest01=newSynchronizedTest();同步测试synchronizedTest02=newSynchronizedTest();executorService.execute(synchronizedTest01);executorService.execute(synchronizedTest02);executorService.shutdown();}输出如下:100258816412671848269最后肯定不是预期的200000,因为synchronized修改方法锁住当前实例,传入不同对象实例线程是修改静态方法,不能保证安全。类对象的锁。公共类SynchronizedTest实现Runnable{privatestaticinti=0;publicsynchronizedstaticvoidgetI(){if(i%1000000==0){System.out.println(i);}}publicsynchronizedstaticvoidincrease(){i++;得到我();}@Overridepublicvoidrun(){for(intj=0;j<1000000;j++){increase();}System.out.println(i);}publicstaticvoidmain(String[]args){ExecutorServiceexecutorService=Executors.newCachedThreadPool();同步测试synchronizedTest01=newSynchronizedTest();同步测试synchronizedTest02=newSynchronizedTest();executorService.execute(synchronizedTest01);executorService.execute(synchronizedTest02);executorService.shutdown();}}输出结果如下:1000000164953020000002000000上面的代码和第一段代码类似,只是increase()和get()方法是静态方法,另外还加了synchronized表示锁是当前类对象,虽然我们传入了不同的对象,但是最终的结果会输出200000的修饰符代码块指定锁定对象,锁定对象,在进入同步方法之前获取给定对象的锁。公共类SynchronizedTest02实现Runnable{privatestaticSynchronizedTest02synchronizedTest02=newSynchronizedTest02();私有静态整数i=0;@Overridepublicvoidrun(){//传入对象锁定当前实例对象//如果是同步的(Synchronized(Synchronized.class)TestLock当前类对象synchronized(synchronizedTest02){for(intj=0;j<1000000;j++){i++;}}}publicstaticvoidmain(String[]args)抛出异常{Threadthread01=newThread(synchronizedTest02);Threadthread02=newThread(synchronizedTest02);thread01.start();thread02.start();Thread.sleep(3000);System.out.println(i);如果是对象,说明锁是当前实例对象,如果是类,说明锁是类对象。原子性原子性是指一个操作不能被中断,要么成功要么失败,Synchroniezd可以实现方法同步,同一时间段内只有一个线程可以拿到锁,进入代码执行,从而达到原子性。底层通过执行mointorenter命令判断是否有ACC_SYNCHRONIZED同步标志。如果是,则表示获取了监视器锁。此时,计数器为+1。执行该方法后,将执行mointorexit。可见性可见性是指一个线程修改了一个共享变量的值,其他线程可以知道这个修改。CPU缓存优化、指令重排等可能会导致共享变量的修改不会立即被其他线程注意到。Synchroniezd通过操作系统内核互斥实现可见性。线程在释放锁之前,必须将共享变量的最新值刷新到主存中。线程在获得锁之前,会清空工作内存中的共享值,从主内存中获取最新的值。.当执行顺序程序时,指令可能会重新排列,CPU执行指令的顺序不一定与程序的顺序一致。指定重排,保证串行语义一致(即重排后CPU执行的执行顺序与程序实际执行顺序一致)。synchronized可以保证CPU执行指令的顺序和程序执行的顺序一致。publicclassLazySingleton{/***单例对象*volatile+双重检测机制->禁止重排序*/privatevolatilestaticLazySingletoninstance=null;/***实例=newLazySingleton();*1.分配对象内存空间*2.初始化对象*3.设置实例指向刚刚分配的内存**JVM和CPU优??化,发生指令重排,1-3-2,线程A执行完毕3,线程B执行第一次判断,直接返回,这次是*有问题的。*通过volatile关键字禁用重新排序*@return*/publicstaticLazySingletongetInstance(){if(null==instance){synchronized(LazySingleton.class){if(null==instance){instance=newLazySingleton();}}}返回实例;}}synchronized的顺序是为了保证线程有序执??行,并不是为了防止指令重排序。上面的代码如果不加volatile关键字,可能的结果是,当第一个线程初始化的时候,设置实例执行分配的内存时,此时第二个线程进来,指令重新排列。在第一次判断直接返回的时候,出现了错误。这个时候实例可能没有初始化成功。Reentrantsynchronized是一种可重入锁,它允许线程请求持有对象锁的第二个关键资源。公共类SynchronizedTest03扩展A{publicstaticvoidmain(String[]args){SynchronizedTest03synchronizedTest03=newSynchronizedTest03();synchronizedTest03.doA();}publicsynchronizedvoiddoA(){subclass:System.out.println("SynchronizedTest03.doA()ThreadId:"+Thread.currentThread().getId());做B();}publicsynchronizedvoiddoB(){System.out.println("子类方法:SynchronizedTest03.doB()ThreadId:"+Thread.currentThread().getId());超级.doA();}}classA{publicsynchronizedvoiddoA(){System.out.println("父类方法:A.doA()ThreadId:"+Thread.currentThread().getId());}}以上代码正常输入如下:子类方法:SynchronizedTest03.doA()ThreadId:1子类方法:SynchronizedTest03.doB()ThreadId:1父类方法:A.doA()ThreadId:1最后输出结果正常,没有出现死锁,说明synchronized是可重入锁。当synchronized锁对象有一个计数器时,它记录线程获得锁的次数。相应的代码执行完后,计数器就会为-1,直到计数器清0,释放锁。类型与升级在介绍锁的类型之前,先说说什么是标记。标记是java对象数据结构的一部分。标记数据在虚拟机中有32位和64位,长度为32位和64位(压缩指针没有开启),它的最后两位是锁状态标志,用来标记当前对象的状态,如下:状态标志存放的是没有锁的内容(偏向锁没有开启)01对象哈希码,对象分代年龄biasedlock(启用偏向锁)01偏向线程id,偏向时间戳,对象世代年龄轻量级锁00指向轻量级锁指针重量级锁10指向重量级锁指针GCmark11null偏向锁偏向锁会偏向第一个如果有运行过程中只有一个线程访问锁,多线程之间不存在争用,线程不需要触发同步。这时候就会给线程加一个偏向锁。如果在运行过??程中有其他线程抢占了锁,则持有偏向锁的线程会被挂起,JVM会消除其上的偏向锁,将锁升级为轻量级锁。UseBiasedLocking是偏向锁检查,1.6之后默认开启,1.5关闭,需要手动开启的参数为XX:UseBiasedLocking=false。偏向锁获取过程:访问markword中的偏向锁是否表示是否为1,锁标志位为01,确认为偏向锁状态。判断mark中的threadid是否指向当前threadid,如果是,则转第5步,否则转第3步,如果mark中的threadid不指向当前线程id,则使用CAS来操作比赛锁。如果竞争成功,则指向当前线程id,执行第5步;如果竞争失败,则执行步骤4。如果CAS竞争锁失败,则表示存在竞争。当到达全局安全点(safepoint)时,获取偏向锁的线程会被挂起,偏向锁升级为轻量级锁并撤销偏向锁(撤销偏向锁会导致停止word,除了GC需要的线程,所有线程都进入等待状态,直到GC任务完成),然后阻塞在安全点的线程会继续执行同步代码。执行同步代码。轻量级锁当锁为偏向锁时,如果在运行过??程中发现其他线程抢占锁,则将偏向锁升级为轻量级锁,其他线程以自旋的形式获取锁,不会阻塞,提高性能,缺点是循环消耗CPU。轻量级锁上锁流程:当代码进入同步块时,如果同步对象锁状态为无锁状态(锁状态标志为01,是否为偏向锁为0),虚拟机会先锁住当前线程在帧栈中建立了一个叫做LockRecord的空间,用来存放锁对象当前markword的一份副本,官方称之为DisplacedMarkWord。将对象的标记字复制到锁记录中。复制成功后,虚拟机会使用CAS操作尝试将对象的标记更新为锁记录的指针,并将锁记录中的owner指向对象的标记。如果更新成功,转第4步,否则转第5步。更新成功意味着线程已经获取到锁定的对象,对象的markword锁标志被置为00,表示对象处于一个轻量级锁定状态。如果更新失败,说明虚拟机首先会检查对象的标记是否指向当前线程的栈帧,如果是,说明当前线程已经获取了对象的锁。如果不是,说明有多个线程在竞争锁,轻量级锁升级为重量级锁,锁标志的状态值变为10,指向重量级锁的指针存储在markword中,并且等待锁的线程将进入阻塞状态。重量级锁当偏向锁升级为轻量级锁时,其他线程会通过自旋的方式获取锁,不会阻塞。如果自旋失败n次,此时轻量级锁将升级为重量级锁Lock。总结一下synchronized的执行过程:检查mark中是否存储了当前线程的id,如果有则说明当前线程处于偏向锁中。如果不是,请尝试使用CAS将markword替换为当前线程的id。如果成功,则表示当前线程获取到锁,偏向标志的位置为1。如果CAS失败,则表示存在竞争,取消偏向锁,然后升级为轻量级锁,则lockflag设置为00。当前线程使用CAS将对象的markword替换为一个lockrecordpointer。如果成功,则当前线程获取锁。如果替换失败,说明其他线程在竞争锁,当前线程尝试使用自选锁获取锁。如果自旋成功获取到锁,它仍然是一个轻量级锁。如果自旋失败,则升级为重量级锁,锁标志位设置为10。