Java并发编程_Java中的锁

ThreadLocal

ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

ThreadLocal 原理

ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值
在这里插入图片描述

ThreadLocal 内存泄露问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好**手动调用remove()**方法

Java中的锁

Lock接口


在这里插入图片描述

队列同步器

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的 方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些 模板方法将会调用使用者重写的方法。

同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放 同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法 来实现自己的同步语义。

队列同步器的实现分析

接下来将从实现角度分析同步器是如何完成线程同步的,主要包括:同步队列、独占式同 步状态获取与释放、共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据 结构与模板方法。

同步队列

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

独占式同步状态获取与释放

在获取同步状态时,同步器维 护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列 (或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步 器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
在这里插入图片描述
由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自 己的前驱是否是头节点,如果是则尝试获取同步状态。可以看到节点和节点之间在循环检查 的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点。
在这里插入图片描述

共享式同步状态获取与释放

写操作要求对资源的独占式访问,而读操作可以是共享 式访问

独占式超时获取同步状态

通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状 态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。
在这里插入图片描述

重入锁

重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对 资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定先 被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线 程最优先获取锁,也可以说锁获取是顺序的。

实现重进入:
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再 次成功获取。
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到 该锁。
如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同 步状态完全释放了,才能返回true。

公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽 然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

读写锁

而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读 线程和其他写线程均被阻塞。
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写 的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

读写锁的实现分析

接下来分析ReentrantReadWriteLock的实现,主要包括:读写状态的设计、写锁的获取与释 放、读锁的获取与释放以及锁降级
读写状态的设计:
而读 写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状 态,使得该状态的设计成为读写锁实现的关键。 如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将 变量切分成了两个部分,高16位表示读,低16位表示写
在这里插入图片描述
写锁的获取与释放:
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当 前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。
写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0 时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对 后续读写线程可见。

读锁的获取与释放:
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问 (或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如 果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程 获取,则进入等待状态。
读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的 值是(1<<16)。

锁降级

锁降级是指把持住(当前拥有的)写锁,再获取到 读锁,随后释放(先前拥有的)写锁的过程。

Condition接口

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以 实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等 待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。
在这里插入图片描述

Condition的实现分析

每个Condition对象都包含着一个队 列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。
等待队列:
同步队列和等待队列中节点类型都是同步器的静态内部类,一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点 (lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部 加入等待队列。
在这里插入图片描述
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列:
在这里插入图片描述

等待:
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释 放锁,同时线程状态变为等待状态。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同 步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列中。
在这里插入图片描述

通知:
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在 唤醒节点之前,会将节点移到同步队列中。
在这里插入图片描述
通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队 列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效 果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

猜你喜欢

转载自blog.csdn.net/qq_42148307/article/details/121400179