JVM——锁

 

对象头[每个对象都具有对象头] 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 类中方法,保证了原子性。

posted @ 2020-11-18 18:02  Java程序员进阶  阅读(71)  评论(0编辑  收藏  举报