前言一、synchronized
1.1对象头1.2同步方法1.3同步代码块 二、ReentrantLock
2.1ReentrantLock概述2.2ReentrantLock执行流程 三、synchronized和Lock的区别总结
前言
如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我们需要对线程进行同步,那么synchronized和ReentrantLock就是实现线程同步的两个重要内容,可以说在并发控制中是必不可少的部分。本章就来介绍synchronized和ReentrantLock的底层原理。
一、synchronized 1.1对象头
在理解synchronized实现原理之前先了解一下Java的对象头和Monitor,在JVM中,对象是分成三部分存在的:对象头、实例数据、对齐填充。
对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。
关键:synchronized 基于进入和退出监视器对象(monitor)来实现方法同步和代码块同步,每个对象都存在着一个monitor与之关联,monitor对象存在于每个Java对象的对象头中(存储的指针的指向)
1.2同步方法
使用 ACC_SYNCHRonIZED 这标志告诉JVM这是一个同步方法
当方法调用时,调用指令将会检查方法的ACC_SYNCHRonIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(monitorenter指令,锁的计数器加1),然后再执行方法,退出方法时释放monitor(monitorexit指令,计数器-1)
反编译后的同步方法字节码文件
1.3同步代码块
同步块是由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果获取到锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。
反编译后的同步代码块字节码文件
常情况下只会执行第一个monitorexit释放锁,然后返回。而如果在执行中发生了异常,第二个monitorexit就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。
二、ReentrantLock 2.1ReentrantLock概述
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义
ReentrantLock 主要利用 CAS+AQS(抽象的队列式的同步器) 来实现,默认是非公平锁,也可以指定为公平锁。
public ReentrantLock() {
sync = new NonfairSync(); //默认,非公平
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync(); //根据参数创建
}
AQS是用CLH队列锁(CLH同步队列是一个 FIFO双向队列,AQS依赖它来完成同步状态的管理)
用 volatile 修饰共享变量 state,线程通过 CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
2.2ReentrantLock执行流程
ReentrantLock先通过CAS尝试获取锁
1.如果此时锁没有被占用,通过CAS获得锁。
2.如果此时锁已经被占用,该线程加入AQS队列并wait()
3.锁被释放的时候,挂在队首的线程就会被notify(),然后继续CAS尝试获取锁,此时:
非公平锁:如果有其他线程尝试lock(),有可能被其他刚好申请锁的线程抢占。
公平锁:只有在队列头的线程才可以获取锁,新来的线程只能插入到队尾。
lock()
如果成功通过CAS修改了state,指定当前线程为该锁的独占线程,标志自己成功获取锁。如果CAS失败的话,调用acquire();
final void lock() { //非公平锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
final void lock() { //公平锁
acquire(1);
}
acquire():
首先,调用tryAcquire(),会尝试再次通过CAS修改state为1,如果失败而且发现锁是被当前线程占用的,就执行重入(state++);
如果锁是被其他线程占有,那么当前线程执行tryAcquire返回失败,并且执行addWaiter()进入等待队列,并挂起自己interrupt()。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
unlock()
释放时候,state- -,通过state==0判断锁是否完全被释放。成功释放锁的话,唤起一个被挂起的线程
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒被挂起的线程
unparkSuccessor(h);
return true;
}
return false;
}
//尝试释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
总结:
1.每一个ReentrantLock自身维护一个AQS队列记录申请锁的线程信息。
2.通过大量CAS保证多个线程竞争锁的时候的并发安全。
3.可重入的功能是通过维护state变量来记录重入次数实现的。
4.公平锁需要维护队列,通过AQS队列的先后顺序获取锁,缺点是会造成大量线程上下文切换。
5.非公平锁可以直接抢占,所以效率更高。
三、synchronized和Lock的区别
1.来源及用法
synchronized:JAVA的一个内置关键字,托管给JVM执行;在需要同步的对象中加入此控制,可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象,加锁解锁的过程是隐式的。
Lock :是一个接口,是JAVA写的控制锁的代码,一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出
2.异常是否释放锁
synchronized:发生异常时候会自动释放占有的锁,因此不会出现死锁
Lock:发生异常时不会主动释放占有的锁,必须手动unlock来释放锁,所以一般会在finally块中写unlock()以防死锁。
3.锁特点
synchronized:可重入、不可中断、非公平。
Lock:可重入、等待时可中断、可判断、可公平可非公平。
4.是否阻塞
synchronized:获取不到锁只能一直阻塞,假设A线程获得锁,B线程等待,如果A线程阻塞,B线程会一直等待下去。
Lock:lock有多个锁获取的方法,可以尝试获得锁,如果尝试获取不到锁,线程可以不用一直等待就结束。
5.锁性能
synchronized:Java1.5中是性能低效的,因为这是一个重量级操作。Java1.6进行了很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等。
Lock:可以提高多个线程进行读写操作的效率。(可以通过readwritelock实现读写分离)
如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
总结
synchronized和ReentrantLock的实现原理与区别是经常会被问到的知识点,都是并发编程中不可或缺的一部分。synchronized 基于进入和退出监视器对象(monitor)来实现方法同步和代码块同步,理解这一点非常关键。同时在ReentrantLock实现原理中再一次提到了CAS的概念,其重要性不言而喻。理解完两者的底层原理,回头去看Lock和synchronized的区别又会有一个新的认识,并且能够记忆深刻。



