java锁的知识点小记:
一.什么是线程安全:
标准定义:多个线程访问一个对象,如果不用考虑这些线程的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调,调用这个对象的行为都能得到正确结果,则称线程安全。
如上定义为侠义的线程安全,实际上,线程安全又分为五种强度,从强到弱以此为“不可变”,“绝对线程安全”,"相对线程安全",“线程兼容”和“线程对立”。
1.不可变:不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的安全保障措施。
2.绝对线程安全:“不管运行环境如何,调用者都不要任何额外的同步措施”通常需要付出很大代价。Java API中标明线程安全的类,大多数都不是绝对线程安全。
3.相对线程安全:就是我们通常所说的线程安全,要保证对这个对象单独操作是线程安全的,我们在调用时候不需要做额外的保障措施。但对一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段保证调用的正确性。
4.线程兼容:线程兼容是指对象本身并不是线程安全的,但可以通过在调用端正确使用同步手段来保证对象在并发环境中可以安全的使用。
5.线程对立:指无论调用端是否采取了同步措施,都无法在并发环境中使用的代码。
二.锁的类型:
宏观上说锁分为悲观锁(阻塞同步)和乐观锁(非阻塞同步)
乐观锁(非阻塞同步):及一种乐观思想,先进行操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据有争用,产生了冲突,就再采取其他的补偿措施。
乐观锁需要操作和冲突检测这两部具有原子性,只能靠硬件完成,硬件保证一个从语义上需要多个操作步骤的行为只用一个指令就可以完成,这包括:
1.测试并设置
2.获取并增加
3.交换
4.比较并交换(compareAndSwap CAS)
5.加载链接/条件存储
java的乐观锁主要靠CAS来完成,cas需要三个操作数,分别为操作v操作变量的内存地址,A旧的预期值,B新值。当V所存储的值为预期值A时,则更新新值,上述比较和交换操作是原子的。
java的CAS操作是通过sun.misc.Unsafe类实现的,java.util.concurrent包中的原子类都是建立在CAS的基础上的。如AtomicInteger的getAndIncrement()操作。
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
unsafe的getAndAddInt函数则是判段cas操作是否成功,如不成功,则一直CAS
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
compareAndSwapInt包含四个参数,var1指向对象的引用,var2操作变量在此对象中的地址偏移量,var5期望值,var5+var4目标值
悲观锁(互斥同步):总认为不去做正确的同步措施,就会出现问题,无论共享数据是否存在竞争,都会进行加锁,java中最基本的互斥同步手段就是synchronized关键字。
三.synchronize底层实现原理
java1.5之前都是使用synchronize实现,synchronize关键字可用来修饰方法,对象或者静态方法。
1.1synchronize方法
synchronize方法同步是隐式的,及无需通过字节码指令来控制,它实现在方法调用和返回操作中。jvm可以从常量池方法表的中的ACC_SYNCHRONIZED的访问标志确定一个方法时候为同步方法。方法调用是调用指令会检查该标识,如果为同步方法则执行线程将先持有monitor,再去执行方法。
1.2synchronize代码块
synchronize关键字进行编译后,会在同步代码块前后形成monitorenter和monitorexit指令,该指令后面跟着一个reference类型的参数指明要锁定或解锁的对象。在执行monitorenter指令时,会去获取对象的锁,如果对象没有被锁定,或者当前线程已经拥有了那个对象的锁,则锁的计数器+1.执行monitorexit指令时锁的计数器-1.如果获取锁失败,则当前线程就要进入阻塞状态。
1.3monitor(监视器锁/管程)的实现方式。
重量级锁是通过monitor实现的,锁的标志位为10,对象头中的指针指向monitor对象的起始地址。在java虚拟机中,monitor是由ObjectMonitor实现。位于\openjdk-jdk8u-jdk8u\openjdk-jdk8u-jdk8u\hotspot\src\share\vm\runtime包中。
ObjectMonitor() {
_header = NULL;//对象头
_count = 0;//锁计数器
_waiters = 0,//等待线程数
_recursions = 0;//重入次数
_object = NULL;
_owner = NULL;//指向获得ObjectMonitor对象的线程
_WaitSet = NULL;//处于wait状态的线程,会被加入waitset
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//处于等待锁block状态的线程,会被加入entryset
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
当多个线程访问同一个代码块时,首先会进入_EntryList集合,当对象获得到线程的monitor后,进入owner区域并将owner变量设置为当前线程同事count+1.若调用wait方法,释放当前持有的monitor,owner变量恢复为null,count-1.同时当前线程进入waitset等待被唤醒。若当前线程执行完毕也将释放monitor并复位变量的值。
四.jvm对锁的优化
4.1偏向锁
是jdk1.6后引入的一项优化,目的是消除数据在无竞争情况下的同步同步原语,偏向所会偏向第一个获得他的线程,如果之后该锁没有被其他线程获取,则偏向线程不需要再同步。
当虚拟机启用偏向锁,那么当锁对象第一次被虚拟机获取时,会将mark word中的标志位设置为01,同时使用cas操作更新markword中的threadID。
当另外一个线程尝试获取这个锁时,偏向模式宣告结束,根据锁对象是否被锁定来判断,撤销偏向恢复为未锁定(标志位为01)或升级为轻量级锁
4.2轻量级锁
轻量级锁是相对于传统锁使用操作系统互斥量而言的,目的是为了减少传统锁使用操作系统互斥量的性能消耗。
轻量级锁在代码进入同步块时,如果此同步对象没有被锁定(标志位为01),当前线程会在栈帧中创建一个名为锁记录的空间(lock record),用于存储当前对象的mark word拷贝。然后虚拟机CAS将mark word更新为指向lock record的指针。如果更新操作成功了,则这个线程就拥有了该对象的锁。并且将mark word锁标志位改为“00”,即表示此对象处于轻量级锁定状态。
如果更新操作失败了,虚拟机首先会检查对象的Mark word是否指向当前线程的栈帧,如果只说明当前线程已拥有这个对象的锁,那就可直接进入同步块执行,否则说明这个锁已经被抢占,轻量级锁需要膨胀为重量级锁,mark word中的标志位改为10.
轻量级锁解解锁过程也是通过CAS完成的,如果对象的mark word仍指向线程的锁记录,就用cas操作将对象当前mark word与栈帧中赋值的mark word替换过来,如果替换失败,说明有其他线程尝试获取该锁,就要在释放锁的同时唤醒被挂起的线程。
五.java锁升级的原理:
未完待续。。。
网友评论