并发编程之synchronized锁(一)
一、设计同步器的意义
多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。
共享:资源可以由多个线程同时访问
可变:资源可以在其生命周期内被修改
引出的问题:由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问那么我们怎么解决线程并发安全问题?实际上,所有的并发模式在解决线程安全问题时,采用的方案都是 序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock同步器的本质就是加锁加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
注意,锁保证了线程执行的有序性,但是一个线程拿到锁执行代码,并不能保证这段代码的有序性执行,即可能会发生指令重排;
二、synchronized锁原理解析
synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。加锁的方式:
1、同步实例方法,锁是当前实例对象
2、同步类方法,锁是当前类对象
3、同步代码块,锁是括号里面的对象
三、synchronized底层原理
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,java中每个对象都有一个内置对象Monitor(监视器锁)。基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(LockCoarsening)、锁消除(Lock Elimination)、轻量级锁(LightweightLocking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与Lock持平。
synchronized关键字被编译成字节码后会被翻译成monitorenter 和monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。
既然每个对象都有一个内置对象Monitor,我们如何看到这内置对象Monitor,以及这个对象锁都有哪些内容呢,这个我们就需要查看jvm源码,
查看jdk源码 \openjdk\hotspot\src\share\vm\runtime 在这个目录下找到objectMonitor.hpp(c++编写)查看jdk原代码
ObjectMonitor() { _header = NULL; //对象头 _count = 0+1+1+1-1-1-1; //记录加锁次数,锁重入时用到 _waiters = 0, //当前有多少处于wait状态的thread _recursions = 0; _object = NULL; _owner = 0; //指向持有ObjectMonitor对象的线程 _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ;//处于等待加锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
我们这个每个对象都有内置对象Monitor,那这个内置对象存在我们对象的哪里呢,这就需要了解在java里面我们的对象的内存划分,看下图:
认识对象的内存结构:
1、对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等
2、对象实际数据:即创建对象时,对象中成员变量,方法等
3、对齐填充:对象的大小必须是8字节的整数倍
可见,和锁相关的信息比如锁的次数,锁的状态都在都对象头里面存储,另外MetaData(元数据指针)存的是指向类对象信息,所以每个对象getClass可以获取类对象。
jvm对象整体加锁过程:
四、逃逸分析
面试问题:实例对象内存中存储在哪
答:如果实例对象存储在堆区时:实例对象内存存在堆区,实例的引用存在栈上,实例的元数据class存在方法区或者元空间
Object实例对象一定是存在堆区的吗
不一定,如果实例对象没有线程逃逸行为
使用逃逸分析,编译器可以对代码做如下优化:
一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
是不是所有的对象和数组都会在堆内存分配空间?
不一定
在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,-XX:+DoEscapeAnalysis : 表示开启逃逸分析 -XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis。
总结一句话:java1.7默认开启了逃逸分析,在开启逃逸分析的情况下,如果一个对象只能够在某一个线程被访问到而不会被其他线程访问,那么这个对象是有可能存在栈内存的,所以不是所有的的对象都在堆内存。