锁,是计算机系统中非常常见的技术,实现线程对资源的独占,防止对资源的并发读写造成错误,本文通过从线程,线程状态,到java提供的锁基础,基础的复盘一下线程和锁
线程
计算机系统中,经常听到线程和进程的概念。
进程:一个被编译好的程序,被系统加载到内存中,开始运行时,就产生了一个该程序的进程。在进程结束前,该程序将占有内存的一部分空间,作为程序运行空间和环境。进程是一种静态的概念,指程序运行时所占有的一些计算机资源,内存 网络等资源;
线程:产生程序进程的过程中,进程都会产生一条主线程,java一般是main方法。线程主要是对计算机cpu计算的占用,计算机cpu在多个线程之间做切换,来处理线程中的程序指令。线程是一种动态的概念,是指程序对系统cpu计算功能的占用,与进程能较长时间稳定存在与内存中不同,线程的创建销毁比较频繁,程序用户的一个个请求,将会在服务器上产生很多的进程(一般情况下,都会有线程池,线程也会被复用,减少新建线程的开销)。
jvm的线程
Thread thread = new Thread();
线程的状态
public enum State { 从上到下值 从 0 到 5NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;}
线程状态流转
状态waiting(等待) 和 blocked(阻塞)的区别
两者都表示线程当前暂停执行的状态,而两者的区别,基本可以理解为:进入 waiting 状态是线程主动的,而进入 blocked 状态是被动的。
更进一步的说,进入 blocked 状态是在同步(synchronized)代码之外,而进入 waiting 状态是在同步代码之内(然后马上退出同步)。
wait 和 notify 只能在 同步代码块中执行 synchronized
阻塞 blocked 和 waiting 的区别, ed 和 ing 时态,代表 blocked是被动进入,waiting是主动进入
blocked 是在抢 synchronized 同步锁失败之后的(同步代码块之外),一种被迫的状态,没抢到锁,blocked 线程让出cpu资源,但是后续如果synchronized 锁被其他线程释放,则被jvm唤醒直接进入running状态,waiting 是在 调用object.wait之后进入的状态(同步代码块之中),是线程自己调用object.wait方法,是自己进入的状态,自己被定住了,则需要别的线程调用 object.notify 解锁,(主动进入,被别的线程唤醒),线程调用object.wait方法,会释放 synchronized 锁,也必须释放,不然别的线程不能进入同步代码块中解锁会造成死锁(造成我自己锁我自己,我自己又锁别人,而我解锁我自己需要别人帮忙,别人又被我锁外面了造成死锁)
-
为什么 waiting 和 notify notifyAll 只能在 synchronized 中使用,为什么总是让 waiting 和 notify 原子操作
因为jvm 希望 waiting 和 notify 要保持原子性,防止出现死锁的情况//有序性 -》 指令重排,代码乱序执行; synchronized语义就要求线程在访问读写共享变量时只能“串行”执行 volatile synchronized //原子性 -》 一个线程对一段代码的执行,要么不执行,要么全部执行完毕 synchronized //可见性 -》 多个线程操作同一快内存对象,各线程的工作内存中该值可能不同 volatile synchronized
// synchronized: 具有原子性,有序性和可见性;
// volatile:具有有序性和可见性
// AQS.lock:具有原子性,有序性和可见性;
线程中断
thread.interrupt();
// 具体来说,当对一个线程,调用 interrupt() 时,
// ① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。
// ② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。
//
// interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
// 也就是说,一个线程如果有被中断的需求,那么就可以这样做。
// ① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
// ② 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。
重入锁 ReentrantLock
//since 1.5
//java.util.concurrent.locks.ReentrantLock
ReentrantLock lock = new ReentrantLock();
ReentrantLock ,是jdk 1.5提供的一种更加灵活的锁机制,是AQS的一种实现
(AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。),相比synchronized,他提供了更多功能扩展,场景引用更多样;
她的特性,主要有如下几点
- 排他性,锁要实现对一个资源的独占
- 等待,要实现未拿到锁线程的存储排队,并让线程等待
- 可重入,以拿到拿到锁钥匙的线程,再次进入时不等待直接进入,防止死锁
- 释放,使用完毕后释放资源,允许其他线程进入
- 等待超时
我们通过源码来了解ReentrantLock 如何实现如上的特性
ReentrantLock.NonfairSync 和ReentrantLock.FairSync的父类 AQS 实例有几个关键的成员变量
- Thread exclusiveOwnerThread; 独占此锁的线程对象(这个是实现重入的关键)
- int state; state是标志位,为0是锁空闲,1为锁被占;lock时调用, compareAndSetState(0, 1) ,如上确定当前线程是获取锁还是去排队
- Node head; Node tail; AQS 提供的链表用来存储 等待中的线程
ReentrantLock 的锁实现也是基于如上AQS的几个关键 成员变量实现
ReentrantLock 内部实现了两种锁 公平锁和非公平锁,默认使用非公平锁
public ReentrantLock() {sync = new ReentrantLock.NonfairSync();}public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
lock.lock(),实现对代码块的加锁,我们通过lock的调用,分析其如何实现锁的功能
// NonfairSync 非公平锁lock的实现final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}// 如果锁空闲, 将state 设置为 1,将当前线程设置为 exclusiveOwnerThread 独裁者线程// 如果锁占用, 调用 acquire 等待获取,无法抢占,其他线程必须去排队// acquire 是 AQS 的实现public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg))selfInterrupt();}}// tryAcquire AQS 的实现是 直接抛出异常,可见虽然AQS提供了队列排队的机制,但是其默认的处理是让没抢到锁的线程直接抛异常protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();} //ReentrantLock.NonfairSync.tryAcquire //ReentrantLock 要实现等待机制 肯定不能直接抛异常//NonfairSync 重写了 tryAcquire,将没抢到lock的线程加入队列中protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();// 贼心不死,临近队列之前 再询问锁是否空闲,c==0则自己不用等待 直接占有锁 线程不等待int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {// 可重入锁的关键,虽然锁被占,但占锁的 有没有可能是我自己,如果是我自己,当然直接进入,不等待int nextc = c + acquires;// 当前 state +1 重入也是有限制的 不断的重入加一 nextc <0 其实是 state 大于int最大值之后出现了,这么大的重入情况,一定是当前线程代码有bug了,没有解锁导致的// ReentrantLock 在此加判断 防止一个线程不断重入导致死锁if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;} //书接上文 AQS !tryAcquire(arg) &&acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg)//上文讲了 tryAcquire 主要是再次判断是否有锁和最大加锁判断//acquireQueued 则是让线程进入等待 和 进入链表的 关键方法// addWaiter 将thread 封装成 node 节点,可以看到 将新的线程 加到了链表的尾部 (进入链表)private AbstractQueuedSynchronizer.Node addWaiter(AbstractQueuedSynchronizer.Node mode) {AbstractQueuedSynchronizer.Node node = new AbstractQueuedSynchronizer.Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureAbstractQueuedSynchronizer.Node pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;}// 加入链表,再次判断是否当前锁是否空闲final boolean acquireQueued(final AbstractQueuedSynchronizer.Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final AbstractQueuedSynchronizer.Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}//多重的询问锁是否空闲之后,还是没抢到,线程就进入的等待的队列,此时 要让此线程等待,释放CPU计算资源selfInterrupt();static void selfInterrupt() {Thread.currentThread().interrupt();}
如上就是lock.lock()的执行流程,我们最终了lock的执行联调, 排他性和等待,可重入 已经实现
lock.unlock(),解锁
public void unlock() {sync.release(1);}//解锁是 调用tryRelease 校验public final boolean release(int arg) {if (tryRelease(arg)) {AbstractQueuedSynchronizer.Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}// 解锁线程 == 占锁线程 可以执行解锁操作,否则报错//1 -》 如果此锁没被重入过 那就是 一lock 一 unlock c = 0,故为0时直接将独占线程设为了null 修改state为0 返回true 准备唤醒下一个等待的线程//2 -》 如果锁有重入 则 c > 0,故本次调 lock 只是将 state -1 ,无法释放锁,待 state == 0 才能解锁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;}// tryRelease 返回 true,代表 修改state为0,独占线程设为了null,锁已经释放,但是,等待着的线程 还需要唤醒,即要处理链表里的线程// 调用 unparkSuccessor// 取出链表的下一个节点// 链表的下一个节点 为null,表明解锁时,没有线程正在排队,// *// 也有一种可能,就是已经有线程进入// 这里要说一下 ReentrantLock 默认非公平锁,新线程是会判断state 如果为0 直接插队,占有锁的,不会一个人跑到链表尾部,所以// 因为有插队的存在,所有 下一个节点 不一定 waitStatus 为 0,可能早在你执行tryRelease 代码时,有人插队拿锁了// 排除以上的情况,则可以 LockSupport.unpark(s.thread); 唤醒下一个节点中的线程,让他从阻塞 变为 运行态private void unparkSuccessor(AbstractQueuedSynchronizer.Node node) {AbstractQueuedSynchronizer.Node s = node.next;if (s == null || s.waitStatus > 0) {s = null;for (AbstractQueuedSynchronizer.Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)//唤醒线程LockSupport.unpark(s.thread);}
如上 unlock 先释放了锁,然后去唤醒链表的下一个节点线程,让别人继续执行。
等待超时
ReentrantLock 类的 lock 是没法设置超时的,即如果拿不到锁 就进入队列排队,不会因为太久拿不到锁,抛出异常。
可以用ReentrantLock 如下两个api来代替,防止线程拿锁太久,导致无响应
- tryLock(),拿锁,拿到就直接占有锁,拿不到就返回false,不排队,让线程去做别的操作
- tryLock(long timeout, TimeUnit unit) ,拿锁,拿不到就等待一定时间 还是拿不到就返回false,让线程去做别的操作
如上也能实现超时处理,为了避免一直拿不到锁 导致的线程等待太久问题,可以使用 tryLock 方法
总结,一套api下来,ReentrantLock 基本实现了 我们所需要锁的几大特性。