其他部分看java全栈知识体系
1.加锁的原理
先看对象头。 在32位虚拟机
普通对象如student等等对象头有64bit.(数组对象和这个不一样,还有个属性表示数组长度)
| object header |
| Mark word 32bit | Klass Word 32位 |
Klass Word 32位指向堆中的class文件创建的类对象Class c=Class.forname();
Mark word 32bit
mark word主要用来表示对象的线程锁状态,另外还可以用来配合GC、以及存放该对象的hashCode 以64系统为例,mark word是64bit来表示:
先看锁标志位和偏向锁标记位:
最低2位,锁标志位(lock)是表示对象的线程锁状态,
其中,
正常和偏向锁时都是01,
轻量级锁用00表示,
重量级锁用10表示,
标记了GC的用11表示。
由于正常和偏向锁时都是01,因此低3位
偏向锁标记位(biased_lock) 用0或1表示是否时偏向的。
上锁原理:
从字节码看:
2.sync锁优化
有时候我们并不是都会并发或者并发量不大,而直接加重量级锁,会调用操作系统底层的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
1.轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized
实现方式,升级,解锁的过程看java全栈。
总的来说:是会在线程的栈帧中创建一个Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。如果开始上锁那么:虚拟机使用CAS操作将标记字段Mark Word拷贝到锁记录中,并且将Mark Word更新为指向Lock Record的指针。如果更新成功了,那么这个线程就拥用了该对象的锁,并且对象Mark Word的锁标志位更新为(Mark Word中最后的2bit)00,即表示此对象处于轻量级锁定状态
那么下次还是这个线程访问的话就可以直接访问。
如果这个更新操作失败,JVM会检查当前的Mark Word中是否存在指向当前线程的栈帧的指针,如果有(说明就是一个线程获取),说明该锁已经被获取,可以直接调用。如果没有(说明我是后来者,也就是现在的当前线程,已经被第一个线程抢占了),则说明该锁被其他线程抢占了,就会锁升级了。
一旦反生锁升级,如果需要解锁,那么这个锁就不解了,直接走重量级的解锁过程。因为对象头已经指向了monitor,而不是栈帧中的信息了。
2.重量级锁竞争前的自旋CAS
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。自旋一定次数后,就阻塞。但是现在都是自适应自旋锁,所以可能获得锁的几率高的会多自旋几次,反之少。
3.偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。 Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的 Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有
偏向锁是默认开启的,哪怕没有多线程,不用sync,就是new个普通类,也会开启,对象头也是101.偏向锁状态。他会延迟几秒启用。解锁后也是偏向锁
锁的优缺点对比
| 锁 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|
| 偏向锁 | 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
| 轻量级锁 | 竞争的线程不会阻塞,提高了响应速度 | 如线程始终得不到锁竞争的线程,使用自旋会消耗CPU性能 | 追求响应时间,同步块执行速度非常快 |
| 重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗 | 追求吞吐量,同步块执行速度较长 |
4.锁消除
锁消除是指虚拟机即时编译器再运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持。意思就是:JVM会判断再一段程序中的同步明显不会逃逸出去从而被其他线程访问到,那JVM就把它们当作栈上数据对待,认为这些数据是线程独有的,不需要加同步。此时就会进行锁消除。
当然在实际开发中,我们很清楚的知道哪些是线程独有的,不需要加同步锁,但是在Java API中有很多方法都是加了同步的,那么此时JVM会判断这段代码是否需要加锁。如果数据并不会逃逸,则会进行锁消除。比如如下操作:在操作String类型数据时,由于String是一个不可变类,对字符串的连接操作总是通过生成的新的String对象来进行的。因此Javac编译器会对String连接做自动优化。在JDK 1.5之前会使用StringBuffer对象的连续append()操作,在JDK 1.5及以后的版本中,会转化为StringBuidler对象的连续append()操作。
public static String test03(String s1, String s2, String s3) {
String s = s1 + s2 + s3;
return s;
}
3.ReentrantLock
默认公平锁,且可以主动放弃争取锁,这样能避免发生死锁。