Lock锁原理解析【Android进阶】
Lock简介
Lock接口位于J.U.C下locks包内,其定义了Lock应该具备的方法。
Lock 方法签名:
- void lock():获取锁(不死不休,拿不到就一直等)
- boolean tryLock():获取锁(浅尝辄止,拿不到就算了)
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException:获取锁(过时不候,在一定时间内拿不到锁,就算了)
- void lockInterruptibly() throws InterruptedException:获取锁(任人摆布,xxx)
- void unlock():释放锁
- Condition newCondition():获得Condition对象
synchronized和lock的区别:
- synchronized是java关键字,是用c++实现的;而lock是用java类,用java可以实现
- synchronized可以锁住代码块,对象和类,但是线程从开始获取锁之后开发者不能进行控制和了解;lock则用起来非常灵活,提供了许多api可以让开发者去控制加锁和释放锁等等。
写个Demo
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException { lock.lock();//其他没拿到锁的卡住不动 Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("start to get lock Interruptibly");
lock.unlock(); //看看会发生什么,注释掉再看看
lock.lock();
System.out.println("拿到锁");
lock.unlock();
System.out.println("释放锁");
}
});
thread.start(); Thread.sleep(3000);
lock.unlock();
}
我们自己来手写一下lock接口的tryLock()、lock()和unLock()方法,实现我们自己的myLock。
public class MyLock implements Lock {
//多并发调用 0-未占用 大于0-占用
AtomicInteger state = new AtomicInteger(); Thread ownerThread = new Thread(); //等待锁的队列
LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue(); @Override
public void lock() {
if (!tryLock()) { //先抢锁,所以是非公平锁
//没拿到锁,放到队列中去进行排队
waiters.add(Thread.currentThread());
//等待被唤醒
for (; ; ) {
if (tryLock()) { //非公平锁情况下,唤醒过来继续获取锁
waiters.poll(); //获取锁成功把自己从队列中取出来
return;
} else //获取锁失败
LockSupport.park(); //线程阻塞
}
}
} @Override
public boolean tryLock() {
if (state.get() == 0) { //如果锁没被占用
if (state.compareAndSet(0, 1)) { //如果成功拿到锁
ownerThread = Thread.currentThread(); //占用锁线程改为当前线程
return true;
}
}
return false;
} @Override
public void unlock() { if (ownerThread != Thread.currentThread()) //占用锁线程不是当前线程无法释放锁
throw new RuntimeException("非法调用,当前锁不属于你"); if (state.decrementAndGet() == 0) //如果成功释放锁
ownerThread = null; //占用锁线程置空
//通知其他线程
// Thread thread = null;
//
// while ((thread = waiters.peek()) != null)
// LockSupport.unpark(thread);
Thread thread = waiters.peek(); //获取队列头部线程,线程还留在队列中
if (thread != null) {
LockSupport.unpark(thread); //取消阻塞
}
} @Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
} @Override
public Condition newCondition() {
return null;
} @Override
public void lockInterruptibly() throws InterruptedException { }
}
几个注意点:
- 锁的占用状态state是AtomicInteger类型,底层原理是CAS,这是为了保证在多并发情况下线程安全问题;
- 当线程1释放锁成功时,获取队列头部线程但并不取出,因为非公平锁模式下,队列头部线程不一定能获取到锁;
- LockSupport的park()和unPark()方法是native方法,可以阻塞,唤醒线程;
Lock默认是非公平锁,上面实现的也是非公平锁,小伙伴们可以试一试。
公平锁和非公平锁区别:
先等待先获取锁是公平锁;先等待也不一定先获取锁,可能被突然到来的线程获取到是非公平锁;
公平锁的实现:
@Override
public void lock() {
checkQueue();//线程来的时候先不获取锁,而是先检查队列中有没有等待的线程,如果有,直接放入队列,如果没有,再去获取锁
if (!tryLock()) { //先抢锁,所以是非公平锁
//没拿到锁,放到队列中去进行排队
waiters.add(Thread.currentThread());
//等待被唤醒
for (; ; ) {
if (tryLock()) { //非公平锁情况下,唤醒过来继续获取锁
waiters.poll(); //获取锁成功把自己从队列中取出来
return;
} else //获取锁失败
LockSupport.park(); //线程阻塞
}
}
}
lock源码
在阅读源码的成长的过程中,有很多人会遇到很多困难,一个是源码太多,另一方面是源码看不懂。在阅读源码方面,我提供一些个人的建议:
- 第一个是抓主舍次,看源码的时候,很多人会发现源码太长太多,看不下去,这就要求我们抓住哪些是核心的方法,哪些是次要的方法。当舍去次要方法,就会发现代码精简和很多,会大大提高我们阅读源码的信心。
- 第二个是不要死扣,有人看源码会一行一行的死扣,当看到某一行看不懂,就一直停在那里死扣,知道看懂为止,其实很多时候,虽然看不懂代码,但是可以从变量名和方法名知道该代码的作用,java中都是见名知意的。
接下来进入阅读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的参数,也可以不提供该参数。提供该参数就是公平锁,不提供该参数就是非公平锁。
以上就是Android开发中的Lock的解析,更多有关乐观锁,显示锁,内置锁的使用或者Android核心技术可以参考《Android核心技术手册》点击查看获取!
总结
- lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
- lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
- lock释放锁的过程:修改状态值,调整等待链表。