JVM——锁

2021-05-14 16:15:41 浏览数 (1)

对象头[每个对象都具有对象头] Mark:对象头的标记(32位),描述对象的 hash、锁信息、垃圾回收标记、年龄;内容包括:①、指向锁记录的指针;②、指向 monitor 的指针;③、GC 标记;④、偏向锁线程 ID;

一、偏向锁


偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。如果明显存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁。其特点如下: 【1】大部分情况下没有竞争的,所以可以通过偏向锁来提高性能; 【2】所谓偏向锁,就是偏心,即锁会偏向于当前已经占有的线程; 【3】将对象 Mark 的标记设置为偏向,并将线程 ID 写入对象的 Mark 头中; 【4】只要没有竞争,获得偏向锁的线程,在将来进入代码块,不需要做同步; 【5】-XX: UseBiasedLocking -默认启动; 【6】在竞争激烈的场合,偏向锁会增加系统负担;

【代码示列】:当没有锁竞争的时候,就会默认使用偏向锁。

代码语言:javascript复制
public static List<Integer> numberList =new Vector<Integer>();
public static void main(String[] args) throws InterruptedException {
	long begin=System.currentTimeMillis();
	int count=0;
	int startnum=0;
	while(count<10000000){
		numberList.add(startnum);
		startnum =2;
		count  ;
	}
	long end=System.currentTimeMillis();
	System.out.println(end-begin);
}

开启偏向锁:-XX: UseBiasedLocking -XX: BiasedLockingStartupDelay=0 关闭偏向锁:-XX: UseBiasedLocking -XX:-BiasedLockingStartupDelay=0 【结论使用偏向锁,可以获得5%的性能提升。

、轻量级锁


如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word 中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。BasicObjectLock:嵌入在线程中

 【1】普通锁处理性能不够理想,轻量级锁是一种快速的锁定方法。

【2】如果对象没有锁定:①、将对象头的 Mark 指针保存到锁对象中。②、将对象头设置为指向锁的指针(在线程空间中)

代码语言:javascript复制
lock->set_displaced_header(mark);
 if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
	  TEVENT (slow_enter: release stacklock) ;
	  return ;
 }
//lock位于线程中 

【3】总结:① 如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁);② 在没有锁竞争的情况下,减少传统锁使用OS 互斥产生的锁性能消耗问题;③ 在竞争激烈时,轻量级锁会做很多额外操作,导致性能下降;

三、自旋锁


自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。然而,自适应自旋也没能彻底解决该问题,如果默认的自旋次数设置不合理(过高或过低),那么自适应的过程将很难收敛到合适的值: 【1】JDK1.7 中,自旋锁为内置实现。 【2】在线程竞争存在时,如果线程可以很快获得锁,那么可以不再 OS 层挂起线程,让线程做几个空操作(自旋) 【3】如果同步块很长,自旋失败,会降低系统性能。 【4】如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能。

四、偏向锁、轻量级锁、自旋锁总结


【1】不是 Java 语言层面的锁优化方法; 【2】内置于 JVM 中的获取锁的优化方法和获取锁的步骤:① 偏向锁可用时,会尝试偏向锁;② 轻量级锁可用会尝试轻量级锁;③ 以上都失败,尝试自旋锁;④ 在失败尝试普通锁,使用 OS互斥量在操作系统层面挂起;

五、如何合理使用锁机制


【1】减少锁持有时间(尽量使用 synchronized 块)

代码语言:javascript复制
public synchronized void syncMethod(){
	othercode1();
	mutextMethod();
	othercode2();
}
public void syncMethod2(){
	othercode1();
	synchronized(this){
		mutextMethod();
	}
	othercode2();
}

【2】减少锁粒度:① 将大对象拆成小对象,大大增加并行度,降低锁竞争;② 偏向锁,轻量级锁成功率高;③ ConcurrentHashMap 中 HashMap 的同步实现;④ Collections.synchronizedMap(map<key,value,m>;⑤ 返回SynchronizedMap 对象。

代码语言:javascript复制
public V get(Object key) {
	synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
	synchronized (mutex) {return m.put(key, value);}
}

ConcurrentHashMap JDK1.7时使用若干个 Segment:Segment<key,vlaue>[] segments;Segment(分段锁) 中维护 HashEntry<k,v>;Put 操作时,先定位到 Segment,锁定一个Segment,执行put;在减少粒度后,ConcurrentHashMap 允许若干个线程同时进入。 【3】锁分离:① 根据功能进行锁分离;② ReadWriteLock;③ 读多写少的情况下,可以提高性能;

④ 读写分离思想可以延伸,只要操作互不影响,锁就可以分离;⑤ LinkedBlockingQueue 链表阻塞队列

【4】锁粗化:通常情况下,为保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能今早的获得资源执行任务。但是,凡是有一个临近值,如果对同一个锁不停的释放获取,其本身也会消耗宝贵的资源,反而不利于性能的优化。

【5】锁消除:在编译时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作

代码语言:javascript复制
public static void main(String args[]) throws InterruptedException {
	long start = System.currentTimeMillis();
	for (int i = 0; i < CIRCLE; i  ) {
		craeteStringBuffer("JVM", "Diagnosis");
	}
	long bufferCost = System.currentTimeMillis() - start;
	System.out.println("craeteStringBuffer: "   bufferCost   " ms");
}

public static String craeteStringBuffer(String s1, String s2) {
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	return sb.toString();
}

开启锁消除】:-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

代码语言:javascript复制
public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();
        if (compareAndSet(current, newValue))
            return current;
    }
}

设置成功返回新值,设置失败返回旧值:public final boolean compareAndSet(int expect , int update)更新成功返回 true。java.util.concurrent.atomic 包使用无锁实现,性能高于一般的锁线程。此方法一般位于 unsafa 类中方法,保证了原子性。

本文来源程序猿进阶,由javajgs_com转载发布,观点不代表Java架构师必看的立场,转载请标明来源出处

0 人点赞