Fork me on GitHub

线程

1.1什么是线程?
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
1.2与进程的关系
每个进程都有多个相应的线程,在执行程序时,实际上是执行相应的一系列线程。进程是资源分配的最小单位,线程是程序执行的最小单位。
2.1线程的生命周期:
新建状态(New):
当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞--线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞--通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
2.2图解:
2.3Java多线程的创建及启动
①继承Thread类,重写该类的run()方法。
代码示例:
如上所示,继承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。
②实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
代码示例:
③使用Callable和Future接口创建线程。具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
2.4sleep()、wait()、join()、yield()方法的区别:
2.5线程安全
2.5.1线程不安全产生原因:
2.5.2保证线程安全的方式:
2.5.2.1什么是线程安全?
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为获取正确的结果(单线程下的结果和多线程下获得的保持一致),那么就称这个类是线程安全的。
2.5.2.2怎么才能做到线程安全?
1.基于JVM的锁
  无法解决分布式情况的问题
2.基于数据库的锁(分布式)
  耗费资源
3.基于redis的锁(分布式)
  可能会出现死锁,推荐使用redission
4.基于zookeeper的锁(分布式)
最优级
 
2.5.2.3避免并发的方式:
2.5.2.3.1线程封闭
什么是线程封闭?
就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题。
实现线程封闭有哪些方法?
①ad-hoc 线程封闭
这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现。
Ad-hoc 线程封闭非常脆弱,应该尽量避免使用。
 
②栈封闭
栈封闭是我们编程当中遇到的最多的线程封闭。
什么是栈封闭呢?
简单的说就是局部变量。
多个线程访问一个方法,此方法中的局部变量都会被拷贝一份到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。
 
2.5.2.3.2无状态的类
没有任何成员变量的类,就叫无状态的类,这种类一定是线程安全的。
无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象.不能保存数据,是不变类。
如果这个类的方法参数中使用了对象,也是线程安全的吗?比如:
当然也是,为何?因为多线程下的使用,固然 user 这个对象的实例会不正常,但是对于 StatelessClass 这个类的对象实例来说,它并不持有 UserVo 的对象实例,它自己并不会有问题,有问题的是 UserVo 这个类,而非 StatelessClass 本身。
 
2.5.2.3.3让类不可变
①加final关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
②根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值。
但是要注意,一旦类的成员变量中有对象,上述的 final 关键字保证不可变并不能保证类的安全性,为何?因为在多线程下,虽然对象的引用不可变,但是对象在堆上的实例是有可能被多个线程同时修改的,没有正确处理的情况下,对象实例在堆中的数据是不可预知的。这就牵涉到了如何安全的发布对象这个问题。
 
2.5.2.3.4加锁和CAS
①synchronized
synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保数据的完整性,

如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,会将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存。

②Lock
Lock是在Java1.5被引入进来的,Lock的引入让锁有了可操作性,我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性。

Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。

测试:
结果:
进入方法我们首先要获取到锁,然后去执行我们业务代码,这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。
其实在Lock还有几种获取锁的方式,这里再说一种,就是tryLock()这个方法跟Lock()是有区别的,Lock在获取锁的时候,如果拿不到锁,就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁,直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。同时还可以设置等待时间。
 
③在cas

compare and swap也就是比较和交换,他是一条CPU的并发原语。

他在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。

Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。

在CAS的基础上实现了一些原子类,如AtomicInteger

 cas的缺点:CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。

ABA问题:如下图:

可以引入版本号的方式,来解决ABA的问题。Java中提供了一个类在CAS时,针对各个版本追加版本号的操作。 AtomicStampeReference,代码示例:

自旋时间过长问题:

  • 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)
  • 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。
2.5.2.3.5安全的发布
类中持有的成员变量,如果是基本类型,发布出去,并没有关系,因为发布出去的其实是这个变量的一个副本.
但是如果类中持有的成员变量是对象的引用,如果这个成员对象不是线程安全的,通过 get 等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。
 
2.5.2.3.6ThreadLocal
ThreadLocal 是实现线程封闭的最好方法。
ThreadLocal 内部维护了一个 Map,Map 的 key 是每个线程的名称,而 Map 的值就是我们要封闭的对象。每个线程中的对象都对应着 Map 中一个值,也就是 ThreadLocal 利用 Map 实现了对象的线程封闭。
注意:为了方式threalocal内存泄漏问题,在使用完threadlocal后应即时调用remove()方法移除entry
 
2.5.2.3.7volatile
  • volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中
  • volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量

其实加了volatile就是告知CPU,对当前属性的读写操作,不允许使用CPU缓存,加了volatile修饰的属性,会在转为汇编之后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:

  • 将当前处理器缓存行的数据立即写回到主内存
  • 这个写回的数据,在其他的CPU内核的缓存中,直接无效,要求必须重新从主内存中拉取。

Lock手动锁也是基于volatile实现的,因为本质也是在进行加锁和释放锁时去对一个被volatile修改是变量进行加减操作。

并不能保证类的线程安全性,只能保证类的可见性,最适合一个线程写,多个线程读的情景。

volatile如何实现的禁止指令重排?

内存屏障概念。将内存屏障看成一条指令。会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。

 

2.5.2.3.8final

final修饰的属性,在运行期间是不允许修改的,这样一来,就间接的保证了可见性,所有多线程读取final属性,值肯定是一样。并不是同其他锁机制一样,从主内存重新读取从而达到可见性的目的

深入synchronized

类锁、对象锁

synchronized的使用一般就是同步方法和同步代码块。

synchronized的锁是基于对象实现的。

如果使用同步方法

  • static:此时使用的是当前类.class作为锁(类锁)

  • 非static:此时使用的是当前对象做为锁(对象锁)

synchronized的优化

JDK团队就在JDK1.6中,对synchronized做了大量的优化

锁消除:在synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,你即便写了synchronized,他也不会触发。

public synchronized void method(){
  // 没有操作临界资源
  // 此时这个方法的synchronized你可以认为木有~~
}

锁膨胀:如果在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。

 

锁升级:ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized性能比较差。

synchronized就在JDK1.6做了锁升级的优化

  • 无锁、匿名偏向:当前对象没有作为锁存在。

  • 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程 。

    • 如果是,直接拿着锁资源走。

    • 如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)

  • 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁

    • 如果成功获取到,拿着锁资源走

    • 如果自旋了一定次数,没拿到锁资源,锁升级。

  • 重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。(用户态&内核态)

synchronized实现原理

synchronized是基于对象实现的。对象的锁信息都是在MarkWord中的

展开MarkWork信息如下;

 锁默认情况下,开启了偏向锁延迟。

如果没开启偏向锁或者设置偏向锁延迟开启,就会出现无锁状态(00);如果正常开启偏向锁了,那么不会出现无锁状态,对象会直接变为匿名偏向(01)

偏向锁在升级为轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点(STW),才可以做偏向锁撤销,在明知道有并发情况,就可以选择不开启偏向锁,或者是设置偏向锁延迟开启

因为JVM在启动时,需要加载大量的.class文件到内存中,这个操作会涉及到synchronized的使用,为了避免出现偏向锁撤销操作,JVM启动初期,有一个延迟4s开启偏向锁的操作

整个锁升级状态的转变:

重量锁底层ObjectMonitor

深入ReentrantLock

3.1 ReentrantLock和synchronized的区别

核心区别:

  • ReentrantLock是个类,synchronized是关键字,当然都是在JVM层面实现互斥锁的方式

效率区别:

  • 如果竞争比较激烈,推荐ReentrantLock去实现,不存在锁升级概念。而synchronized是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的。

底层实现区别:

  • 实现原理是不一样,ReentrantLock基于AQS实现的,synchronized是基于ObjectMonitor

功能向的区别:

  • ReentrantLock的功能比synchronized更全面。

    • ReentrantLock支持公平锁和非公平锁

    • ReentrantLock可以指定等待锁资源的时间

加锁流程:

公平锁与非公平锁:

公平锁是指多个线程按照请求锁的顺序获取锁,即先到先得的原则。在公平锁中,如果有多个线程等待获取锁,那么锁会依次分配给等待时间最长的线程,这样可以避免线程饥饿的情况。

公平锁的实现比较复杂,需要维护一个双向线程等待队列因此性能会比较低

非公平锁是指多个线程按照竞争获取锁的顺序获取锁,即先到不一定先得的原则。在非公平锁中,如果有多个线程等待获取锁,那么锁可能会直接分配给等待时间较短的线程,这样可能会导致一些线程一直无法获取锁,出现线程饥饿的情况。

非公平锁的实现比较简单,不需要维护一个线程等待队列,因此性能会比较高

ReentrantLock中实现公平与非公平锁的lock源码:

 1 // 非公平锁
 2 final void lock() {
 3     // 上来就先基于CAS的方式,尝试将state从0改为1
 4     if (compareAndSetState(0, 1))
 5         // 获取锁资源成功,会将当前线程设置到exclusiveOwnerThread属性,代表是当前线程持有着锁资源
 6         setExclusiveOwnerThread(Thread.currentThread());
 7     else
 8         // 执行acquire,尝试获取锁资源
 9         acquire(1);
10 }
11 // 公平锁
12 final void lock() {
13     //  执行acquire,尝试获取锁资源
14     acquire(1);
15 }

acquire方法,公平锁和非公平锁的逻辑一样,源码如下:

 1 public final void acquire(int arg) {
 2     // tryAcquire:当前线程是否可以尝试获取锁资源
 3     if (!tryAcquire(arg) &&
 4         // 没有拿到锁资源
 5         // addWaiter(Node.EXCLUSIVE):将当前线程封装为Node节点,插入到AQS的双向链表的结尾
 6         // acquireQueued:查看我是否是第一个排队的节点,如果是可以再次尝试获取锁资源,如果长时间拿不到,挂起线程
 7         // 如果不是第一个排队的额节点,就尝试挂起线程即可
 8         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
 9         // 中断线程的操作
10         selfInterrupt();
11 }

tryAcquire方法是竞争锁资源的逻辑,分为公平锁和非公平锁,具体的实现为:非公平锁实现nonfairTryAcquire、公平锁实现tryAcquire

 1 // 非公平锁实现
 2 final boolean nonfairTryAcquire(int acquires) {
 3     // 获取当前线程
 4     final Thread current = Thread.currentThread();
 5     // 获取了state属性
 6     int c = getState();
 7     // 判断state当前是否为0,之前持有锁的线程释放了锁资源
 8     if (c == 0) {
 9         // 再次抢一波锁资源
10         if (compareAndSetState(0, acquires)) {
11        //拿锁成功,将当前线程设置到属性exclusiveOwnerThread中
12             setExclusiveOwnerThread(current);
13             //拿锁成功返回true
14             return true;
15         }
16     }
17     // 不是0,代表有线程持有着锁资源,判断持有锁的线程是否是当前线程,如果是,证明是锁重入操作
18     else if (current == getExclusiveOwnerThread()) {
19         // 将state + 1
20         int nextc = c + acquires;
21         if (nextc < 0) // 说明对重入次数+1后,超过了int正数的取值范围
22             // 01111111 11111111 11111111 11111111
23             // 10000000 00000000 00000000 00000000
24             // 说明重入的次数超过界限了。
25             throw new Error("Maximum lock count exceeded");
26         // 正常的将计算结果,复制给state
27         setState(nextc);
28         // 锁重入成功
29         return true;
30     }
31     // 返回false
32     return false;
33 }
 1 // 公平锁实现
 2    protected final boolean tryAcquire(int acquires) {
 3        // 获取当前线程
 4        final Thread current = Thread.currentThread();
 5        // 获取state属性
 6        int c = getState();
 7        if (c == 0) {
 8            // 查看AQS中是否有排队的Node,没人排队抢一手 。有人排队,如果我是第一个,也抢一手
10            if (!hasQueuedPredecessors() &&
11                // 抢一手
12                compareAndSetState(0, acquires)) {
13                setExclusiveOwnerThread(current);
14                return true;
15            }
16        }
17        // 锁重入(当前线程等于持有锁的线程)
18        else if (current == getExclusiveOwnerThread()) {
19            int nextc = c + acquires;
20            if (nextc < 0)
21                throw new Error("Maximum lock count exceeded");
22            setState(nextc);
23            return true;
24        }
25        return false;
26    }
 1 public final boolean hasQueuedPredecessors() {
 2   //头尾节点
 3     Node t = tail; 
 4     Node h = head;
 5   //s为头结点的next节点
 6     Node s;
 7   //如果头尾节点相等,证明没有线程排队,直接去抢占锁资源
 8     return h != t &&
 9     //// s节点不为null,并且s节点的线程为当前线程(排在第一名的是不是我)
10         ((s = h.next) == null || s.thread != Thread.currentThread());
11 }

在非公平锁实现中,当线程没有获取到锁资源时,会将当前线程放入到一个双向队列中,具体的实现方法是addWaiter

 1 // 没有拿到锁资源,过来排队,  mode:代表互斥锁
 2    private Node addWaiter(Node mode) {
 3        // 将当前线程封装为Node,
 4        Node node = new Node(Thread.currentThread(), mode);
 5        // 拿到尾结点
 6        Node pred = tail;
 7        // 如果尾结点不为null
 8        if (pred != null) {
 9            // 1.当前节点的prev指向尾结点
10            node.prev = pred;
11            // 2.以CAS的方式,将当前线程设置为tail节点
12            if (compareAndSetTail(pred, node)) {
13                // 3.将之前的尾结点的next指向当前节点
14                pred.next = node;
15                return node;
16            }
17        }
18        // 如果CAS失败,以死循环的方式,保证当前线程的Node一定可以放到AQS队列的末尾
19        enq(node);
20        return node;
21    }
22 
23    private Node enq(final Node node) {
24        for (;;) {
25            // 拿到尾结点
26            Node t = tail;
27            // 如果尾结点为空,AQS中一个节点都没有,构建一个伪节点,作为head和tail
28            if (t == null) { 
29                if (compareAndSetHead(new Node()))
30                    tail = head;
31            } else {
32                // 比较熟悉了,以CAS的方式,在AQS中有节点后,插入到AQS队列的末尾
33                node.prev = t;
34                if (compareAndSetTail(t, node)) {
35                    t.next = node;
36                    return t;
37                }
38            }
39        }
40    }

当线程没有拿到锁资源后,并且到AQS排队了之后触发的方法acquireQueued

// 当前没有拿到锁资源后,并且到AQS排队了之后触发的方法。  中断操作这里不用考虑
final boolean acquireQueued(final Node node, int arg) {
    // 不考虑中断
    // failed:获取锁资源是否失败(这里简单掌握落地,真正触发的,还是tryLock和lockInterruptibly)
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 死循环…………
        for (;;) {
            // 拿到当前节点的前继节点
            final Node p = node.predecessor();
            // 前继节点是否是head,如果是head,再次执行tryAcquire尝试获取锁资源。
            if (p == head && tryAcquire(arg)) {
                // 获取锁资源成功
                // 设置头结点为当前获取锁资源成功Node,并且取消thread信息
                setHead(node);
                // help GC
                p.next = null; 
                // 获取锁失败标识为false
                failed = false;
                return interrupted;
            }
            // 没拿到锁资源……
            // shouldParkAfterFailedAcquire:基于上一个节点转改来判断当前节点是否能够挂起线程,如果可以返回true,
            // 如果不能,就返回false,继续下次循环
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 这里基于Unsafe类的park方法,将当前线程挂起
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            // 在lock方法中,基本不会执行。
            cancelAcquire(node);
    }
}
// 获取锁资源成功后,先执行setHead
private void setHead(Node node) {
    // 当前节点作为头结点  伪
    head = node;
    // 头结点不需要线程信息
    node.thread = null;
    node.prev = null;
}

// 当前Node没有拿到锁资源,或者没有资格竞争锁资源,看一下能否挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // -1,SIGNAL状态:代表当前节点的后继节点,可以挂起线程,后续我会唤醒我的后继节点
    // 1,CANCELLED状态:代表当前节点以及取消了
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 上一个节点为-1之后,当前节点才可以安心的挂起线程
        return true;
    if (ws > 0) {
        // 如果当前节点的上一个节点是取消状态,我需要往前找到一个状态不为1的Node,作为他的next节点
        // 找到状态不为1的节点后,设置一下next和prev
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 上一个节点的状态不是1或者-1,那就代表节点状态正常,将上一个节点的状态改为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

释放锁流程:

释放锁流程概述

 释放锁源码:

public void unlock() {
    // 释放锁资源不分为公平锁和非公平锁,都是一个sync对象
    sync.release(1);
}

// 释放锁的核心流程
public final boolean release(int arg) {
    // 核心释放锁资源的操作之一
    if (tryRelease(arg)) {
        // 如果锁已经释放掉了,走这个逻辑
        Node h = head;
        // h不为null,说明有排队的
        // 如果h的状态不为0(为-1),说明后面有排队的Node,并且线程已经挂起了。
        if (h != null && h.waitStatus != 0)
            // 唤醒排队的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}
// ReentrantLock释放锁资源操作
protected final boolean tryRelease(int releases) {
    // 拿到state - 1(并没有赋值给state)
    int c = getState() - releases;
    // 判断当前持有锁的线程是否是当前线程,如果不是,直接抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // free,代表当前锁资源是否释放干净了。
    boolean free = false;
    if (c == 0) {
        // 如果state - 1后的值为0,代表释放干净了。
        free = true;
        // 将持有锁的线程置位null
        setExclusiveOwnerThread(null);
    }
    // 将c设置给state
    setState(c);
    // 锁资源释放干净返回true,否则返回false
    return free;
}

// 唤醒后面排队的Node
private void unparkSuccessor(Node node) {
    // 拿到头节点状态
    int ws = node.waitStatus;
    if (ws < 0)
        // 先基于CAS,将节点状态从-1,改为0
        compareAndSetWaitStatus(node, ws, 0);
    // 拿到头节点的后续节点。
    Node s = node.next;
    // 如果后续节点为null或者后续节点的状态为1,代表节点取消了。
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 如果后续节点为null,或者后续节点状态为取消状态,从后往前找到一个有效节点环境
        for (Node t = tail; t != null && t != node; t = t.prev)
            // 从后往前找到状态小于等于0的节点
            // 找到离head最新的有效节点,并赋值给s
            if (t.waitStatus <= 0)
                s = t;
    }
    // 只要找到了这个需要被唤醒的节点,执行unpark唤醒
    if (s != null)
        LockSupport.unpark(s.thread);
}

深入ReentrantReadWriteLock

为什么要出现读写锁

synchronized和ReentrantLock都是互斥锁。

如果说有一个操作是读多写少的,还要保证线程安全的话。如果采用上述的两种互斥锁,效率方面很定是很低的。

在这种情况下,咱们就可以使用ReentrantReadWriteLock读写锁去实现。

读读之间是不互斥的,可以读和读操作并发执行。

但是如果涉及到了写操作(读写、写写),那么还得是互斥的操作。

读写锁的实现原理

ReentrantReadWriteLock还是基于AQS实现的,还是对state进行操作,拿到锁资源就去干活,如果没有拿到,依然去AQS队列中排队。

读锁操作:基于state的高16位进行操作。

写锁操作:基于state的低16为进行操作。

ReentrantReadWriteLock依然是可重入锁。

写锁重入:读写锁中的写锁的重入方式,基本和ReentrantLock一致,没有什么区别,依然是对state进行+1操作即可,只要确认持有锁资源的线程,是当前写锁线程即可。只不过之前ReentrantLock的重入次数是state的正数取值范围,但是读写锁中写锁范围就变小了。

读锁重入:因为读锁是共享锁。读锁在获取锁资源操作时,是要对state的高16位进行 + 1操作。因为读锁是共享锁,所以同一时间会有多个读线程持有读锁资源。这样一来,多个读操作在持有读锁时,无法确认每个线程读锁重入的次数。为了去记录读锁重入的次数,每个读操作的线程,都会有一个ThreadLocal记录锁重入的次数

写锁的饥饿问题:读锁是共享锁,当有线程持有读锁资源时,再来一个线程想要获取读锁,直接对state修改即可。在读锁资源先被占用后,来了一个写锁资源,此时,大量的需要获取读锁的线程来请求锁资源,如果可以绕过写锁,直接拿资源,会造成写锁长时间无法获取到写锁资源。

读锁在拿到锁资源后,如果再有读线程需要获取读锁资源,需要去AQS队列排队。如果队列的前面需要写锁资源的线程,那么后续读线程是无法拿到锁资源的。持有读锁的线程,只会让写锁线程之前的读线程拿到锁资源

写锁加锁流程概述:

读锁加锁流程概述:

 

posted @ 2021-07-12 09:57  来一杯面向对象的茶  阅读(98)  评论(0编辑  收藏  举报
1