Java中锁的分类
- 悲观锁,就是悲观思想,认为写操作多,遇到的并发可能高。不管是否发生多线程冲突,每次去读写数据的时候,都会上锁,导致锁之间的争夺,挂起、唤醒等开销。Synchronized就是悲观锁
- 乐观锁,就是乐观思想,认为读多写少,遇到并发情况小。每次去读数据的时候都不会上锁,但是在更新数据的时候,先读取数据,然后加锁(如果和上次读的数据时一样,则更新),否则失败,重复读-比较-写操作。比如:偏向锁、轻量级锁(CAS轮询)、自旋锁。
在多线程的加锁机制中,JVM会首先尝试乐观锁,失败后才调用悲观锁。
对象的内存分布
在HotSpot虚拟机中,对象在内存中的存储布局分为三部分:对象头、实例数据、对其填充。
对象头(Mark Word)
对象自身的运行时数据
如:哈希吗(HashCode)、GC分代年龄(Generational GC Age)等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,简称“Mark Word”如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2Word存储对象头。
指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
对象头信息是与对象自身定义的数据无关的额外存储成本。它会根据对象的状态复用自己的存储空间。例如:在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32bit空间中的25bit用于存储对象哈希吗(HashCode),4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0
锁的升级与对比
Java SE 1.6种锁一共有四种状态,级别从低到高:无锁状态、偏向锁、轻量级锁、重量级锁。随着竞争的情况逐渐升级,但不能降级。
- 偏向锁
顾名思义,就是偏向于当前已经占有锁的线程。如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
-XX:+UseBiasedLocking
- 自旋锁
自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。
使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,往往毅然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了被挂起的操作 ,反而浪费了系统的资源。
在JDK1.6中,Java虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。
在JDK1.7开始,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁,自旋锁总是会执行,自旋锁次数也由虚拟机自动调整。
- 轻量级锁
- 重量级锁
锁优化
减少锁持有时间
例如:对一个方法加锁,不如对方法中需要同步的几行代码加锁;减小锁粒度
ConcurrentHashMap采取对segment加锁而不是整个map加锁,提高并发性;锁分离
根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提高了并发性。锁粗化
这看起来与思路1有冲突,其实不然。思路1是针对一个线程中只有个别地方需要同步,所以把锁加在同步的语句上而不是更大的范围,减少线程持有锁的时间;锁消除
锁消除是编译器做的事:根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程(即不会影响线程空间外的数据),那么可以认为这段代码是线程安全的,不必要加锁。
参考文献
1. https://www.cnblogs.com/charlesblc/p/5994162.html
2. https://www.jianshu.com/p/78cf35f01b2f
3. http://www.cnblogs.com/think-in-java/p/5520462.html