对象头[每个对象都具有对象头] 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架构师必看的立场,转载请标明来源出处