lock
是一个接口,而synchronized
是在JVM
层面实现的。synchronized
释放锁有两种方式:java
jvm
会让线程释放锁。lock
锁的释放,出现异常时必须在finally
中释放锁,否则容易形成线程死锁。lock
显式获取锁和释放锁,提供超时获取锁、可中断地获取锁。node
synchronized
是以隐式地获取和释放锁,synchronized
没法中断一个正在等待获取锁的线程。安全
synchronized
原始采用的是CPU
悲观锁机制,即线程得到的是独占锁。独占锁意味着其余线程只能依靠阻塞来等待线程释放锁。而在CPU
转换线程阻塞时会引发线程上下文切换,当有不少线程竞争锁的时候,会引发CPU
频繁的上下文切换致使效率很低。bash
Lock
用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS
操做。多线程
具体的悲观锁和乐观锁的详细介绍请参考这篇文章[]并发
在JDK5
中增长了一个Lock
接口实现类ReentrantLock
.它不只拥有和synchronized
相同的并发性和内存语义,还多了锁投票,定时锁,等候和中断锁等.它们的性能在不一样的状况下会有不一样。jvm
在资源竞争不是很激烈的状况下,synchronized
的性能要因为ReentrantLock
,可是在资源竞争很激烈的状况下,synchronized
的性能会降低得很是快,而ReentrantLock
的性能基本保持不变.ide
接下来咱们会进一步研究ReentrantLock
的源代码,会发现其中比较重要的得到锁的一个方法是compareAndSetState
。源码分析
在阅读源码的成长的过程当中,有不少人会遇到不少困难,一个是源码太多,另外一方面是源码看不懂。在阅读源码方面,我提供一些我的的建议:性能
接下来进入阅读lock的源码部分,在lock的接口中,主要的方法以下:
public interface Lock {
// 加锁
void lock();
// 尝试获取锁
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 解锁
void unlock();
}
复制代码
在lock接口的实现类中,最主要的就是ReentrantLock
,来看看ReentrantLock
中lock()
方法的源码:
// 默认构造方法,非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 构造方法,公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 加锁
public void lock() {
sync.lock();
}
复制代码
在初始化lock实例对象的时候,能够提供一个boolean的参数,也能够不提供该参数。提供该参数就是公平锁,不提供该参数就是非公平锁。
非公平锁就是不按照线程先来后到的时间顺序进行竞争锁,后到的线程也可以获取到锁,公平锁就是按照线程先来后到的顺序进行获取锁,后到的线程只能等前面的线程都获取锁完毕才执行获取锁的操做,执行有序。
咱们来看看lock()这个方法,这个有区分公平锁和非公平锁,这个二者的实现不一样,先来看看公平锁,源码以下:
// 直接调用 acquire(1)
final void lock() {
acquire(1);
}
复制代码
咱们来看看acquire(1)的源码以下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
复制代码
这里的判断条件主要作两件事:
tryAcquire(arg)
尝试的获取锁acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
就将当前的线程加入到存储等待线程的队列中。其中tryAcquire(arg)
是尝试获取锁,这个方法是公平锁的核心之一,它的源码以下:
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前线程拥有着的状态
int c = getState();
// 若为0,说明当前线程拥有着已经释放锁
if (c == 0) {
// 判断线程队列中是否有,排在前面的线程等待着锁,如果没有设置线程的状态为1。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 设置线程的拥有着为当前线程
setExclusiveOwnerThread(current);
return true;
}
// 如果当前的线程的锁的拥有者就是当前线程,可重入锁
} else if (current == getExclusiveOwnerThread()) {
// 执行状态值+1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置status的值为nextc
setState(nextc);
return true;
}
return false;
}
复制代码
在tryAcquire()
方法中,主要是作了如下几件事:
hasQueuedPredecessors()
再判断等待线程队列中,是否存在排在前面的线程。compareAndSetState(0, acquires)
设置当前的线程状态为1。setExclusiveOwnerThread(current)
current == getExclusiveOwnerThread()
判断锁的拥有者的线程,是否为当前线程,实现锁的可重入。公平锁的tryAcquire()
,实现的原理图以下:
acquireQueued()
方法,该方法是将线程加入等待的线程队列中,源码以下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 死循环处理
for (;;) {
// 获取前置线程节点
final Node p = node.predecessor();
// 这里又尝试的去获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
// 直接return interrupted
return interrupted;
}
// 在获取锁失败后,应该将线程Park(暂停)
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
复制代码
acquireQueued()
方法主要执行如下几件事:
p == head && tryAcquire(arg)
,则跳出循环,即获取锁成功。shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()
就会将线程暂停。在acquire(int arg)
方法中,最后如果条件成立,执行下面的源码:
selfInterrupt();
// 实际执行的代码为
Thread.currentThread().interrupt();
复制代码
即尝试获取锁失败,就会将锁加入等待的线程队列中,并让线程处于中断等待。公平锁lock()
方法执行的原理图以下:
有了流程图,在后面的实现本身的东西才能一步一步的进行。这也是阅读源码的必要之一。
在lock()
方法,其实在lock()方法中,已经包含了两方面:
lock()
。tryAquire()
。接下来,咱们来看一下unlock()方法的源码。
public void unlock() {
sync.release(1);
}
复制代码
直接调用release(1)
方法,来看release
方法源码以下:
public final boolean release(int arg) {
// 尝试释放当前节点
if (tryRelease(arg)) {
// 取出头节点
Node h = head;
if (h != null && h.waitStatus != 0)
// 释放锁后要即便唤醒等待的线程来获取锁
unparkSuccessor(h);
return true;
}
return false;
}
复制代码
经过调用tryRelease(arg)
,尝试释放当前节点,如果释放锁成功,就会获取的等待队列中的头节点,就会即便唤醒等待队列中的等待线程来获取锁。接下来看看tryRelease(arg)
的源码以下:
// 尝试释放锁
protected final boolean tryRelease(int releases) {
// 将当前状态值-1
int c = getState() - releases;
// 判断当前线程是不是锁的拥有者,若不是直接抛出异常,非法操做,直接一点的解释就是,你都没有拥有锁,还来释放锁,这不是骗人的嘛
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//执行释放锁操做 1.若状态值=0 2.将当前的锁的拥有者设为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 从新更新status的状态值
setState(c);
return free;
}
复制代码
总结上面的几个方法,unlock释放锁方法的执行原理图以下:
hasQueuedPredecessors()
去判断是否队列中还有等待的前置节点线程。
以下面的非公平锁,尝试获取锁nonfairTryAcquire()
源码以下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 直接就将status-1,并不会判断是否还有前置线程在等待
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
复制代码
以上就是公平锁和非公平锁的主要的核心方法的源码,接下来咱们实现本身的一个锁,首先依据前面的分析中,要实现本身的锁,拥有的锁的核心属性以下:
status
,0为未占用锁,1未占用锁,而且是线程安全的。lock
锁的核心的Api
以下:
lock
方法trylock
方法unlock
方法依据以上的核心思想来实现本身的锁,首先定义状态值status,使用的是AtomicInteger
原子变量来存放状态值,实现该状态值的并发安全和可见性。定义以下:
// 线程的状态 0表示当前没有线程占用 1表示有线程占用
AtomicInteger status =new AtomicInteger();
复制代码
接下来定义等待线程队列,使用LinkedBlockingQueue
队列来装线程,定义以下:
// 等待的线程
LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<Thread>();
复制代码
最后的属性未当前锁的拥有者,直接就用Thread
来封装,定义以下:
// 当前线程拥有者
Thread ownerThread =null;
复制代码
接下来定义lock()
方法,依据上面的源码分析,在lock
方法中主要执行的几件事以下:
@Override
public void lock() {
// TODO Auto-generated method stub
// 尝试获取锁
if (!tryLock()) {
// 获取锁失败,将锁加入等待的队列中
waitersQueue.add(Thread.currentThread());
// 死循环处理队列中的锁,不断的获取锁
for (;;) {
if (tryLock()) {
// 直到获取锁成功,将该线程从等待队列中删除
waitersQueue.poll();
// 直接返回
return;
} else {
// 获取锁不成功,就直接暂停等待。
LockSupport.park();
}
}
}
}
复制代码
而后是trylock
方法,依据上面的源码分析,在trylock
中主要执行的如下几件事:
@Override
public boolean tryLock() {
// 判断是否有现成占用
if (status.get()==0) {
// 执行状态值加1
if (status.compareAndSet(0, 1)) {
// 将当前线程设置为锁拥有者
ownerThread = Thread.currentThread();
return true;
} else if(ownerThread==Thread.currentThread()) {
// 实现锁可重入
status.set(status.get()+1);
}
}
return false;
}
复制代码
最后就是unlock方法,依据上面的源码分析,在unlock中主要执行的事情以下:
@Override
public void unlock() {
// TODO Auto-generated method stub
// 判断当前线程是不是锁拥有者
if (ownerThread!=Thread.currentThread()) {
throw new RuntimeException("非法操做");
}
// 判断状态值是否为0
if (status.decrementAndGet()==0) {
// 清空锁拥有着
ownerThread = null;
// 从等待队列中获取前置线程
Thread t = waitersQueue.peek();
if (t!=null) {
// 并当即唤醒该线程
LockSupport.unpark(t);
}
}
}
复制代码
以上就是实现本身的非公平的可重入锁,lock的源码其实并不复杂,只要认真看都能看懂,在阅读源码的过程当中,会遇到比较复杂的问题。遇到问题不要慌,网上查询资料,相信不少都能找到答案,由于java的生态如此完善,几乎90%的东西网上都会有,只要沉得住气,相信必定会有所收获。