当前位置: 首页 > 科技观察

Java并发编程之Synchronized关键字

时间:2023-03-12 10:58:24 科技观察

并发编程的重点和难点是数据同步、线程安全、锁。编写线程安全代码的核心是管理对共享和可变状态的访问。Shared意味着变量可以被多个线程访问,而mutable意味着变量的值可以在其生命周期内改变。当多个线程访问一个状态变量,其中一个执行写操作时,必须使用同步机制来协调这些线程对该变量的访问。Java中主要的同步机制是关键字synchronized,它提供了独占锁的方式。Gogou从以下几个方面来学习synchronized:synchronized关键字的特点synchronized关键字可以实现一种简单的策略来防止线程干扰和内存一致性错误。如果一个对象对多个线程可见,那么这个对象的所有读写都必须同步完成。synchronized的特点:不间断:synchronized关键字提供了独占锁定方式。一旦一个线程持有锁对象,其他线程将进入阻塞状态或等待状态,直到前一个线程释放锁,中间过程无法中断。原子性:synchronized关键字的不间断性保证了它的原子性。可见性:synchronized关键字包含了两条JVM指令:monitorenter和monitorexit,可以保证任何线程在执行monitorenter时必须随时从主存中获取数据,而不是从线程工作内存中获取数据。监视器退出后,必须将工作内存的更新值保存到主内存中,从而保证数据的可见性。有序性:synchronized关键字修饰的同步方法是串行执行的,但是它修饰的代码块中的指令顺序还是会发生变化,这种变化遵循java的happens-before规则。重入:如果拥有锁的线程再次获取锁,则monitor的计数器会加1,线程释放锁时会减1,直到计数器为0,表示线程已经释放锁,其他线程被阻塞,直到计数器不为0。关键字synchronized的用法synchronized关键字锁定对象,可以修改代码块和方法,但不能修改类对象和变量。代码块,锁对象是objectprivatefinalObjectobj=newObject();publicvoidsync(){synchronized(obj){}}方法,锁对象是thispublicsynchronizedvoidsyncMethod(){}静态方法,锁对象是classpublicsynchronizedstaticvoidsyncStaticMethod(){}最常用used是用synchronized关键字修饰对象,可以控制锁的粒度,所以gogou了解了它最常用场景的字节码文件。我们先来看看Gogou的测试用例:publicclassTestSynchronized{privateintindex;privatefinalstaticintMAX=100;publicvoidsync(){synchronized(newObject()){while(index":()V4:returnpublicvoidsync();代码:0:new#2//classjava/lang/Object3:dup4:invokespecial#1//Methodjava/lang/Object."":()V7:dup8:astore_19:monitorenter//进入同步代码块10:aload_0//加载数据11:getfield#3//Fieldindex:I14:bipush10016:if_icmpge3219:aload_020:dup21:getfield#3//Fieldindex:I24:iconst_125:iadd//添加1个操作26:putfield#3//Fieldindex:I29:goto10//跳转到10行32:aload_133:monitorexit//退出同步代码块34:goto42//跳转到第42行37:astore_2//刷新数据38:aload_139:monitorexit40:aload_241:athrow42:returnExceptiontable:fromtotargettype103437any374037anymonitorenter和monitorexit成对出现,有时候看到一个monitorenter对应多个monitorexit,但是可以肯定的是一定有在每个monitorexit之前成为monitorenter。从字节码文件中我们可以看出,aload操作是在monitorenter之后执行的,astore操作是在monitorexit之后执行的。TIPS:使用synchronized关键字时,注意锁对象不能为空;锁的范围不宜过大;不要尝试使用不同的监视器来锁定相同的方法;避免多个锁交叉等待造成死锁;锁扩展在jdk1.6之前,一个线程在获取锁时,如果锁对象已经被其他线程持有,线程就会挂起,进入阻塞状态。唤醒阻塞线程的过程涉及到用户态和内核态的切换,性能损失比较大。同步,作为亲儿子,太差了肯定不好。在jdk1.6做了优化,锁状态分为无锁状态、偏向锁、轻量级锁、重量级锁。锁的升级过程是:在了解锁的升级过程之前,gogo主要了解monitor和objectheader。刚开始学习锁扩展的时候,因为没花时间去理解这两个概念,Gogou对锁升级的记忆只维持了3天,最后Gogou又花了两天时间学习对象头和监视器。才是真正理解锁的扩展原理。所以大家在学习一门知识的时候,不要靠背去背一个知识点,一定要会。每个对象都与一个监视器相关联。监视器对象与实例对象一起创建和销毁。监视器是C++支持的监视器。对锁对象的争夺不仅仅是持有监视器的权利。狗狗在OpenJdk源码中找到了ObjectMonitor的源码://initializethemonitor,exceptionthesemaphore,allotherfields//aresimpleintegersorpointersObjectMonitor(){_header=NULL;_count=0;_waiters=0,_recursions=0;_object=NULL;_owner=NULL;_WaitSet=NULL;_WaitSetLock=0;_Responsible=NULL;_succ=NULL;_cxq=NULL;FreeNext=NULL;_EntryList=NULL;_SpinFreq=0;_SpinClock=0;OwnerIsThread=0;}protected://protectedforjvmtiRawMonitorvoid*volatile_owner;//pointertoowningthreadORBasicLockvolatileintptr_t_recursions;//recursioncount,0forfirstentryprivate:intOwnerIsThread;//_owneris(Thread*)vsSP/BasicLockObjectWaiter*volatile_cxq;//LLofrecently-arrivedthreadsblockedonentry.//ThelistisactuallycomposedofWaitNodes,acting//asproxiesforThreads.protected:ObjectWaiter*volatile_EntryList;//Threadsblockedonentryorreentry.private:Thread*volatile_succ;//Heirpresumptivethread-usedforfutilewakeupthrottlingThread*volatile_Responsible;int_PromptDrain;//rqsttodraincxqintoEntryListASAP}owner:指向线程的指针。即锁对象关联的管程中owner指向哪个线程表明该线程持有该锁对象。waitSet:进入阻塞等待的线程队列。当一个线程调用wait方法时,会进入waitset队列,等待其他线程唤醒。entryList:当多个线程进入同步代码块时,阻塞的线程会被放入entryList。那么什么是对象头,它和synchronized有什么关系呢?在JVM中,一个对象在内存中被分为三个区域:对象头MarkWord(标记域):用来存放对象的hashcode,世代年龄,锁标志位,是否可以偏向,数据存储在其中的值会在运行过程中发生变化。KlassPoint(类型指针):这个指针指向它的类元数据,JVM通过这个指针来判断这个对象是哪个类实例。指针的位长为JVM的一个字长,即32位的JVM为32位,64位的JVM为64位。实例数据用于存储类的数据信息填充数据虚拟机要求对象的起始地址必须是8字节的整数倍,不满足时需要填充。我们先用一张图来理解锁升级过程中对象头的变化:接下来我们分析锁升级过程:第一个分支锁标志为01:当线程运行到同步代码块时,会先判断lockFlag,如果lockflag为01,则继续判断biasflag。如果偏置标志为0,则表示该锁对象没有被其他线程持有,可以获取锁。此时当前线程通过CAS方法修改线程ID。如果修改成功,则锁升级为偏向锁。如果偏置标志为1,则表示该锁对象已经被占用。进一步判断threadids是否相等,说明当前线程持有的锁对象是可重入的。如果线程ID不相等,则意味着锁由另一个线程持有。需要进一步判断持有偏向锁的线程的活动状态。如果原来持有偏向锁的线程处于非活动状态或者已经退出同步代码块,则意味着原先持有偏向锁的线程可以释放偏向锁。释放后,偏向锁回到解锁状态,线程再次尝试获取锁。主要原因是偏向锁不会主动释放,只有在其他线程竞争偏向锁时才会释放。如果原来持有偏向锁的线程没有退出同步代码块,则锁升级为轻量级锁。偏向锁的流程图如下:第二个分支锁标志为00:在第一个分支中,我们了解到如果偏向锁已经被其他线程占用,则将锁升级为轻量级锁。这时在原来持有偏向锁的线程的栈帧中分配锁记录LockRecord,将对象头中的MarkWord信息复制到锁记录中。MarkWord的指针指向原来持有偏向锁的线程中的锁记录。这时,原本持有偏向锁的线程获取了轻量级锁,继续执行同步块代码。如果线程在运行同步块时发现锁标志为00,就会在当前线程的栈帧中分配一条锁记录,并将对象头中的MarkWord复制到锁记录中。通过CAS操作,MarkWord中的指针指向自己的锁记录。如果成功,当前线程将获得一个轻量级锁。如果修改失败,则进入自旋,通过CAS不断修改MarkWord中的指针指向自己的锁记录。当自旋超过一定次数(默认10次),会升级为权重锁。轻量锁的锁主动释放。持有轻量级锁的线程在执行完同步代码块后,首先会判断MarkWord中的指针是否还指向自己,锁记录中的MarkWord信息与锁对象中的MarkWord信息是否一致。如果一致,则锁释放成功。如果不是,则锁可能已升级为重量锁。轻量级流程图如下:第三个分支的lockflag为10:当lockflag为10时,锁已经是权重锁,线程会先判断monitor中的owner指针是否指向自己,如果是,则获取权重锁,否则挂起。整个锁升级过程的流程图如下。看懂了就得自己画了。总结:synchronized关键字是一种排他性的锁方式,不能被中断,保证原子性、可见性和顺序。synchronized关键字可用于修饰方法和代码块,但不能用于修饰变量和类。多个线程执行同步代码块时获取锁的过程在不同的锁状态下是不同的。偏向锁修改MarkWord中的线程ID,轻量级锁修改MarkWord中的指针指向自己的锁记录,而权重锁则是修改monitor中的指针指向自己。我今天学到了这个!今天就这样吧!并发编程、JVM、数据结构的基础知识已经更新,稍后补上!

猜你喜欢