了解多线程并发的都比较熟悉Lock,Lock实际上就是一个接口,用户可以实现Lock接口,完成对锁的控制,也可以并发包里面的Lock锁实现类ReentrantLock 使用锁,但是大部分人都是只是停留在会使用的基础上,很少去了解Lock锁底层是怎么实现的,当需要换工作的时候,面试又是经常被问到,然而却经常说不出来,去年底我也开始面试,面试了好几家公司都被问到锁的实现原理,然而我却什么都没说得上来,只是会说比较肤浅的使用而已
lock是一个接口,而synchronized是在JVM层面实现的。synchronized释放锁有两种方式:
lock锁的释放,出现异常时必须在finally中释放锁,不然容易造成线程死锁。lock显式获取锁和释放锁,提供超时获取锁、可中断地获取锁。
synchronized是以隐式地获取和释放锁,synchronized无法中断一个正在等待获取锁的线程。
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作。
在阅读源码的成长的过程中,有很多人会遇到很多困难,一个是源码太多,另一方面是源码看不懂。在阅读源码方面,我提供一些个人的建议:
接下来进入阅读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的参数,也可以不提供该参数。提供该参数就是公平锁,不提供该参数就是非公平锁。
创建一个类HarryLock,实现Lock接口,我们手写Lock实现,以学习为目的,简单实现加锁和释放锁就可以,本文主要讨论的是互斥锁
了解多线程并发的都知道,互斥锁的特性,只有一个线程拥有锁,其他并发的线程只能等待锁释放后,才能抢锁,如果线程持有锁,可以再次获取锁,也就是可重入,因此,我们需要定义几个变量:
1.定义一个owner变量,也就是拥有锁的线程对象
2.定义一个count,用来记录获取锁的次数
3.定义一个等待队列waiter,用来存储抢锁失败的线程
代码如下:
Thread owner;
AtomicInteger count=new AtomicInteger();
LinkedBlockingDequewaiter=new LinkedBlockingDeque<>();
首先我们来写尝试获取锁,如果获取失败,直接返回,代码如下
@Override
public boolean tryLock() {
int c = count.get();
if(c>0){
//已经有线程持有锁
if(Thread.currentThread()==owner){
//当前线程,可重入锁+1
c=c+1;
count.set(c);
return true;
}
}else {
if(count.compareAndSet(c,c+1)){
//加锁成功
owner=Thread.currentThread();
return true;
}
}
return false;
}
线程在获取锁的过程中,如果获取不到锁,是会阻塞,直到获取到锁,因此,获取锁的接口实现,需要有一个死循环,去获取锁,如果获取失败,则要挂起线程,避免死循环消耗性能
@Override
public void lock() {
if(!tryLock()){
waiter.offer(Thread.currentThread());
for (;;){
//加锁不成功,自旋
Thread head = waiter.peek();
if(head!=null&&Thread.currentThread()==head){
if(!tryLock()){
//获取锁失败,挂起线程
LockSupport.park();
}else {
//获取锁成功,将当前线程从头部删除
waiter.poll();
return;
}
}else {
LockSupport.park(); //将当前线程挂起
}
}
}
}
释放锁就简单一些,只有持有锁的线程,才能释放锁,否则就抛异常,当释放锁后,要唤醒其他线程继续抢锁
@Override
public void unlock() {
int c = count.get();
if(c>0&&owner!=Thread.currentThread()){
//当前线程不是持有锁的线程,释放锁是要报错的
throw new IllegalMonitorStateException(); //抛IllegalMonitorStateException
}
count.set(c-1);
Thread peek = waiter.peek();
if(peek!=null){
LockSupport.unpark(peek);
}
c = count.get();
if(c==0){
owner=null;
}
}
好了,现在获取锁和释放锁的代码基本实现完成,接下来我们来做一个测试
package com.blockman.test;
import java.util.concurrent.locks.Lock;
public class LockTest {
static int count=0;
public static void main(String[] args) {
Lock lock=new HarryLock();
for (int x=0;x<10;x++){
new Thread(){
@Override
public void run() {
// lock.lock();
// lock.lock();
for (int y=0;y<1000;y++){
count++;
}
System.out.println(Thread.currentThread().getId()+"::"+count);
// lock.unlock();
// lock.unlock();
}
}.start();
}
}
}
首先,没有加锁时,直接运行,count结果理论上不是10000
将注释放开后,运行程序,每次结果都是10000,说明加锁和释放锁几乎没啥问题,另外如果同一个线程连续加锁多次,也是能够成功的,说明我们手写的锁也支持可重入锁。更多有关Android开发进阶技术的学习,可以点击查看《Android核心技术手册》点击可查看详细类目。