网站建设收费标准不一,推广标题怎么写,衡南网站建设,青海营销网站建设服务设计同步器的意义多线程编程中#xff0c;有可能会出现多个线程同时访问同一个共享、可变资源的情况#xff0c;这个资源我们称之其为临界资源#xff1b;这种资源可能是#xff1a;对象、变量、文件等。共享#xff1a;资源可以由多个线程同时访问可变#xff1a;资源可…设计同步器的意义多线程编程中有可能会出现多个线程同时访问同一个共享、可变资源的情况这个资源我们称之其为临界资源这种资源可能是对象、变量、文件等。共享资源可以由多个线程同时访问可变资源可以在其生命周期内被修改引出的问题由于线程执行的过程是不可控的所以需要采用同步机制来协同对对象可变状态的访问如何解决线程并发安全问题实际上所有的并发模式在解决线程安全问题时采用的方案都是序列化访问临界资源。即在同一时刻只能有一个线程访问临界资源也称作同步互斥访问。Java 中提供了两种方式来实现同步互斥访问synchronized 和 Lock同步器的本质就是加锁加锁目的序列化访问临界资源即同一时刻只能有一个线程访问临界资源(同步互斥访问)不过有一点需要区别的是当多个线程执行一个方法时该方法内部的局部变量并不是临界资源因为这些局部变量是在每个线程的私有栈中因此不具有共享性不会导致线程安全问题。synchronized原理详解synchronized内置锁是一种对象锁(锁的是对象而非引用)作用粒度是对象可以用来实现对临界资源的同步互斥访问是可重入的。加锁的方式同步(非静态)实例方法锁是当前实例对象(锁的new出的对象)同步静态类方法锁是当前类对象锁的类同步代码块锁是括号里面的对象synchronized底层原理synchronized是基于JVM内置锁实现通过内部对象Monitor(监视器锁)实现基于进入与退出Monitor对象实现方法与代码块同步监视器锁的实现依赖底层操作系统的Mutex lock互斥锁实现它是一个重量级锁性能较低。当然JVM内置锁在1.5之后版本做了重大的优化如锁粗化Lock Coarsening、锁消除Lock Elimination、轻量级锁Lightweight Locking、偏向锁Biased Locking、适应性自旋Adaptive Spinning等技术来减少锁操作的开销内置锁的并发性能已经基本与Lock持平。synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。每个同步对象都有一个自己的Monitor(监视器锁)加锁过程如下图所示Monitor监视器锁 任何一个对象都有一个Monitor与之关联当且一个Monitor被持有后它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步虽然具体实现细节不一样但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。monitorenter每个对象都是一个监视器锁monitor。当monitor被占用时就会处于锁定状态线程执行monitorenter指令时尝试获取monitor的所有权过程如下如果monitor的进入数为0则该线程进入monitor然后将进入数设置为1该线程即为monitor的所有者如果线程已经占有该monitor只是重新进入则进入monitor的进入数加1如果其他线程已经占用了monitor则该线程进入阻塞状态直到monitor的进入数为0再重新尝试获取monitor的所有权monitorexit执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时monitor的进入数减1如果减1后进入数为0那线程退出monitor不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。monitorexit指令出现了两次第1次为同步正常退出释放锁第2次为发生异步退出释放锁通过上面两段描述我们应该能很清楚的看出Synchronized的实现原理Synchronized的语义底层是通过一个monitor的对象来完成其实wait/notify等方法也依赖于monitor对象这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法否则会抛出java.lang.IllegalMonitorStateException的异常的原因。看一个同步方法package it.yg.juc.sync;
public class SynchronizedMethod {public synchronized void method() {System.out.println(Hello World!);}
} 反编译结果从编译的结果来看方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成理论上其实也可以通过这两条指令来实现不过相对于普通方法其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的当方法调用时调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置如果设置了执行线程将先获取monitor获取成功之后才能执行方法体方法执行完后再释放monitor。在方法执行期间其他任何线程都无法再获得同一个monitor对象。两种同步方式本质上没有区别只是方法的同步是一种隐式的方式来实现无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现被阻塞的线程会被挂起、等待重新调度会导致“用户态和内核态”两个态之间来回切换对性能有较大影响。什么是monitor可以把它理解为 一个同步工具也可以描述为 一种同步机制它通常被 描述为一个对象。与一切皆对象一样所有的Java对象是天生的Monitor每一个Java对象都有成为Monitor的潜质因为在Java的设计中 每一个Java对象自打娘胎里出来就带了一把看不见的锁它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁MarkWord锁标识位为10其中指针指向的是Monitor对象的起始地址。在Java虚拟机HotSpot中Monitor是由ObjectMonitor实现的其主要数据结构如下位于HotSpot虚拟机源码ObjectMonitor.hpp文件C实现的ObjectMonitor() {_header NULL;_count 0; // 记录个数_waiters 0,_recursions 0;_object NULL;_owner NULL;_WaitSet NULL; // 处于wait状态的线程会被加入到_WaitSet_WaitSetLock 0 ;_Responsible NULL ;_succ NULL ;_cxq NULL ;FreeNext NULL ;_EntryList NULL ; // 处于等待锁block状态的线程会被加入到该列表_SpinFreq 0 ;_SpinClock 0 ;OwnerIsThread 0 ;}ObjectMonitor中有两个队列_WaitSet 和 _EntryList用来保存ObjectWaiter对象列表 每个等待锁的线程都会被封装成ObjectWaiter对象 _owner指向持有ObjectMonitor对象的线程当多个线程同时访问一段同步代码时首先会进入 _EntryList 集合当线程获取到对象的monitor后进入 _Owner区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1若线程调用 wait() 方法将释放当前持有的monitorowner变量恢复为nullcount自减1同时该线程进入 WaitSet集合中等待被唤醒若当前线程执行完毕也将释放monitor锁并复位count的值以便其他线程进入获取monitor(锁)同时Monitor对象存在于每个Java对象的对象头Mark Word中存储的指针的指向Synchronized锁便是通过这种方式获取锁的也是为什么Java中任意对象可以作为锁的原因同时notify/notifyAll/wait等方法会使用到Monitor锁对象所以必须在同步代码块中使用。监视器Monitor有两种同步方式互斥与协作。多线程环境下线程之间如果需要共享数据需要解决互斥访问数据的问题监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。那么有个问题来了我们知道synchronized加锁加在对象上对象是如何记录锁状态的呢答案是锁状态是被记录在每个对象的对象头Mark Word中下面我们一起认识一下对象的内存布局对象的内存布局HotSpot虚拟机中对象在内存中存储的布局可以分为三块区域对象头Header、实例数据Instance Data和对齐填充Padding。对象头比如 hash码对象所属的年代对象锁锁状态标志偏向锁线程ID偏向时间数组长度数组对象等。Java对象头一般占有2个机器码在32位虚拟机中1个机器码等于4字节也就是32bit在64位虚拟机中1个机器码是8个字节也就是64bit但是 如果对象是数组类型则需要3个机器码因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小但是无法从数组的元数据来确认数组的大小所以用一块来记录数组长度。实例数据存放类的属性数据信息包括父类的属性信息对齐填充由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的仅仅是为了字节对齐对象头HotSpot虚拟机的对象头包括两部分信息第一部分是“Mark Word”用于存储对象自身的运行时数据 如哈希码HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等它是实现轻量级锁和偏向锁的关键。这部分数据的长度在32位和64位的虚拟机暂 不考虑开启压缩指针的场景中分别为32个和64个Bits官方称它为“Mark Word”。对象需要存储的运行时数据很多其实已经超出了32、64位Bitmap结构所能记录的限度但是对象头信息是与对象自身定义的数据无关的额 外存储成本考虑到虚拟机的空间效率Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机 中对象未被锁定的状态下Mark Word的32个Bits空间中的25Bits用于存储对象哈希码HashCode4Bits用于存储对象分代年龄2Bits用于存储锁标志位1Bit固定为0在其他状态轻量级锁定、重量级锁定、GC标记、可偏向下对象的存储内容如下表所示。但是如果对象是数组类型则需要三个机器码因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小但是无法从数组的元数据来确认数组的大小所以用一块来记录数组长度。对象头信息是与对象自身定义的数据无关的额外存储成本但是考虑到虚拟机的空间效率Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据它会根据对象的状态复用自己的存储空间也就是说Mark Word会随着程序的运行发生变化。变化状态如下32位虚拟机锁状态25bit4bit1bit2bit23bit2bit是否偏向锁是否禁用偏向锁标志位无锁态对象的hashCode分代年龄001轻量级锁指向栈中锁记录的指针00重量级锁指向Monitor的指针10GC标记空11偏向锁线程IDEpoch分代年龄10164位虚拟机现在我们虚拟机基本是64位的而64位的对象头有点浪费空间,JVM默认会开启指针压缩所以基本上也是按32位的形式记录对象头的。手动设置-XX:UseCompressedOops哪些信息会被压缩对象的全局静态变量(即类属性)对象头信息64位平台下原生对象头大小为16字节压缩后为12字节对象的引用类型64位平台下引用类型本身大小为8字节压缩后为4字节对象数组类型64位平台下数组类型本身大小为24字节压缩后16字节 在Scott oaks写的《java性能权威指南》第八章8.22节提到了当heap size堆内存大于32GB是用不了压缩指针的对象引用会额外占用20%左右的堆空间也就意味着要38GB的内存才相当于开启了指针压缩的32GB堆空间。这是为什么呢看下面引用中的红字来自openjdk wikihttps://wiki.openjdk.java.net/display/HotSpot/CompressedOops。32bit最大寻址空间是4GB开启了压缩指针之后呢一个地址寻址不再是1byte而是8byte因为不管是32bit的机器还是64bit的机器java对象都是8byte对齐的而类是java中的基本单位对应的堆内存中都是一个一个的对象。Compressed oops represent managed pointers (in many but not all places in the JVM) as 32-bit values which must be scaled by a factor of 8 and added to a 64-bit base address to find the object they refer to. This allows applications to address up to four billion objects (not bytes), or a heap size of up to about 32Gb. At the same time, data structure compactness is competitive with ILP32 mode.对象头分析工具运行时对象头锁状态分析工具JOL他是OpenJDK开源工具包引入下方maven依赖dependencygroupIdorg.openjdk.jol/groupIdartifactIdjol-core/artifactIdversion0.10/version
/dependency打印markwordSystem.out.println(ClassLayout.parseInstance(object).toPrintable());
object为我们的锁对象此处打印的结果需要取第一部分来判断锁类型因为涉及到大端和小端模式可自行了解锁的膨胀升级过程锁的状态总共有四种无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争锁可以从偏向锁升级到轻量级锁再升级的重量级锁但是锁的升级是单向的也就是说只能从低到高升级不会出现锁的降级。从JDK 1.6 中默认是开启偏向锁和轻量级锁的可以通过-XX:-UseBiasedLocking来禁用偏向锁。下图为锁的升级全过程偏向锁偏向锁是Java 6之后加入的新锁它是一种针对加锁操作的优化手段经过研究发现在大多数情况下锁不仅不存在多线程竞争而且总是由同一线程多次获得因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是如果一个线程获得了锁那么锁就进入偏向模式此时Mark Word 的结构也变为偏向锁结构当这个线程再次请求锁时无需再做任何同步操作即获取锁的过程这样就省去了大量有关锁申请的操作从而也就提供程序的性能。所以对于没有锁竞争的场合偏向锁有很好的优化效果毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合偏向锁就失效了因为这样场合极有可能每次申请锁的线程都是不相同的因此这种场合下不应该使用偏向锁否则会得不偿失需要注意的是偏向锁失败后并不会立即膨胀为重量级锁而是先升级为轻量级锁。下面我们接着了解轻量级锁。默认开启偏向锁开启偏向锁-XX:UseBiasedLocking -XX:BiasedLockingStartupDelay0
关闭偏向锁-XX:-UseBiasedLocking轻量级锁倘若偏向锁失败虚拟机并不会立即升级为重量级锁它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁在整个同步周期内都不存在竞争”注意这是经验数据。需要了解的是轻量级锁所适应的场景是线程交替执行同步块的场合如果存在同一时间访问同一锁的场合就会导致轻量级锁膨胀为重量级锁。自旋锁轻量级锁失败后虚拟机为了避免线程真实地在操作系统层面挂起还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下线程持有锁的时间都不会太长如果直接挂起操作系统层面的线程可能会得不偿失毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态这个状态之间的转换需要相对比较长的时间时间成本相对较高因此自旋锁会假设在不久将来当前的线程可以获得锁因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因)一般不会太久可能是50个循环或100循环在经过若干次循环后如果得到锁就顺利进入临界区。如果还不能获得锁那就会将线程在操作系统层面挂起这就是自旋锁的优化方式这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。锁消除消除锁是虚拟机另外一种锁的优化这种优化更彻底Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译又称即时编译)通过对运行上下文的扫描去除不可能存在共享资源竞争的锁通过这种方式消除没有必要的锁可以节省毫无意义的请求锁时间如下StringBuffer的append是一个同步方法但是在add方法中的StringBuffer属于一个局部变量并且不会被其他线程所使用因此StringBuffer不可能存在共享资源竞争的情景JVM会自动将其锁消除。锁消除的依据是逃逸分析的数据支持。锁消除前提是java必须运行在server模式server模式会比client模式作更多的优化同时必须开启逃逸分析-XX:DoEscapeAnalysis 开启逃逸分析
-XX:EliminateLocks 表示开启锁消除。逃逸分析使用逃逸分析编译器可以对代码做如下优化一、同步省略。如果一个对象被发现只能从一个线程被访问到那么对于这个对象的操作可以不考虑同步。二、将堆分配转化为栈分配。如果一个对象在子程序中被分配要使指向该对象的指针永远不会逃逸对象可能是栈分配的候选而不是堆分配。三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到那么对象的部分或全部可以不存储在内存而是存储在CPU寄存器中。是不是所有的对象和数组都会在堆内存分配空间不一定在Java代码运行时通过JVM参数可指定是否开启逃逸分析 -XX:DoEscapeAnalysis 表示开启逃逸分析 -XX:-DoEscapeAnalysis 表示关闭逃逸分析。从jdk 1.7开始已经默认开启逃逸分析如需关闭需要指定-XX:-DoEscapeAnalysispublic class T0_ObjectStackAlloc {/*** 进行两种测试* 关闭逃逸分析同时调大堆空间避免堆内GC的发生如果有GC信息将会被打印出来* VM运行参数-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:PrintGCDetails -XX:HeapDumpOnOutOfMemoryError** 开启逃逸分析* VM运行参数-Xmx4G -Xms4G -XX:DoEscapeAnalysis -XX:PrintGCDetails -XX:HeapDumpOnOutOfMemoryError** 执行main方法后* jps 查看进程* jmap -histo 进程ID**/public static void main(String[] args) {long start System.currentTimeMillis();for (int i 0; i 500000; i) {alloc();}long end System.currentTimeMillis();//查看执行时间System.out.println(cost-time (end - start) ms);try {Thread.sleep(100000);} catch (InterruptedException e1) {e1.printStackTrace();}}private static TulingStudent alloc() {//Jit对编译时会对代码进行 逃逸分析//并不是所有对象存放在堆区有的一部分存在线程栈空间TulingStudent student new TulingStudent();return student;}static class TulingStudent {private String name;private int age;}
}