对象头[每个对象都具有对象头] Mark:对象头的标记(32位),描述对象的 hash、锁信息、垃圾回收标记、年龄;内容包括:①、指向锁记录的指针;②、指向 monitor 的指针;③、GC 标记;④、偏向锁线程 ID;
一、偏向锁
偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。如果明显存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁。其特点如下:
【1】大部分情况下没有竞争的,所以可以通过偏向锁来提高性能;
【2】所谓偏向锁,就是偏心,即锁会偏向于当前已经占有的线程;
【3】将对象 Mark 的标记设置为偏向,并将线程 ID 写入对象的 Mark 头中;
【4】只要没有竞争,获得偏向锁的线程,在将来进入代码块,不需要做同步;
【5】-XX:+UseBiasedLocking -默认启动;
【6】在竞争激烈的场合,偏向锁会增加系统负担;
【代码示列】:当没有锁竞争的时候,就会默认使用偏向锁。
1 public static List<Integer> numberList =new Vector<Integer>();
2 public static void main(String[] args) throws InterruptedException {
3 long begin=System.currentTimeMillis();
4 int count=0;
5 int startnum=0;
6 while(count<10000000){
7 numberList.add(startnum);
8 startnum+=2;
9 count++;
10 }
11 long end=System.currentTimeMillis();
12 System.out.println(end-begin);
13 }
开启偏向锁:-XX:+UseBiasedLocking -XX:+BiasedLockingStartupDelay=0
关闭偏向锁:-XX:+UseBiasedLocking -XX:-BiasedLockingStartupDelay=0
【结论】:使用偏向锁,可以获得5%的性能提升。
二、轻量级锁
如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word 中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。BasicObjectLock:嵌入在线程中
【1】普通锁处理性能不够理想,轻量级锁是一种快速的锁定方法。
【2】如果对象没有锁定:①、将对象头的 Mark 指针保存到锁对象中。②、将对象头设置为指向锁的指针(在线程空间中)
1 lock->set_displaced_header(mark);
2 if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
3 TEVENT (slow_enter: release stacklock) ;
4 return ;
5 }
6 //lock位于线程中
【3】总结:① 如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁);② 在没有锁竞争的情况下,减少传统锁使用OS 互斥产生的锁性能消耗问题;③ 在竞争激烈时,轻量级锁会做很多额外操作,导致性能下降;
三、自旋锁
自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。然而,自适应自旋也没能彻底解决该问题,如果默认的自旋次数设置不合理(过高或过低),那么自适应的过程将很难收敛到合适的值:
【1】JDK1.7 中,自旋锁为内置实现。
【2】在线程竞争存在时,如果线程可以很快获得锁,那么可以不再 OS 层挂起线程,让线程做几个空操作(自旋)
【3】如果同步块很长,自旋失败,会降低系统性能。
【4】如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能。
四、偏向锁、轻量级锁、自旋锁总结
【1】不是 Java 语言层面的锁优化方法;
【2】内置于 JVM 中的获取锁的优化方法和获取锁的步骤:① 偏向锁可用时,会尝试偏向锁;② 轻量级锁可用会尝试轻量级锁;③ 以上都失败,尝试自旋锁;④ 在失败尝试普通锁,使用 OS互斥量在操作系统层面挂起;
五、如何合理使用锁机制
【1】减少锁持有时间(尽量使用 synchronized 块)
1 public synchronized void syncMethod(){
2 othercode1();
3 mutextMethod();
4 othercode2();
5 }
6 public void syncMethod2(){
7 othercode1();
8 synchronized(this){
9 mutextMethod();
10 }
11 othercode2();
12 }
【2】减少锁粒度:① 将大对象拆成小对象,大大增加并行度,降低锁竞争;② 偏向锁,轻量级锁成功率高;③ ConcurrentHashMap 中 HashMap 的同步实现;④ Collections.synchronizedMap(map<key,value,m>;⑤ 返回SynchronizedMap 对象。
1 public V get(Object key) {
2 synchronized (mutex) {return m.get(key);}
3 }
4 public V put(K key, V value) {
5 synchronized (mutex) {return m.put(key, value);}
6 }
ConcurrentHashMap JDK1.7时使用若干个 Segment:Segment<key,vlaue>[] segments;Segment(分段锁) 中维护 HashEntry<k,v>;Put 操作时,先定位到 Segment,锁定一个Segment,执行put;在减少粒度后,ConcurrentHashMap 允许若干个线程同时进入。
【3】锁分离:① 根据功能进行锁分离;② ReadWriteLock;③ 读多写少的情况下,可以提高性能;
④ 读写分离思想可以延伸,只要操作互不影响,锁就可以分离;⑤ LinkedBlockingQueue 链表阻塞队列
【4】锁粗化:通常情况下,为保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能今早的获得资源执行任务。但是,凡是有一个临近值,如果对同一个锁不停的释放获取,其本身也会消耗宝贵的资源,反而不利于性能的优化。
【5】锁消除:在编译时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作
1 public static void main(String args[]) throws InterruptedException {
2 long start = System.currentTimeMillis();
3 for (int i = 0; i < CIRCLE; i++) {
4 craeteStringBuffer("JVM", "Diagnosis");
5 }
6 long bufferCost = System.currentTimeMillis() - start;
7 System.out.println("craeteStringBuffer: " + bufferCost + " ms");
8 }
9
10 public static String craeteStringBuffer(String s1, String s2) {
11 StringBuffer sb = new StringBuffer();
12 sb.append(s1);
13 sb.append(s2);
14 return sb.toString();
15 }
【开启锁消除】:-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks ——执行时间198ms;
【关闭锁消除】:-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks ——执行时间254ms;
六、无锁
① 锁是悲观的操作;② 无锁是乐观的操作;③ 无锁的实现方式:CAS(Compare and Swap)、非阻塞同步;
CAS(V,E,N)V:表示更新的变量,E:表示预期值,N:表示新值。当V=E时,才会将V的值设为N,如果 V值和 E值不同,则说明有其他线程做了更新,则当前线程什么都不做,最后 CAS 返回 V 的真实值。
在应用层面判断多线程的干扰,如果有干扰则通知线程重试。例如:java.util.concurrent.atomic.AtomicInteger
1 public final int getAndSet(int newValue) {
2 for (;;) {
3 int current = get();
4 if (compareAndSet(current, newValue))
5 return current;
6 }
7 }
设置成功返回新值,设置失败返回旧值:public final boolean compareAndSet(int expect , int update)更新成功返回 true。java.util.concurrent.atomic 包使用无锁实现,性能高于一般的锁线程。此方法一般位于 unsafa 类中方法,保证了原子性。