java多线程并发(一)
1. java线程创建方式
1.1 继承 Thread 类
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。 启动线程的唯一方法就是通过 Thread 类的 start()实例方法
public static void main(String[] args) {
class MyThread extends Thread{
@Override
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread = new MyThread();
myThread.start();
}
1.2 实现 Runnable 接口
自定义类实现 Runnable 接口,但是需要实例化一个 Thread,然后传入自己的自定义类
public static void main(String[] args) {
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread = new MyThread();
//实例化Thread,传入参数
Thread thread = new Thread(myThread);
thread.start();
}
1.3 Callable结合Future有返回值线程
有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现有返回结果的多线程了
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
List<Future> futureList = new ArrayList<>();
for (int i = 0; i < 10;i++){
futureList.add(executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return Thread.currentThread().getName() + "执行成功";
}
}));
}
try {
// 获取线程执行返回的结果
for (Future future : futureList) {
String result = future.get().toString();
System.out.println(result);
}
}catch (Exception e){
e.printStackTrace();
}
executorService.shutdown();
}
1.4 基于线程池的方式
基于线程池的方式,后面会讲述到。
2. 五种线程池
2.1 FixedThreadPool
固定线程数的线程池,核心线程数和最大线程数是一样的,就算任务数超过线程数,则是将任务放到任务队列进行等待,工作队列为 LinkedBlockingQueue,特点是任务可以无止境的提交,任务过多最终导致OOM。
2.2 CachedThreadPool
可缓存线程池,特点是线程数几乎是可以无限增加的,当线程闲置时可以对线程进行回收,所以说该线程池的线程数量不是固定不变的,存储提交任务的队列为 SynchronousQueue ,队列的容量为0,所以实际上不存储任务,只负责对任务进行中转和传递,工作队列为 SynchronousQueue,因为同步队列的特点,所以一旦累积无止境的创建线程,最终线程过多导致内存不足,而且线程过多也会频繁导致上下文切换(CPU时间片轮询策略)。
2.3 ScheduledThreadPool
支持定时或周期性执行任务的线程池,工作队列为 LinkedBlockingQueue。
2.4 SingleThreadExecutor
这个线程池只有一个线程 ,这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去,工作队列为 DelayedWorkQueue。
2.5 ForkJoinPool
ForkJoinPool主要是用来解决将一个大任务拆分成多个小任务,然后将小任务分给其他线程同时处理的问题,适用场景:ForkJoinPool 非常适合用于递归的场景,例如树的遍历、最优路径搜索等场景。
2.6 线程池的工作原理
值得注意的是,有一些知识点是需要知道的,下面我们的举个例子说明
假设有10个核心线程,已经创建好了5个线程,其中里面有2个是处于空闲状态的,则一个任务进来,还是会继续创建核心线程,其实这2个空闲状态的线程并不是真正的空闲,当他们执行完自己的任务后,会去任务队列获取任务,因为我们这时核心线程都还没创建满,肯定任务队列是没有任务的,所以获取不到就会处于阻塞状态,只有当10个核心线程创建满,任务一但加入队列,立刻唤醒之前那2个空闲线程来获取任务进行执行。所以是这样来保证N个线程一定是处于活状态的。
3. java阻塞队列
3.1 LinkedBlockingQueue
没有容量限制的阻塞队列,容量为 Integer.MAX_VALUE,几乎是无限制的
3.2 SynchronousQueue
同步队列,不做任务的存储,只做转发和传递,没有容量
3.3 DelayedWorkQueue
延迟工作队列,特点是可以延迟执行任务,DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是 “堆” 的数据结构
4. 线程池的拒绝策略
4.1 AbortPolicy
拒绝并抛出异常
4.2 DiscardPolicy
直接丢弃
4.3 DiscardOldestPolicy
丢弃存活时间最长的,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点
4.4 CallerRunsPolicy
谁提交谁执行,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务,通常是主线程(一般推荐使用这种拒绝策略)
5. 线程生命周期
5.1 五种线程状态
- New-新建状态
- Runnable 可运行状态
- Running 运行状态
- 阻塞状态:分为计时等待、等待、被阻塞
- 已终止
5.2 线程阻塞和等待的区别
阻塞:指线程请求锁失败进入的状态,处于阻塞状态线程会不断请求锁资源,一旦请求成功,就会进入就绪队列,等待执行(即等待分配CPU执行权)。
等待:处于等待状态需要等待其他线程的唤醒,或者是 join 的线程结束,线程进入等待状态会主动释放CPU执行权和锁资源。
6. 终止线程的三种方式
6.1 正常运行结束
程序运行结束,线程自动结束
6.2 使用退出标志
有些线程需要长时间运行,只有在外部某些条件满足的情况下,才能结束线程,使用一个 boolean 变量,用volatile修饰,通过这个标志来控制线程的退出
public class Chap4Main {
public volatile boolean exitVal = false;
public static void main(String[] args) throws InterruptedException{
Chap4Main chap4Main = new Chap4Main();
chap4Main.test();
Thread.sleep(2000);
chap4Main.exitVal = true;
}
public void test(){
class ThreadA extends Thread{
@Override
public void run() {
while (!exitVal){
System.out.println(Thread.currentThread().getName() + "正在执行");
}
System.out.println(Thread.currentThread().getName() + "已经退出了");
}
}
ThreadA threadA = new ThreadA();
threadA.start();
}
}
6.3 使用 Interrupt 方法
使用此方法中断线程有两种情况
- 线程处于阻塞状态,如果使用 sleep、wait方法,使线程处于阻塞状态,当调用线程的 interrupt 方法时,会抛出 InterruptException 异常,则捕获异常,然后 break 调出循环状态,进而结束线程的运行状态
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){
//非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行 break 跳出循环
}
}
}
}
- 线程未处于阻塞状态,使用 isInterrupted() 判断线程的中断标志,进而来退出循环
7. 线程的基本方法
7.1 wait
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后, 会释放对象的锁。因此, wait 方法一般用在同步方法或同步代码块中
7.2 sleep
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态
7.3 yield
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片
7.4 interrupt
给线程一个中断标识
7.5 join-重点学习
join() 方法可以用来等待其他线程终止,在当前线程调用A线程的 join 方法,则当前线程转为阻塞状态,必须等待A线程终止后,当前线程才能往下执行,可以用来控制线程的执行顺序
System.out.println(Thread.currentThread().getName() + "线程运行开始!");
Thread6 thread1 = new Thread6();
thread1.setName("线程 B");
thread1.join(); //thread1终止后主线程才能往下执行
System.out.println("这时 thread1 执行完毕之后才能执行主线程");
7.6 notify、notifyAll
notify:唤醒在此对象监视器上等待的单个线程
notifyAll:唤醒再次监视器上等待的所有线程
8. Volatile关键字
适用场景:一般使用 volatile 修饰变量,适用于一个变量被多个线程共享,线程直接给这个变量赋值
变量可见性:保证变量对所有线程可见,即当一个线程修改变量的值,新的值对于其他线程是可以立即获取的
禁止重排序:禁止指令重排
9. ThreadLocal关键字
线程本地变量,或者又叫线程本地存储
ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本,而不会影响其他线程的副本,确保了线程安全
适用场景:
1.保存线程不安全的工具类,例如每个线程都需要用到 SimpleDateFormat 时间工具类
2.每个线程内需要保存类似于全局变量的信息,例如在拦截器中获取的用户信息,可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享,因为不同线程获取到的用户信息是不一样的
注意要点:每次用完 ThreadLocal 都要调用 remove 方法,避免内存泄露
补充知识点:内存溢出和内存泄露
内存溢出:程序申请内存时,没有足够的内存空间可供使用,出现out of memory
内存泄露:申请分配内存进行使用,使用完之后无法回收,如java创建对象,申请堆内存空间,在GC时回收不了对象,称为内存泄露
ThreadLocal中的Map 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。所以,如果ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候, key 会被清理掉,⽽ value 不会被清理掉。这样⼀来, ThreadLocal的Map 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话, value 永远⽆法被 GC 回收,这个时候就可能会产⽣内存泄露。
ThreadLocal中的Map 实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null的记录。使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法 。
10. JAVA锁
10.1 synchronized关键字
synchronized 可以修饰方法,也可以修饰代码块,但是修饰方法也可分为静态方法与非静态方法
- 修饰静态方法
public class SynchronizedTest {
private static int number = 0;
public static void main(String[] args) {
//创建5个线程,制造多线程场景
for (int i = 0; i < 5; i++) {
new Thread("Thread-" + i) {
@Override
public void run() {
try {
//sleep一个随机时间
Thread.sleep(new Random().nextInt(5) * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用静态方法
SynchronizedTest.testStaticSynchronized();
}
}.start();
}
}
/**
* 静态方法加锁
*/
public synchronized static void testStaticSynchronized() {
//对number加1操作
number++;
//打印(线程名 + number)
System.out.println(Thread.currentThread().getName() +
" -> 当前number为" + number);
}
}
// Thread-4 -> 当前number为1
// Thread-3 -> 当前number为2
// Thread-1 -> 当前number为3
// Thread-0 -> 当前number为4
// Thread-2 -> 当前number为5
可以看出没有并发异常
- 修饰非静态方法
public class SynchronizedTest {
private static int number = 0;
public static void main(String[] args) {
//创建5个线程,制造多线程场景
for (int i = 0; i < 5; i++) {
new Thread("Thread-" + i) {
@Override
public void run() {
try {
//sleep一个随机时间
Thread.sleep(new Random().nextInt(5) * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
SynchronizedTest test = new SynchronizedTest();
//通过对象,调用非静态方法
test.testNonStaticSynchronized();
}
}.start();
}
}
/**
* 非静态方法加锁
*/
public synchronized void testNonStaticSynchronized() {
number++;
System.out.println(Thread.currentThread().getName() + " -> " + number);
}
}
// logcat日志
// Thread-0 -> 当前number为1
// Thread-4 -> 当前number为1
// Thread-2 -> 当前number为3
// Thread-1 -> 当前number为4
// Thread-3 -> 当前number为4
看logcat日志,出现了数据异常,很明显不加static修饰,是没办法保证线程同步的
经过上面两个例子分析,我们可以发现
类锁:当synchronized修饰一个static方法时,获取到的是类锁,作用于这个类的所有对象
对象锁:当synchronized修饰一个非static方法时,获取到的是对象锁,作用于调用该方法的当前对象
类锁和对象锁不同,他们之间不会产生互斥
第一,synchronized修饰代码块,需要指明加的是类锁还是对象锁
public class SynchronizedTest {
public static void main(String[] args) {
//通过synchronized修饰代码块
synchronized (SynchronizedTest.class) {
System.out.println("this is in synchronized");
}
}
}
因为synchronized仅仅是Java提供的关键字,那么要想知道底层原理,我们需要通过javap命令反编译class文件,看看他的字节码到底长啥样
看反编译的结果,着重看红色标注的地方。我们可以清楚地发现,代码块同步是使用monitorenter和monitorexit两个指令完成的,monitorenter指令插入到同步代码块的开始位置,monitorexit插入到方法结束处和异常处,被同步的代码块由monitorenter指令进入,然后在monitorexit指令处结束。
这里的重要角色monitor到底是什么呢?简单来说,可以直接理解为锁对象,只不过是虚拟机实现的,底层是依赖于操作系统的Mutex Lock实现。任何Java对象都有一个monitor与之关联,或者说所有的Java对象天生就可以成为monitor,这也就可以解释我们平时在使用synchronized关键字时可以将任意对象作为锁的原因了。
monitorenter
在执行monitorenter时,当前线程会尝试获取锁,如果这个monitor没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1,获取锁成功,继续执行下面的代码。如果获取锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
monitorexit
与此对应的,当执行monitorexit指令时,锁的计数器也会减1,当计数器等于0时,当前线程就释放锁,不再是这个monitor的所有者。这个时候,其他被这个monitor阻塞的线程便可以尝试去获取这个monitor的所有权了。
到这里,synchronized修饰代码块实现同步的原理,我相信你已经搞懂了吧,那趁热打铁,继续看看修饰方法又是怎么处理的
第二,synchronized修饰方法
public class SynchronizedTest {
public static void main(String[] args) {
doSynchronizedTest();
}
//通过synchronized修饰方法
public static synchronized void doSynchronizedTest(){
System.out.println("this is in synchronized");
}
}
从反编译的结果来看,这次方法的同步并没有直接通过指令monitorenter和monitorexit来实现,但是相对于其他普通的方法,它的方法描述多了一个ACC_SYNCHRONIZED标识符。想必你都能猜出来,虚拟机无非就是根据这个标识符来实现方法同步,其实现原理大致是这样的:虚拟机调用某个方法时,调用指令首先会检查该方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象,这样也保证了同步。当然这里monitor其实会有类锁和对象锁两种情况,上面就有说到。
关于synchronized的原理,这边再简单总结一下。synchronized关键字的实现同步分两种场景,代码块同步是使用monitorenter和monitorexit指令的形式,而方法同步使用的ACC_SYNCHRONIZED标识符的形式。但万变不离其宗,这两种形式的根本都是基于 JVM 进入和退出 monitor 对象锁来实现操作同步
从上面的原理分析知道,synchronized关键字是基于JVM进入和退出 monitor 对象锁来实现操作同步,这种抢占式获取monitor锁,性能上铁定堪忧呀。这时候烦人的面试官又上线了,请问JDK1.6以后对synchronized锁做了哪些优化?这个问题难度较大,我们细细地说。
其实在JDK1.6之前,synchronized内部实现的锁都是重量级锁,也就是说没有抢到CPU使用权的线程都得堵塞,然而在程序真正运行过程中,其实很多情况下并不需要这么重,每次都直接堵塞反而会导致更多的线程上下文切换,消耗更多的资源。所以在JDK1.6以后,对synchronized锁进行了优化,引入偏向锁,轻量级锁,重量级锁的概念
锁信息
熟悉synchronized原理的同学应该都知道,当一个线程访问synchronized包裹的同步代码块时,必须获取monitor锁才能进入代码块,退出或抛出异常时再去释放monitor锁。这里就有问题了,线程需要获取的synchronized锁信息是存在哪里的呢?所以在介绍各种锁的概念之前,我们必须先尝试解答这个疑惑。
在学习JVM时,我们了解过一个对象是由三部分组成的,分别是对象头、实例数据以及对齐填充。其中对象头里又存储了对象本身运行时数据,包括哈希码、GC分代年龄,当然还有我们这里要讲的与锁相关的标识,比如锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等
简单来说:对象头的数据块 Mark Word,里面有2个重要字段:锁标志位和占用该锁的 Thread ID,这样子来实现锁的原理的。
偏向锁
锁是用于并发场景的,然而,在大多数情况下,锁其实并不存在多线程竞争,甚至都是由同一个线程多次获取,所以没有必要花太多代价去放在锁的获取上,这时偏向锁就应运而生了
偏向锁,顾名思义,它会偏向于第一个访问锁的线程。当一个线程第一次访问同步代码块并尝试获取锁时,会直接给线程加一个偏向锁,并在对象头的锁记录里存储锁偏向的线程ID,这样的话,以后该线程再次进入和退出同步代码块时就不需要进行CAS等操作来加锁和解锁,只需查看对象头里是否存储着指向当前线程的偏向锁即可。很明显,偏向锁的做法无疑是消除了同一线程竞争锁的开销,大大提高了程序的运行性能,针对的是同一线程的情况
当然,如果在运行过程中,突然有其他线程抢占该锁,如果通过CAS操作获取锁成功,直接替换对象头中的线程ID为新的线程ID,继续会保持偏向锁状态;反之如果没有抢成功时,那么持有偏向锁的线程会被挂起,造成STW现象,JVM会自动消除它身上的偏向锁,偏向锁升级为轻量级锁。
轻量级锁
轻量级锁位于偏向锁与重量级锁之间,其主要目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。自旋锁就是轻量级锁的一种典型实现。
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做进入阻塞挂起状态,它们只需要稍微等一等,其实就是进行自旋操作,等持有锁的线程释放锁后即可立即获取锁,这样就进一步避免切换线程引起的消耗
当然,自旋锁也不是最终解决方案,比如遇到锁的竞争非常激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu进行自旋检查,这对于业务来讲就是无用功。
如果线程自旋带来的消耗大于线程阻塞挂起操作的消耗,那么自旋锁就弊大于利了,所以这个自旋的次数是个很重要的阈值,JDK1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间
重量级锁
自旋多次还是失败后,一般就直接升级成重量级锁了,也就是锁的最高级别了,在上一篇synchronized的原理里有讲到其底层基于monitor对象实现,而monitor的本质又是依赖于操作系统的Mutex Lock实现。这里其实又涉及到我们之前有篇文章讲过的一个知识,频繁切换线程的危害?因为操作系统实现线程之间的切换需要进行用户态到内核态的切换,不用想就知道,切换成本当然就很高了。
当JVM检查到重量级锁之后,会把想要获得锁的线程进行阻塞,插入到一个阻塞队列,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程,都需要上面所说的从用户态转换到内核态,这个成本比较高,有可能比真正需要执行的同步代码的消耗还要大。
我们依次理了一遍,由无锁到偏向锁,再到轻量级锁,最后升级为重量级锁
总结:synchronized关键字在加锁时,每个对象都有个 monitor 对象, 加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
10.2 ReentrantLock关键字
ReentrantLock 继承接口 Lock 并实现了接口中定义的方法,是一种可重入锁,除了能完成synchronized能完成的工作外,还提供了可响应中断锁、可轮训锁请求、定时锁等
ReentrantLock 与 synchronized 的比较
- ReentrantLock 通过方法 lock() 与 unlock()来进行加锁与解锁操作,与 synchronized 会被 JVM 自动解锁机制不同, ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作
- ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用 ReentrantLock
ReentrantLock 实现
public class MyService {
private Lock lock = new ReentrantLock();
//Lock lock=new ReentrantLock(true);//公平锁
//Lock lock=new ReentrantLock(false);//非公平锁
private Condition condition = lock.newCondition();//创建 Condition
public void testMethod() {
try {
lock.lock();//lock 加锁
//wait 方法等待:
//System.out.println("开始 wait");
condition.await();
//通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
//signal 方法唤醒
condition.signal();//condition 对象的 signal 方法可以唤醒 wait 线程
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName()
+ (" " + (i + 1)));
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
**Condition 类和 Object 类锁方法区别 **
- Condition 类的 awiat 方法和 Object 类的 wait 方法等效
- Condition 类的 signal 方法和 Object 类的 notify 方法等效
- Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
- ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的
**tryLock 和 lock 和 lockInterruptibly 的区别 **
- tryLock 能获得锁就返回 true,不能就立即返回 false, tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
- lock 能获得锁就返回 true,不能的话一直等待获得锁
- lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常
10.3 ReadWriteLock关键字
读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率
读锁
多个读锁不互斥,数据只供读取
写锁
读锁与写锁互斥,并且多个写锁互斥,只能同时一个线程负责修改数据
10.4 Lock接口
Lock接口的主要方法
-
void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁(处于等待状态).
-
boolean tryLock(): 如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
-
void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
-
Condition newCondition(): 条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
-
getHoldCount() : 查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次数。
-
getQueueLength(): 返回正等待获取此锁的线程估计数,比如启动 10 个线程, 1 个线程获得锁,此时返回的是 9
-
getWaitQueueLength:(Condition condition) 返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
-
hasWaiters(Condition condition): 查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
-
hasQueuedThread(Thread thread): 查询给定线程是否等待获取此锁
-
hasQueuedThreads(): 是否有线程等待此锁
-
isFair(): 该锁是否公平锁
-
isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true
-
isLock(): 此锁是否有任意线程占用
-
lockInterruptibly(): 如果当前线程未被中断,获取锁
-
tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
-
tryLock(long timeout TimeUnit unit): 如果锁在给定等待时间内没有被另一个线程保持,则获取该锁
10. 5 偏向锁
Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得 。
偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护 。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令, 而偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令
10.6 轻量级锁
轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁
当然,轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁
明白一点:轻量级锁所适应的场景是线程交替执行同步代码块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
10.7 重量级锁
synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间 ,因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”
偏向锁、轻量级锁、重量级锁小结
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争
重量级锁:有实际竞争,且锁竞争时间长
10.8 共享锁和独占锁
独占锁
独占锁模式下,每次只能有一个线程能持有锁, ReentrantLock 就是以独占方式实现的互斥锁,独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待
共享锁
共享锁则允许多个线程同时获取锁,并发访问共享资源,如: ReadWriteLock。 共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源
10.9 可重入锁
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的权限,但不受
影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁
作者:weixin_38319425
来源链接:https://blog.csdn.net/weixin_38319425/article/details/123132286