【8月后端】JAVA多线程(112000字)
- 1. 多线程环境下的线程安全体现在哪些方面?
- 2. 创建线程的方式及其区别?
- 3. 说一下从Java API层面上的6种线程状态
- 4 final原理
- 4 ThreadLocal有了解吗?
- 5. synchronized 和Lock区别
- 6. as-if-serial与happens-before
- 【同步的方式】
- [1] synchronized同步方法
- [2 synchronized同步代码块
- [3] 使用volatile实现线程同步
- [4] 使用 ReentrantLock实现线程同步
- [5] 使用ThreadLocal实现线程同步
- [6] 使用LinkedBlockingQueue实现线程同步
- [7] 使用原子变量实现线程同步
- 【锁】
- [1] 公平锁/非公平锁
- [2] 可重入锁
- [3] 独享锁/共享锁(互斥锁/读写锁)
- [4] 乐观锁/悲观锁
- [5] 分段锁
- [6] 偏向锁/轻量级锁/重量级锁
- [7] 自旋锁
- [8] 可中断锁/不可中断锁/超时时间
- [9] 显式锁/隐式锁
- [10] 条件变量
- [11] AQS
- 【<锁的属性>】
- [1] 为什么要加锁之临界区
- [2] 实现一个锁需要考虑哪些方面?
- 【<锁升级>】
- [1] Java对象头
- [2] Mark word 结构
- [3] Monitor
- [4] synchronized锁升级
- 无锁
- 偏向锁(可关闭)
- 轻量级锁
- 重量级锁
- [6] synchronized锁对比
- 【CAS】
- [1] 什么是CAS?
- [2] 什么是自旋锁?
- [3] CAS可能出现的问题有什么?
- [4] 哪里有用到CAS?
- [5] CAS 和volatile如何实现无锁并发?
- 【<死锁>】
- [1] 什么是死锁
- [2] 产生死锁的原因?
- [3] 死锁的产生必须满足的四个必要条件
- [4] 解决死锁的基本方法
- [5] 说下对悲观锁和乐观锁的理解?
- [6] 乐观锁常见的两种实现方式是什么?
- 【<线程锁死>】
- [1] 什么是线程锁死
- [2] 线程锁死分为哪两种
- [3] 活锁
- [3] 线程饥饿
- [5] 线程活性故障总结
- 【<多线程常用方法>】
- [1] start 与 run
- [2] sleep 与 yield
- [3] sleep 和 wait
- [4] Daemon
- [5] join和yield
- [6] wait() notify() notifyAll()
- [7] await() signal() signalAll()
- [8] InterruptedException
- [9] interrupted()
- [10] Executor
- [11] CopyOnWriteArrayList
- 【ReentrantLock】
- [1] ReentrantLock简介
- [2] ReentrantLock方法
- [3] ReentrantLock实战
- [4] ReentrantLock原理
- [5] ReentrantLock如何实现可重入锁?
- [6] ReentrantLock如何实现公平锁和非公平锁?
- [7] 条件变量Condition实现原理?
- [8] 谈谈 synchronized 和 ReenTrantLock 的区别?
- 【< volatile 关键字专题>】
- [1] 谈一下你对 volatile 关键字的理解?
- [2]Volatile如何保证可见性和有序性?
- 1. 可见性
- 2.有序性的
- [3] volatile在什么情况下可以替代锁?
- 【< synchronized专题>】
- [1] synchronized 关键字?
- [2] synchronized 关键字使用场景
- [3] synchronized 内部字节码指令
- [4] synchronized如何保证有序性、可见性、原子性?
- 1. 原子性
- 2. 可见性
- 3. 有序性
- [5] synchronized 关键字锁升级过程?
- [5] JVM 对 synchronized 的锁优化
- [8] synchronized 和 volatile 的区别是什么?
- 【< AQS>】
- [1]什么是AQS
- [2] LCK队列源码及其实现
- [3] 独占式同步状态获取
- [4] 共享式同步状态获取与释放
- [5] 独占式超时获取同步状态
- [5]AQS的思想
- 【< J.U.C>】
- [1] AQS
- [2] ReentrantLock
- [3] ReentrantReadWriteLock
- [4] StampedLock
- [5] Semaphore
- [6] threadlocal
- [7] CountDownLatch
- [8] CyclicBarrier?
- [9] Atmoic
- [10] FutureTask
- [11] ForkJoin
- 【Java中的线程池】
- [1] 使用线程池的好处
- [2] Executor框架
- [3] ThreadPoolExecutor类的参数字段
- [4] 线程池的排队策略
- [6] 拒绝策略
- [5] 常见的阻塞队列
- [6] Java提供的四种线程池
- [7] 手写一个线程池
1. 多线程环境下的线程安全体现在哪些方面?
答:多线程环境下的线程安全主要体现在原子性,可见性与有序性方面。
-
原子性是一组操作要么完全发生,要么没有发生,其余线程不会看到中间过程的存在。
对于涉及到共享变量访问的操作,若该操作从执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,该操作具有原子性。即,其它线程不会“看到”该操作执行了部分的中间结果。
**举例:**银行转账流程中,A账户减少了100元,那么B账户就会多100元,这两个动作是一个原子操作。我们不会看到A减少了100元,但是B余额保持不变的中间结果。
Java原子性的实现方式:
- 利用锁的排他性,保证同一时刻只有一个线程在操作一个共享变量
- 利用**CAS(Compare And Swap)**保证
- Java语言规范中,保证了除long和double型以外的任何变量的写操作都是原子操作
关于原子性,你应该注意的地方:
- 原子性针对的是多个线程的共享变量,所以对于局部变量来说不存在共享问题,也就无所谓是否是原子操作
- 单线程环境下讨论是否是原子操作没有意义
- volatile关键字仅仅能保证变量写操作的原子性,不保证复合操作,比如说读写操作的原子性
-
可见性是指一个线程对共享变量的更新对于另外一个线程是否可见的问题。
**定义:**可见性是指一个线程对于共享变量的更新,对于后续访问该变量的线程是否可见的问题。
为了阐述可见性问题,我们先来简单介绍处理器缓存的概念。
现代处理器处理速度远大于主内存的处理速度,所以在主内存和处理器之间加入了寄存器,高速缓存,写缓冲器以及无效化队列等部件来加速内存的读写操作。也就是说,我们的处理器可以和这些部件进行读写操作的交互,这些部件可以称为处理器缓存。
处理器对内存的读写操作,其实仅仅是与处理器缓存进行了交互。一个处理器的缓存上的内容无法被另外一个处理器读取,所以另外一个处理器必须通过缓存一致性协议来读取的其他处理器缓存中的数据,并且同步到自己的处理器缓存中,这样保证了其余处理器对该变量的更新对于另外处理器是可见的。
在单处理器中,为什么也会出现可见性的问题呢?
单处理器中,由于是多线程并发编程,所以会存在线程的上下文切换,线程会将对变量的更新当作上下文存储起来,导致其余线程无法看到该变量的更新。所以单处理器下的多线程并发编程也会出现可见性问题的。
-
有序性是指一个线程对共享变量的更新在其余线程看起来是按照什么顺序执行的问题。
**定义:**有序性是指一个处理器上运行的线程所执行的内存访问操作在另外一个处理器上运行的线程来看是否有序的问题。
重排序:
为了提高程序执行的性能,Java编译器在其认为不影响程序正确性的前提下,可能会对源代码顺序进行一定的调整,导致程序运行顺序与源代码顺序不一致。重排序是对内存读写操作的一种优化,在单线程环境下不会导致程序的正确性问题,但是多线程环境下可能会影响程序的正确性。
重排序举例:
Instance instance = new Instance()都发生了啥?
具体步骤如下所示三步:- 在堆内存上分配对象的内存空间
- 在堆内存上初始化对象
- 设置instance指向刚分配的内存地址
第二步和第三步可能会发生重排序,导致引用型变量指向了一个不为null但是也不完整的对象。(在多线程下的单例模式中,我们必须通过volatile来禁止指令重排序)
什么是重排序?
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
重排序的类型有哪些呢?源码到最终执行会经过哪些重排序呢?
一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标,而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。
JMM对底层尽量减少约束,使其能够发挥自身优势。
因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。
一般重排序可以分为如下三种:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
- 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
这里还得提一个概念,
as-if-serial
。不管怎么重排序,单线程下的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
- 总结:
- 原子性是一组操作要么完全发生,要么没有发生,其余线程不会看到中间过程的存在。注意,原子操作+原子操作不一定还是原子操作。
- 可见性是指一个线程对共享变量的更新对于另外一个线程是否可见的问题。
- 有序性是指一个线程对共享变量的更新在其余线程看起来是按照什么顺序执行的问题。
- 可以这么认为,原子性 + 可见性 -> 有序性
缘可续
2. 创建线程的方式及其区别?
方法一,继承 Thread类
// 创建线程对象
Thread t = new Thread() {public void run() {// 要执行的任务}
};
// 启动线程
t.start();
例如:
package cn.mycast;import lombok.extern.slf4j.Slf4j;@Slf4j(topic= "c.创建一个线程")
public class 创建一个线程 {public static void main(String[] args) {Thread t1 =new Thread("t1"){@Overridepublic void run() {log.debug("hello");}};t1.start();}
}
结果
15:33:39.617 c.创建一个线程 [t1] - hello
方法二,实现 Runnable 接口
把【线程】和【任务】(要执行的代码)分开
-
Thread 代表线程
-
Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {public void run(){// 要执行的任务}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
例如:
// 创建任务对象
Runnable task2 = new Runnable() {@Overridepublic void run() {log.debug("hello");}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
输出:
9:19:00 [t2] c.ThreadStarter - hello
Java 8 以后可以使用 lambda 精简代码
// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐Thread t2 = new Thread(task2, "t2");
t2.start();
区别:
方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
-
用 Runnable 更容易与线程池等高级 API 配合
-
用 Runnable 让任务类脱离了 Thread 继承体系,更灵活(java不能实现多继承的补偿)
方法三,实现Callable接口
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {log.debug("hello"); return 100});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
方法四:使用线程池创建
3. 说一下从Java API层面上的6种线程状态
- 新建(New):这是属于一个已经创建的线程,但是还没有调用start方法启动的线程所处的状态。
- 可运行(Runnable):该状态包含两种可能。有可能正在运行,或者正在等待CPU资源。包含了操作系统线程状态种的运行,可运行状态和阻塞状态(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行);
- 阻塞(Blocked):阻塞状态,当线程准备进入synchronized同步块或同步方法(排它锁)的时候,需要申请一个监视器锁而进行的等待,会使线程进入BLOCKED状态。如果其线程释放了锁就会结束此状态;
- 等待(Waiting):该状态的出现是因为调用了方法1。处于该状态下的线程在等待另一个线程 执行一些其余action来将其唤醒。等待其他线程显式唤醒,否则不会再被分配CPU时间片;
- 限期等待(Timed Waiting):该状态和上一个状态其实是一样的,调用了方法2是不过其等待的时间是明确的。
- 死亡(TERMINATED):消亡状态比较容易理解,那就是线程执行结束了,run方法执行结束表示线程处于消亡状态了。
方法1:
进入方法 | 退出方法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
LockSupport.park() 方法 | LockSupport.unpark(Thread) |
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
方法2:
进入方法 | 退出方法 |
---|---|
Thread.sleep() 方法 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) |
LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) |
附:线程状态转换
假设有线程 Thread t
情况 1 NEW --> RUNNABLE
- 当调用 t.start() 方法时,由 NEW --> RUNNABLE
情况 2 RUNNABLE <–> WAITING
t 线程用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
- 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
- 竞争锁成功,t 线程从 WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 WAITING --> BLOCKED
情况 3 RUNNABLE <–> WAITING
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
- 注意是当前线程在t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
情况 4 RUNNABLE <–> WAITING
-
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
-
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
情况 5 RUNNABLE <–> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
-
调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
-
t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
- 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
情况 6 RUNNABLE <–> TIMED_WAITING
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
注意是当前线程在t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE
情况 7 RUNNABLE <–> TIMED_WAITING
-
当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
-
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
情况 8 RUNNABLE <–> TIMED_WAITING
-
当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
-
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE
情况 9 RUNNABLE <–> BLOCKED
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE <–> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
4 final原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QwWJRmLC-1596599154311)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200714195425381.png)]
4 ThreadLocal有了解吗?
**答:**使用ThreadLocal维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。
ThreadLocal内部实现机制:
- 每个线程内部都会维护一个类似HashMap的对象,称为ThreadLocalMap,里边会包含若干了Entry(K-V键值对),相应的线程被称为这些Entry的属主线程
- Entry的Key是一个ThreadLocal实例,Value是一个线程特有对象。Entry的作用是为其属主线程建立起一个ThreadLocal实例与一个线程特有对象之间的对应关系
- Entry对Key的引用是弱引用;Entry对Value的引用是强引用。
5. synchronized 和Lock区别
https://blog.csdn.net/qq_29373285/article/details/85964460
- 在实现上
synchronized是一个关键字,它基于JVM。它有锁升级过程,从偏向锁,轻量级锁,到重量级锁。
Lock是一个接口,它是基于JDK,它实现的主要实现类是ReentrantLock,它的使用也离不开AQS。
- 在使用上
synchronized是隐式锁,加锁解锁对使用者是隐藏的,可以作用于方法,代码块和类。
Lock是显示锁,需要手动上锁和释放锁(lock和unlock)
- 在功能上
-
Lock和synchronized都是互斥锁且支持可重入
-
Lock支持默认非公平锁,但支持公平锁,synchronized只支持非公平锁
-
lock的condition支持多个条件变量,但是synchronized
-
Lock 可中断,而 synchronized 不行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3idqMrdX-1596599154315)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200725094645345.png)]
6. as-if-serial与happens-before
as-if-serial规则:
as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。
happens-before:
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。具体的定义为:
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
下面来比较一下as-if-serial和happens-before:
- as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
- as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
- as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度
【同步的方式】
https://www.cnblogs.com/Terry-Wu/p/10788663.html
为何要使用同步?
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,
从而保证了该变量的唯一性和准确性。
[1] synchronized同步方法
即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如:
public synchronized void save(){}
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
[2 synchronized同步代码块
即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
代码如:
synchronized(object){ }
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
package com.xhj.thread;/*** 线程同步的运用** @author XIEHEJUN**/public class SynchronizedThread {class Bank {private int account = 100;public int getAccount() {return account;}/*** 用同步方法实现** @param money*/public synchronized void save(int money) {account += money;}/*** 用同步代码块实现** @param money*/public void save1(int money) {synchronized (this) {account += money;}}}class NewThread implements Runnable {private Bank bank;public NewThread(Bank bank) {this.bank = bank;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {// bank.save1(10);bank.save(10);System.out.println(i + "账户余额为:" + bank.getAccount());}}}/*** 建立线程,调用内部类*/public void useThread() {Bank bank = new Bank();NewThread new_thread = new NewThread(bank);System.out.println("线程1");Thread thread1 = new Thread(new_thread);thread1.start();System.out.println("线程2");Thread thread2 = new Thread(new_thread);thread2.start();}public static void main(String[] args) {SynchronizedThread st = new SynchronizedThread();st.useThread();}}
[3] 使用volatile实现线程同步
- volatile关键字为域变量的访问提供了一种免锁机制,
- 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
- 因此每次使用该域就要重新计算,而不是使用寄存器中的值
- volatile不会提供任何原子操作,它也不能用来修饰final类型的变量
例如: 在上面的例子当中,只需在account前面加上volatile修饰,即可实现线程同步。
代码实例:
//只给出要修改的代码,其余代码与上同class Bank {//需要同步的变量加上volatileprivate volatile int account = 100;public int getAccount() {return account;}//这里不再需要synchronizedpublic void save(int money) {account += money;}}
注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。 用final域,有锁保护的域和volatile域可以避免非同步的问题。
[4] 使用 ReentrantLock实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。 ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
- ReentrantLock() : 创建一个ReentrantLock实例
- lock() : 获得锁
- unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
例如: 在上面例子的基础上,改写后的代码为: 代码实例:
//只给出要修改的代码,其余代码与上同class Bank {//需要同步的变量加上volatileprivate volatile int account = 100;public int getAccount() {return account;}//这里不再需要synchronizedpublic void save(int money) {account += money;}}
注:关于Lock对象和synchronized关键字的选择:
- 最好两个都不用,使用一种java.util.concurrent包提供的机制, 能够帮助用户处理所有与锁相关的代码。
- 如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
- 如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
[5] 使用ThreadLocal实现线程同步
如果使用ThreadLocal(局部变量)管理变量,则每一个使用该变量的线程都获得该变量的副本, 副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal 类的常用方法
- ThreadLocal() : 创建一个线程本地变量
- get() : 返回此线程局部变量的当前线程副本中的值
- initialValue() : 返回此线程局部变量的当前线程的"初始值"
- set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
在上面例子基础上,修改后的代码为:
//只改Bank类,其余代码与上同public class Bank{//使用ThreadLocal类管理共享变量accountprivate static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue(){return 100;}};public void save(int money){account.set(account.get()+money);}public int getAccount(){return account.get();}}
注:ThreadLocal与同步机制
- a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
- b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式
[6] 使用LinkedBlockingQueue实现线程同步
前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。 本小节主要是使LinkedBlockingQueue来实现线程的同步 LinkedBlockingQueue是一个基于已连接节点的,范围任意的blocking queue。
队列是先进先出的顺序(FIFO),关于队列以后会详细讲解~
LinkedBlockingQueue 类常用方法
- LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
- put(E e) : 在队尾添加一个元素,如果队列满则阻塞
- size() : 返回队列中的元素个数
- take() : 移除并返回队头元素,如果队列空则阻塞
代码实例:
实现商家生产商品和买卖商品的同步
package com.xhj.thread;import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;/*** 用阻塞队列实现线程同步 LinkedBlockingQueue的使用** @author XIEHEJUN**/
public class BlockingSynchronizedThread {/*** 定义一个阻塞队列用来存储生产出来的商品*/private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();/*** 定义生产商品个数*/private static final int size = 10;/*** 定义启动线程的标志,为0时,启动生产商品的线程;为1时,启动消费商品的线程*/private int flag = 0;private class LinkBlockThread implements Runnable {@Overridepublic void run() {int new_flag = flag++;System.out.println("启动线程 " + new_flag);if (new_flag == 0) {for (int i = 0; i < size; i++) {int b = new Random().nextInt(255);System.out.println("生产商品:" + b + "号");try {queue.put(b);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println("仓库中还有商品:" + queue.size() + "个");try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}} else {for (int i = 0; i < size / 2; i++) {try {int n = queue.take();System.out.println("消费者买去了" + n + "号商品");} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println("仓库中还有商品:" + queue.size() + "个");try {Thread.sleep(100);} catch (Exception e) {// TODO: handle exception}}}}}public static void main(String[] args) {BlockingSynchronizedThread bst = new BlockingSynchronizedThread();LinkBlockThread lbt = bst.new LinkBlockThread();Thread thread1 = new Thread(lbt);Thread thread2 = new Thread(lbt);thread1.start();thread2.start();}}
注:BlockingQueue定义了阻塞队列的常用方法,尤其是三种添加元素的方法,我们要多加注意,当队列满时:
- add()方法会抛出异常
- offer()方法返回false
- put()方法会阻塞
[7] 使用原子变量实现线程同步
需要使用线程同步的根本原因在于对普通变量的操作不是原子的。那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即-这几种行为要么同时完成,要么都不完成。在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
- AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
- addAddGet(int dalta) : 以原子方式将给定值与当前值相加
- get() : 获取当前值
代码实例:
只改Bank类,其余代码与上面第一个例子同
class Bank {private AtomicInteger account = new AtomicInteger(100);public AtomicInteger getAccount() {return account;}public void save(int money) {account.addAndGet(money);}}
补充–原子操作主要有:
- 对于引用变量和大多数原始变量(long和double除外)的读写操作;
- 对于所有使用volatile修饰的变量(包括long和double)的读写操作。
【锁】
JAVA锁有哪些种类,以及区别(转)
- 实现上
Synchronized
ReentrantLock
CAS
Volatile
2 类型上
- 公平锁/非公平锁 :
- 可重入锁
- 独享锁/共享锁
- 互斥锁/读写锁
- 乐观锁/悲观锁
- 分段锁
- 偏向锁/轻量级锁/重量级锁
- 自旋锁
上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释
[1] 公平锁/非公平锁
- 介绍:
公平锁是指多个线程按照申请锁的顺序来获取锁,
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
- 优缺点:
公平锁可以防止线程饥饿。
非公平锁的优点在于吞吐量比公平锁大,有可能会造成优先级反转或者饥饿现象。
- 实现:
对于Java ReentrantLock
而言,默认是非公平锁。,但是支持公平锁。
对于Synchronized
而言,也是一种非公平锁。由于其并不像ReentrantLock
是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
1. 公平调度方式:
按照申请的先后顺序授予资源的独占权。
2. 非公平调度方式:
在该策略中,资源的持有线程释放该资源的时候,等待队列中一个线程会被唤醒,而该线程从被唤醒到其继续执行可能需要一段时间。在该段时间内,**新来的线程(活跃线程)**可以先被授予该资源的独占权。
如果新来的线程占用该资源的时间不长,那么它完全有可能在被唤醒的线程继续执行前释放相应的资源,从而不影响该被唤醒的线程申请资源。
公平调度和非公平调度方式优缺点分析
非公平调度策略:
- 优点:吞吐率较高,单位时间内可以为更多的申请者调配资源
- 缺点:资源申请者申请资源所需的时间偏差可能较大,并可能出现线程饥饿的现象
公平调度策略:
- 优点:线程申请资源所需的时间偏差较小;不会出现线程饥饿的现象;适合在资源的持有线程占用资源的时间相对长或者资源的平均申请时间间隔相对长的情况下,或者对资源申请所需的时间偏差有所要求的情况下使用;
- 缺点:吞吐率较小
接下来,我们一起来看看JVM对synchronized内部锁的调度方式吧。
JVM对synchronized内部锁的调度
JVM对内部锁的调度是一种非公平的调度方式,JVM会给每个内部锁分配一个入口集(Entry Set),用于记录等待获得相应内部锁的线程。当锁被持有的线程释放的时候,该锁的入口集中的任意一个线程将会被唤醒,从而得到再次申请锁的机会。被唤醒的线程等待占用处理器运行时可能还有其他新的活跃线程与该线程抢占这个被释放的锁.
[2] 可重入锁
- 介绍:
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
- 优缺点:
可重入锁的一个好处是可一定程度避免死锁。
- 实现:
对于Java ReentrantLock
而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock
重新进入锁。
对于Synchronized
而言,也是一个可重入锁。
synchronized可重入锁的实现:
之前谈到过,每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
synchronized void setA() throws Exception{Thread.sleep(1000);setB();
}synchronized void setB() throws Exception{Thread.sleep(1000);
}
[3] 独享锁/共享锁(互斥锁/读写锁)
- 介绍:
独享锁是指该锁一次只能被一个线程所持有。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
共享锁是指该锁可被多个线程所持有。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
- 实现:
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
和Synchronized
读写锁在Java中的具体实现就是ReadWriteLock
对于Synchronized
而言,当然是独享锁。
[4] 乐观锁/悲观锁
- 介绍:
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。悲观锁适合写操作非常多的场景
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
- 优缺点:
从悲观锁适合写操作非常多的场景
乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
- 实现:
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
[5] 分段锁
- 介绍:
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
- 实现:
我们以ConcurrentHashMap
来说一下分段锁的含义以及设计思想,ConcurrentHashMap
中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
[6] 偏向锁/轻量级锁/重量级锁
- 介绍:
这三种锁是指锁的状态,并且是针对Synchronized
。在Java 5通过引入锁升级的机制来实现高效Synchronized
。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
- 实现:
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
[7] 自旋锁
- 介绍:
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。
- 优缺点
好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU造成ABA问题。
- 实现:
典型的自旋锁实现的例子,可以参考自旋锁的实现
[8] 可中断锁/不可中断锁/超时时间
可中断锁:顾名思义,就是可以相应中断的锁。**不会无限制等待下去,是避免死锁的一种方式。**在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
超时时间:中断是被动的打断,而设置超时时间是主动的打断,可以避免死锁。
[9] 显式锁/隐式锁
synchronized加锁是隐式加锁,使用者不会看到其加锁解锁过程锁升级过程·。
ReentrantLock 加锁和解锁是显示的。如果 ReentrantLock 调用lock方法加锁, unlock 方法解锁,否则会造成死锁。
[10] 条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
synchronized 是那些不满足条件的线程都在一间休息室等消息
而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤
醒
使用要点:
- await 前需要获得锁
await 执行后,会释放锁,进入 conditionObject 等待
await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
竞争 lock 锁成功后,从 await 后继续执行
[11] AQS
AQS是AbustactQueuedSynchronizer(队列同步器)的简称,它是一个Java提供的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。
AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。
AQS中可重写的方法分为独占式与共享式的
可以直接调用的模板方法有
同步器提供的如下3个方法来访问或修改同步状态。 ·getState():获取当前同步状态。 ·setState(int newState):设置当前同步状态。 ·compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
2.2 实现
2.2.1 同步队列
同步队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,获取锁失败那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和 后继节点
使用CAS将节点插入到尾部,并用tail指向该结点
2.2.2 独占锁的获取和释放流程
获取
- 调用入口方法acquire(arg)
- 调用模版方法tryAcquire(arg)尝试获取锁,若成功则返回,若失败则走下一步
- 将当前线程构造成一个Node节点,并利用addWaiter(Node node) 将其加入到同步队列尾部
- 调用acquireQueued(Node node,int arg)方法,使得该 节点以“死循环”的方式获取同步状态
- 自旋时,首先判断其前驱节点为头节点且释放&是否成功获取同步状态,两个条件都成立,则将当前线程的节设置为头节点,如果不是,则利用LockSupport.park(this)将当前线程挂起 ,等待前驱节点释放唤醒自己,之后继续判断。
释放
- 调用入口方法release(arg)
- 调用模版方法tryRelease(arg)释放同步状态
- 利用LockSupport.unpark(currentNode.next.thread)唤醒后继节点(接获取的第五步)
2.2.3 共享锁的获取和释放流程
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态
获取锁
- 在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态
- tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是 tryAcquireShared(int arg)方法返回值大于等于0。
- 可以看到,在doAcquireShared(int arg)方法的自 旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。
释放锁
- 调用releaseShared(arg)模版方法释放同步状态
- 调用模版方法tryReleaseShard(arg)释放同步状态
- 如果释放成功,则遍历整个队列,利用LockSupport.unpark(nextNode.thread)唤醒所有后继节点
- 与独占式区别在于线程安全释放,通过循环和CAS保证,因为释放同步状态的操作会同时来自多个线程
2.2.4 独占锁和共享锁在实现上的区别
- 独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态
- 共享锁的同步状态>1,取值由上层同步组件确定
- 独占锁队列中头节点运行完成后释放它的直接后继节点
- 共享锁队列中头节点运行完成后释放它后面的所有节点
- 共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况
2.2.5 重入锁
重入锁指的是当前线程成功获取锁后,如果再次访问该临界区,则不会对自己产生互斥行为。Java中ReentrantLock和synchronized都是可重入锁,synchronized由JVM偏向锁实现可重入锁,ReentrantLock可重入性基于AQS实现。
重入锁的基本原理是判断上次获取锁的线程是否为当前线程(current == getExclusiveOwnerThread()
),如果是则可再次进入临界区,如果不是,则阻塞。
final boolean nonfairTryAcquire(int acquires) {//获取当前线程final Thread current = Thread.currentThread();//通过AQS获取同步状态int c = getState();//同步状态为0,说明临界区处于无锁状态,if (c == 0) {//修改同步状态,即加锁if (compareAndSetState(0, acquires)) {//将当前线程设置为锁的ownersetExclusiveOwnerThread(current);return true;}}//如果临界区处于锁定状态,且上次获取锁的线程为当前线程else if (current == getExclusiveOwnerThread()) {//则递增同步状态int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
如果是获取锁的线程再次请求,则将同步状态值进行增加并返回 true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放 同步状态时减少同步状态值
2.2.6 公平锁和非公平锁
对于非公平锁,只要CAS设置 同步状态成功,则表示当前线程获取了锁,而公平锁则不同
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {//此处为公平锁的核心,即判断同步队列中当前节点是否有前驱节点if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该 方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
2.2.7 读写锁
Java提供了一个基于AQS到读写锁实现ReentrantReadWriteLock
,该读写锁到实现原理是:将同步变量state按照高16位和低16位进行拆分,高16位表示读锁,低16位表示写锁。
写锁的获取与释放 写锁是一个独占锁,所以我们看一下ReentrantReadWriteLock中tryAcquire(arg)的实现:
protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread();int c = getState();int w = exclusiveCount(c);if (c != 0) {if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires);return true;}if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;}
上述代码的处理流程已经非常清晰:
- 获取同步状态,并从中分离出低16为的写锁状态
- 如果同步状态不为0,说明存在读锁或写锁
- 如果存在读锁(c !=0 && w == 0),则不能获取写锁(保证写对读的可见性)
- 如果当前线程不是上次获取写锁的线程,则不能获取写锁(写锁为独占锁)
- 如果以上判断均通过,则在低16为写锁同步状态上利用CAS进行修改(增加写锁同步状态,实现可重入) 将当前线程设置为写锁的获取线程
写锁的释放过程与独占锁基本相同:
protected final boolean tryRelease(int releases) {if (!isHeldExclusively())throw new IllegalMonitorStateException();int nextc = getState() - releases;boolean free = exclusiveCount(nextc) == 0;if (free)setExclusiveOwnerThread(null);setState(nextc);return free;}
在释放的过程中,不断减少读锁同步状态,只为同步状态为0时,写锁完全释放。
读锁的获取与释放
读锁是一个共享锁,获取读锁的步骤如下:
- 获取当前同步状态
- 计算高16为读锁状态+1后的值
- 如果大于能够获取到的读锁的最大值,则抛出异常
- 如果存在写锁并且当前线程不是写锁的获取者,则获取读锁失败
- 如果上述判断都通过,则利用CAS重新设置读锁的同步状态
读锁的释放步骤与写锁类似,即不断的释放写锁状态,直到为0时,表示没有线程获取读锁。
三、使用AQS与Lock自定义一个锁
class Mutex implements Lock { // 静态内部类,自定义同步器 private static class Sync extends AbstractQueuedSynchronizer { // 是否处于占用状态 protected boolean isHeldExclusively() { return getState() == 1; } // 当状态为0的时候获取锁 public boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 释放锁,将状态设置为0 protected boolean tryRelease(int releases) { if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true; } // 返回一个Condition,每个condition都包含了一个condition队列 Condition newCondition() { return new ConditionObject(); } } // 仅需要将操作代理到Sync上即可 private final Sync sync = new Sync();public void lock() { sync.acquire(1); }public boolean tryLock() { return sync.tryAcquire(1); }public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); }public boolean isLocked() { return sync.isHeldExclusively(); }public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException{ return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } }
流程:
- 这个自定义类Mutex首先实现了Lock接口,
- 内部静态类Sync继承了AQS抽象类,并重写了独占式的tryAcquire和tryRelease方法,
- 接着Mutex实例化Sync内部类,
- Mutex类重写Lock接口的方法,如lock、tryLock、unlock等方法,具体实现是通过调用Sync类中的重写的方法(tryAcquire)以及模板方法(acquire)等
- 用户使用Mutex时调用Mutex提供的方法,在Mutex的实现中,调用同步器的模板方法acquire(int args)
【<锁的属性>】
[1] 为什么要加锁之临界区
多个线程访问共享资源时,在多个线程对共享资源读写操作时发生指令交错,就会出现问题。一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
[2] 实现一个锁需要考虑哪些方面?
实现一个锁,主要需要考虑2个问题
- 如何线程安全的修改锁状态位?
- 得不到锁的线程,如何排队?
【<锁升级>】
[1] Java对象头
我们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 Klass Pointer(类型指针)
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在上面中我们知道了,synchronized
用的锁是存在Java对象头里的,那么具体是存在对象头哪里呢?答案是:存在锁对象的对象头的Mark Word中,那么MarkWord在对象头中到底长什么样,它到底存储了什么呢?
在64位的虚拟机中:
在32位的虚拟机中:
https://stackoverflow.com/questions/26357186/what-is-in-java-object-header
[2] Mark word 结构
默认(Normal):hashcode(地址码);age(分代中的年龄);biased—lock(是不是偏向锁),01(加锁状态:表示没有和任何monitor关联)
重量级锁(Normal):ptr_to_heavyweight_moniter(指向锁的地址),10(加锁状态:已经与monitor关联)轻轻量级锁(Normal):ptr_to_lock_record(锁记录的地址),00(加锁状态:轻量级锁)
锁状态 | 存储内容 | 标志位 |
---|---|---|
无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量的指针 | 11 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y1RfvPQD-1596599154320)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200714201707252.png)]
[3] Monitor
Monitor 被翻译为监视器或管程 。每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BJLqTIub-1596599154320)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200714200132363.png)]
-
刚开始 Monitor 中 Owner 为 null
-
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,
-
在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入
EntryList BLOCKED (Monitor中只能有一个 Owner)
-
Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
-
图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲
wait-notify 时会分析
https://www.jianshu.com/p/c3313dcf2c23
① owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
② _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
③ _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中
④ _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则
[4] synchronized锁升级
无锁
不通过阻塞的方式来访问并修改资源。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功,CAS就是一个无锁的形式。
偏向锁(可关闭)
偏向锁是指一段同步代码仅仅被一个线程所访问时,那么会给对象加偏向锁。简单来说:第一次使用时,使用CAS将线程ID存储到mark word上,之后测试这个锁是自己的就不需要再竞争了。
加锁:
- 访问Mark Word中偏向锁的标识,如果锁标志位(biased_lock)是为0,则说明无锁。将对象头的markword当前线程ID,并执行同步代码块。
- 如果锁标志位是否为01,为可偏向状态,则测试将Mark Word中线程ID是否指向当前线程 。
- 如果是,执行同步代码;
- 如果否,则通过CAS操作竞争偏向锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行同步代码块,如果竞争失败,则说明有其他线程在使用,执行撤销操作。
撤销:有竞争时进行撤销,一旦有了竞争就升级为轻量级锁,他会当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,撤销偏向锁的时候会导致stop the word操作。
关闭:有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;
- 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 关闭偏向锁:-XX:-UseBiasedLocking
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7a2WeJRU-1596599154320)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715094211946.png)]
轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
加锁:这里涉及到一个锁记录的概念,线程在执行同步代码块之前,每个的栈桢都新建一个锁记录的结构,提前将对象的markword复制到锁记录中,官方称为displaced mark word。然后尝试使用CAS(displaced mark word==markword)将对象头的Markword替换为指向锁记录的指针。
- 如果成功,则执行代码块
- 如果失败,则使用CAS自旋操作来获取锁
解锁:使用原子操作的CAS将displaced mark word替换回对象头
-
如果成功,则表示没有竞争发生
如果失败,则表明当前存在竞争,则升级
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VaY9x140-1596599154320)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715100154850.png)]
重量级锁
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。详细见monitor。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zy60VhMh-1596599154321)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715100210104.png)]
[6] synchronized锁对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到索竞争的线程,使用自旋会消耗CPU | 追求响应速度,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
【CAS】
[1] 什么是CAS?
**CAS,英文全称compare-and-swap,即比较并交换,是一种乐观锁的思想。**CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。
工作内存首先会读取当前内存中将要修改的值,即预期值。然后计算结果值,在修改结果值之前将当前工作内存中的值与预期值对比,如果相等则修改并返回ture,不相等,返回false。(自旋状态失败下:从读取预期值开始重复上述步骤。)
cas里面比较E和当前新值相等后,在修改前有被其他线程修改了怎么办? 即这里怎么保证这两步之间的原子性的?
有个lock指令(lock cmpxchg)保证在CPU操作一个格子时其他CPU不能操作它,再底层是lock指令后时候锁定一个北桥电信号,并非总线指令。
https://www.cnblogs.com/yungyu16/p/13200626.html
[2] 什么是自旋锁?
cas是一种乐观锁机制,cas可以不用自旋机制,失败也可以直接返回false。只是一般应用场景下,cas都会带有重试机制(while和for实现空转,不断尝试)。
//CAS模拟实现public class SimulatedCAS {private int value;public synchronized int get() {return value;}public synchronized int compareAndSwap(int expectedValue, int newValue) {int oldValue = value;if (oldValue == expectedValue) {value = newValue;}return oldValue;}public synchronized boolean compareAndSet(int expectedValue, int newValue) {return (expectedValue == compareAndSwap(expectedValue, newValue));}}
[3] CAS可能出现的问题有什么?
- ABA 问题:在读取预期值到将其与现在的值比较的在这段时间里,另一个线程将旧的预期值改为其他值,然后又改回 A(即A->B->A)。那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 “ABA” 问题。
ABA问题的解决思路就是使用版本号机制,每次更新就把版本号加1,那么原来的A->B->A就会变为A1->B2->A3,根据**版本号(时间戳,布尔值)**二次确认值是否被修改过。
JDK 1.5 以后的AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
**ABA可能产生问题,也可能不产生问题。**https://blog.csdn.net/superfjj/article/details/106465175
- 循环时间长开销大:自旋 CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一:它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二:它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。
- 只能保证一个共享变量的原子操作:CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
但是从 JDK 1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用 AtomicReference 类把多个共享变量合并成一个共享变量来操作(比如i=2,j=a,合并一下ij=2a)。
[4] 哪里有用到CAS?
- ReenterLock内部的AQS
- 各种Atomic开头的原子类
- synchronized中轻量级锁的加锁和解锁都用到了 CAS 操作。
[5] CAS 和volatile如何实现无锁并发?
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思。
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。
【<死锁>】
原文链接:https://blog.csdn.net/hd12370/article/details/82814348
[1] 什么是死锁
死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
[2] 产生死锁的原因?
可归结为如下两点:
a. 竞争资源
系统中的资源可以分为两类:
- 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
- 另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
b. 进程间推进顺序非法
若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁
例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁
[3] 死锁的产生必须满足的四个必要条件
死锁是最常见的一种线程活性故障。死锁的起因是多个线程之间相互等待对方而被永远暂停(处于Runnable)。死锁的产生必须满足如下四个必要条件:
- 资源互斥:一个资源每次只能被一个线程使用。即进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放,
- 不剥夺条件:线程已经获得的资源,在未使用完之前,不能强行剥夺,即只能由获得该资源的进程自己来释放(只能是主动释放)。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
总结:死锁只能发生在像 synchronized 的同步代码块中,一个资源只能被一个线程占用。而且占用资源的锁不会因为请求其他资源失败而主动释放当前锁,已经持有的锁也不能被其他进程剥夺。另外还需要形成一个循环等待的结构,否则它申请的锁有可能被释放。
[4] 解决死锁的基本方法
- 预防死锁----- 确保系统永远不会进入死锁状态
产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。
a、破坏“占有且等待”条件
方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。
优点:简单易实施且安全。
缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。
使进程经常发生饥饿现象。
方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。
b、破坏“不可抢占”条件
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。
该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。
c、破坏“循环等待”条件
可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。如图所示:
这样虽然避免了循环等待,但是这种方法是比较低效的,资源的执行速度回变慢,并且可能在没有必要的情况下拒绝资源的访问,比如说,进程c想要申请资源1,如果资源1并没有被其他进程占有,此时将它分配个进程c是没有问题的,但是为了避免产生循环等待,该申请会被拒绝,这样就降低了资源的利用率
————————————————
原文链接:https://blog.csdn.net/guaiguaihenguai/article/details/80303835
- 避免死锁----- 在使用前进行判断,只允许不会产生死锁的进程申请资源
预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
死锁避免是利用额外的检验信息,在分配资源时判断是否会出现死锁,只在不会出现死锁的情况下才分配资源。
两种避免办法:
1、如果一个进程的请求会导致死锁,则不启动该进程
2、如果一个进程的增加资源请求会导致死锁 ,则拒绝该申请。
银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。
a、银行家算法的相关数据结构
可利用资源向量Available:用于表示系统里边各种资源剩余的数目。由于系统里边拥有的资源通常都是有很多种(假设有m种),所以,我们用一个有m个元素的数组来表示各种资源。数组元素的初始值为系统里边所配置的该类全部可用资源的数目,其数值随着该类资源的分配与回收动态地改变。
最大需求矩阵Max:用于表示各个进程对各种资源的额最大需求量。进程可能会有很多个(假设为n个),那么,我们就可以用一个nxm的矩阵来表示各个进程多各种资源的最大需求量
分配矩阵Allocation:顾名思义,就是用于表示已经分配给各个进程的各种资源的数目。也是一个nxm的矩阵。
需求矩阵Need:用于表示进程仍然需要的资源数目,用一个nxm的矩阵表示。系统可能没法一下就满足了某个进程的最大需求(通常进程对资源的最大需求也是只它在整个运行周期中需要的资源数目,并不是每一个时刻都需要这么多),于是,为了进程的执行能够向前推进,通常,系统会先分配个进程一部分资源保证进程能够执行起来。那么,进程的最大需求减去已经分配给进程的数目,就得到了进程仍然需要的资源数目了。
银行家算法通过对进程需求、占有和系统拥有资源的实时统计,确保系统在分配给进程资源不会造成死锁才会给与分配。
死锁避免的优点:不需要死锁预防中的抢占和重新运行进程,并且比死锁预防的限制要少。
死锁避免的限制:
必须事先声明每个进程请求的最大资源量
考虑的进程必须无关的,也就是说,它们执行的顺序必须没有任何同步要求的限制
分配的资源数目必须是固定的。
在占有资源时,进程不能退出
————————————————
原文链接:https://blog.csdn.net/guaiguaihenguai/article/details/80303835
如何避免死锁的发生?
- **粗锁法:**使用一个粒度粗的锁来消除“请求与保持条件”,缺点是会明显降低程序的并发性能并且会导致资源的浪费。
- 锁排序法:(必须回答出来的点)指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作
在多线程条件下,如何避免死锁?
通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法。
- 使用显式锁中的**ReentrantLock.try(long,TimeUnit)**来申请锁。
- 检测死锁
首先为每个进程和每个资源指定一个唯一的号码;
然后建立资源分配表和进程等待表
- 解除死锁
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
————————————————
原文链接:https://blog.csdn.net/Beyond_2016/article/details/81363361
[5] 说下对悲观锁和乐观锁的理解?
- 悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如:行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
- 乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
- 两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
[6] 乐观锁常见的两种实现方式是什么?
乐观锁一般会使用版本号机制或者 CAS 算法实现。
-
版本号机制
一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加 1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
-
CAS 算法
即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数:
1、需要读写的内存值 V
2、进行比较的值 A
3、拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
【<线程锁死>】
[1] 什么是线程锁死
线程锁死是另一种常见的线程活性故障,与线程死锁不可以混为一谈。线程锁死的定义如下:
线程锁死是指等待线程由于唤醒其所需的条件永远无法成立,或者其他线程无法唤醒这个线程而一直处于非运行状态(线程并未终止)导致其任务 一直无法进展。
线程死锁和线程锁死的外部表现是一致的,即故障线程一直处于非运行状态使得其所执行的任务没有进展。但是锁死的产生条件和线程死锁不一样,即使产生死锁的4个必要条件都没有发生,线程锁死仍然可能已经发生。
[2] 线程锁死分为哪两种
- 信号丢失锁死:
信号丢失锁死是因为没有对应的通知线程来将等待线程唤醒,导致等待线程一直处于等待状态。
典型例子是等待线程在执行Object.wait( )/Condition.await( )前没有对保护条件进行判断,而此时保护条件实际上可能已经成立,此后可能并无其他线程更新相应保护条件涉及的共享变量使其成立并通知等待线程,这就使得等待线程一直处于等待状态,从而使其任务一直无法进展。
- 嵌套监视器锁死:
嵌套监视器锁死是由于嵌套锁导致等待线程永远无法被唤醒的一种故障。
比如一个线程,只释放了内层锁Y.wait(),但是没有释放外层锁X; 但是通知线程必须先获得外层锁X,才可以通过 Y.notifyAll()来唤醒等待线程,这就导致出现了嵌套等待现象。
[3] 活锁
活锁是一种特殊的线程活性故障。当一个线程一直处于运行状态,但是其所执行的任务却没有任何进展称为活锁。比如,一个线程一直在申请其所需要的资源,但是却无法申请成功。
[3] 线程饥饿
线程饥饿是指线程一直无法获得其所需的资源导致任务一直无法运行的情况。线程调度模式有公平调度和非公平调度两种模式。在线程的非公平调度模式下,就可能出现线程饥饿的情况。
[5] 线程活性故障总结
- 线程饥饿发生时,如果线程处于可运行状态,也就是其一直在申请资源,那么就会转变为活锁
- 只要存在一个或多个线程因为获取不到其所需的资源而无法进展就是线程饥饿,所以线程死锁其实也算是线程饥饿
【<多线程常用方法>】
[1] start 与 run
https://www.cnblogs.com/agilestyle/p/11421515.html
start:用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。
run :run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
[2] sleep 与 yield
sleep()
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield():
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
[3] sleep 和 wait
- **sleep方法:**是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
- wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。
[4] Daemon
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
package cn.mycast;import lombok.extern.slf4j.Slf4j;import static cn.itcast.n2.util.Sleeper.sleep;@Slf4j(topic= "c.守护线程")
public class 守护线程 {public static void main(String[] args) {//方法1Thread t1 = new Thread(()-> {sleep(2);log.debug("hello");});t1.setDaemon(true);t1.start();sleep(1);log.debug("运行结束...");}
}
[5] join和yield
**join 方法:**当前线程调用,则其它线程全部停止,等待当前线程执行完毕,接着执行(即调用此方法的线程必须加入)。
yield 方法:该方法使得线程放弃当前分得的 CPU 时间。但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
所以,该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
[6] wait() notify() notifyAll()
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
注意:
- 它们都属于 Object 的一部分,而不属于 Thread。
- 只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。
- 使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
[7] await() signal() signalAll()
await() signal() signalAll()–多线程的协调。java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
使用 Lock 来获取一个 Condition 对象。
[8] InterruptedException
InterruptedException–中断机制,通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
try {Thread.sleep(2000);System.out.println("Thread run");} catch (InterruptedException e) {e.printStackTrace();}
[9] interrupted()
interrupted()–中断机制,如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
[10] Executor
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。主要有三种 Executor:
- CachedThreadPool:一个任务创建一个线程;
- FixedThreadPool:所有任务只能使用固定大小的线程;
- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
[11] CopyOnWriteArrayList
CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,修改完毕后,再原子性时候修改共享访问的变量,让它指向新的对象。更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。
【ReentrantLock】
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
ReentrantLock ,re: 可重新 entrant:进入 Lock:锁 ,所以他就是可冲入锁
[1] ReentrantLock简介
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。相对于 synchronized 它具备如下特点:
- 可重入():是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
- 可中断:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量:synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的。
public class LockExample {private Lock lock = new ReentrantLock();public void func() {lock.lock();try {for (int i = 0; i < 10; i++) {System.out.print(i + " ");}} finally {lock.unlock(); // 确保释放锁,从而避免发生死锁。}}
}
[2] ReentrantLock方法
-
加锁
ReentrantLock 类提供了最基本的加锁和解锁方法:
public void lock();
class Counter {private static int counter = 0;private static ReentrantLock lock = new ReentrantLock();public static int getCounter() {return counter;}public static void increase() {try {lock.lock();counter++;} finally {lock.unlock();}}private Counter() {}; }
这个方法保证了线程安全,他和 synchronized 关键字实现了相同的效果:
class Counter {private static int counter = 0;public static int getCounter() {return counter;}public synchronized static void increase() {counter++;}private Counter() {}; }
显然,synchronized 关键字的实现更为简洁和清晰,同时,如果 ReentrantLock 忘记调用 unlock 方法将会造成死锁,这是必须要注意的一点
因此,如果仅仅是想要进行上面代码中这样的加锁和解锁,synchronized 还是最好的选择
-
可重入
默认可重入
-
可中断
public void lockInterruptibly() // 可中断锁,调用线程 interrupt 方法,则锁方法抛出 InterruptedException
-
可以设置超时时间
public boolean tryLock(long timeout, TimeUnit unit) // 尝试获取锁,最多等待 timeout 时长
-
可以设置为公平锁:
使用 synchronized 锁是不保证等待的线程获取到锁的顺序的,这就是非公平锁,除了默认的非公平锁构造方法外,ReentrantLock 还提供了一个带有 boolean 参数的构造方法:
public ReentrantLock(boolean fair);
如果传入参数为 true,则会创建公平锁,所谓的公平锁,就是保证了先进入等待的线程一定先获取到锁
可以通过 isFair 方法查询 ReentrantLock 对象是否是公平锁:
public final boolean isFair();
-
支持多个条件变量
通过 newCondition 方法,可以创建出 Condition 对象
Condition 接口提供了如下的方法:
void await(); // 可被中断的等待
boolean await(long time, TimeUnit unit); // 最多等待 time 时长的可中断等待
long awaitNanos(long nanosTimeout); // 最多等待 nanosTimeout 毫秒的可中断等待
boolean awaitUntil(Date deadline); // 等待直到指定时间的可中断等待
void awaitUninterruptibly(); // 不可中断的等待
void signal(); // 唤醒一个线程
void signalAll(); // 唤醒所有等待中的线程
上面的五个等待方法中,除了 awaitUninterruptibly 方法,其他四个都可以被 interrupt 方法中断,而 signal 和 signalAll 方法可以中断上述所有等待方法
但是,signal 和 signalAll 方法只能唤醒通过当前 Condition 对象调用过等待方法的线程
基于上述特性,我们可以精准的控制让某个指定的线程被唤醒,而 Object 的 notify、notifyAll 方法的唤醒则是随机的,同一个 ReentrantLock 每次调用 newCondition 方法都将获得不同的 Condition 对象
- 查询接口
int getHoldCount(); // 获取当前线程持有该锁的次数
boolean isHeldByCurrentThread(); // 判断当前线程是否持有该锁
boolean isLocked(); // 获取锁状态是否为加锁状态
boolean isFair(); // 当前锁是否是公平锁
Thread getOwner(); // 获取持有锁的线程
boolean hasQueuedThreads(); // 判断当前锁是否有线程在等待
boolean hasQueuedThread(Thread thread); // 判断指定线程是否在等待该锁
int getQueueLength(); // 获取正在等待该锁的线程数量
boolean hasWaiters(Condition condition); // 判断是否有线程等待在该 Condition 对象上
int getWaitQueueLength(Condition condition); // 获取等待在该 Condition 对象的线程数
[3] ReentrantLock实战
加锁及其解锁:
class Counter {private static int counter = 0;private static ReentrantLock lock = new ReentrantLock();
public static int getCounter() {return counter;
}public static void increase() {try {lock.lock();counter++;} finally {lock.unlock();}
}private Counter() {};}
https://baijiahao.baidu.com/s?id=1648624077736116382&wfr=spider&for=pc
可重入:可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {method1();
}
public static void method1() {lock.lock();try {log.debug("execute method1");method2();} finally {lock.unlock();}
}
public static void method2() {lock.lock();try {log.debug("execute method2");method3();} finally {lock.unlock();}
}
public static void method3() {lock.lock();try {log.debug("execute method3");} finally {lock.unlock();}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BNjXr1no-1596599154323)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716080920156.png)]
可打断:
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090log.debug("启动...");try {lock.lockInterruptibly();} catch (InterruptedException e) {e.printStackTrace();log.debug("等锁的过程中被打断");return;}try {log.debug("获得了锁");} finally {lock.unlock();}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {sleep(1);t1.interrupt();log.debug("执行打断");
} finally {lock.unlock();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LQHH7OAp-1596599154324)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716080945053.png)]
可超时:
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {log.debug("启动...");if (!lock.tryLock()) {log.debug("获取立刻失败,返回");return;}try {log.debug("获得了锁");} finally {lock.unlock();}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {sleep(2);
} finally {lock.unlock();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECeWKX59-1596599154325)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716080851608.png)]
公平锁:
ReentrantLock lock = new ReentrantLock(false);
lock.lock();
for (int i = 0; i < 500; i++) {new Thread(() -> {lock.lock();try {System.out.println(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {System.out.println(Thread.currentThread().getName() + " start...");lock.lock();try {System.out.println(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}
}, "强行插入").start();
lock.unlock()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TrPSxPZW-1596599154325)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716080827688.png)]
条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
synchronized 是那些不满足条件的线程都在一间休息室等消息
而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤
醒
使用要点:
await 前需要获得锁
await 执行后,会释放锁,进入 conditionObject 等待
await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
竞争 lock 锁成功后,从 await 后继续执行
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {new Thread(() -> {try {lock.lock();while (!hasCigrette) {try {waitCigaretteQueue.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("等到了它的烟");} finally {lock.unlock();}}).start();new Thread(() -> {try {lock.lock();while (!hasBreakfast) {try {waitbreakfastQueue.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("等到了它的早餐");} finally {lock.unlock();}}).start();sleep(1);sendBreakfast();sleep(1);sendCigarette();
}
private static void sendCigarette() {lock.lock();try {log.debug("送烟来了");hasCigrette = true;waitCigaretteQueue.signal();} finally {lock.unlock();}
}
private static void sendBreakfast() {lock.lock();try {log.debug("送早餐来了");hasBreakfast = true;waitbreakfastQueue.signal();} finally {lock.unlock();}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8OFFylM4-1596599154326)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716080757372.png)]
[4] ReentrantLock原理
如何实现一个锁?
实现一个锁,主要需要考虑2个问题
- 如何线程安全的修改锁状态位?
- 得不到锁的线程,如何排队?
带着这2个问题,我们看一下JUC中的ReentrantLock是如何做的?
ReentrantLock类的大部分逻辑,都是其均继承自AQS的内部类Sync实现的
如何线程安全的修改锁状态位?
锁状态位的修改主要通过,内部类Sync实现的,们可以发现线程安全的关键在于:volatile变量和CAS原语的配合使用
public abstract class AbstractQueuedSynchronizer{//锁状态标志位:volatile变量(多线程间通过此变量判断锁的状态)private volatile int state;protected final int getState() {return state;}protected final void setState(int newState) {state = newState;}}abstract static Sync extends AbstractQueuedSynchronizer {final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();//volatile读,确保了锁状态位的内存可见性int c = getState();//锁还没有被其他线程占用if (c == 0) {//此时,如果多个线程同时进入,CAS操作会确保,只有一个线程修改成功if (compareAndSetState(0, acquires)) {//设置当前线程拥有独占访问权setExclusiveOwnerThread(current);return true;}}//当前线程就是拥有独占访问权的线程,即锁重入else if (current == getExclusiveOwnerThread()) {//重入锁计数+1int nextc = c + acquires;if (nextc < 0) //溢出throw new Error("Maximum lock count exceeded");//只有获取锁的线程,才能进入此段代码,因此只需要一个volatile写操作,确保其内存可见性即可setState(nextc);return true;}return false;}//只有获取锁的线程才会执行此方法,因此只需要volatile读写确保内存可见性即可protected final boolean tryRelease(int releases) {//锁计数器-1int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;//锁计数器为0,说明锁被释放if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}}
得不到锁的线程,如何排队?
JUC中锁的排队策略,是基于CLH队列的变种实现的。因此,我们先看看啥是CLH队列
如上图所示,获取不到锁的线程,会进入队尾,然后自旋,直到其前驱线程释放锁。
这样做的好处:假设有1000个线程等待获取锁,锁释放后,只会通知队列中的第一个线程去竞争锁,减少了并发冲突。(ZK的分布式锁,为了避免惊群效应,也使用了类似的方式:获取不到锁的线程只监听前一个节点)
为什么说JUC中的实现是基于CLH的“变种”,因为原始CLH队列,一般用于实现自旋锁。而JUC中的实现,获取不到锁的线程,一般会时而阻塞,时而唤醒。
[5] ReentrantLock如何实现可重入锁?
可重入锁的实现原理:
如果当前锁的状态不为0,表示有线程占有该锁。再判断如果当前线程就是占有这个锁的线程,修改当前线程的同步状态值,同步状态加1,这样就实现了可重入。
每次重新获取都会对同步状态进行加一的操作,那么释放的时候处理思路是怎样的了?释放锁会调用unlock方法,内部有一个tryRelease方法,每释放一次锁同步状态减1,只有当同步状态为0时,锁成功被释放,返回true。
static final class NonfairSync extends Sync {// ...// Sync 继承过来的方法, 方便阅读, 放在此处final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入else if (current == getExclusiveOwnerThread()) {// state++int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}// Sync 继承过来的方法, 方便阅读, 放在此处protected final boolean tryRelease(int releases) {// state-- int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// 支持锁重入, 只有 state 减为 0, 才释放成功if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}
}
[6] ReentrantLock如何实现公平锁和非公平锁?
总结:公平锁和非公平锁**(默认)**只有两处不同:
非公平:
- 调用lock()方法时,首先去通过CAS尝试设置锁资源的state变量,如果设置成功,则设置当前持有锁资源的线程为当前请求线程
- 调用tryAcquire方法时,首先获取当前锁资源的state变量,如果为0,则通过CAS去尝试设置state,如果设置成功,则设置当前持有锁资源的线程为当前请求线程
以上两步都属于插队现象,可以提高系统吞吐量
公平:
1.调用lock()方法时,不进行CAS尝试
2.调用tryAcuqire方法时,首先获取当前锁资源的state变量,如果为0,则判断该节点是否是头节点可以去获取锁资源,如果可以才通过CAS去尝试设置state
上面通过判断该线程是否是队列的头结点,从而保证公平性
public ReentrantLock() {sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}
公平锁的 lock 方法:
static final class FairSync extends Sync {final void lock() {acquire(1);}// AbstractQueuedSynchronizer.acquire(int arg)public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
}
非公平锁的 lock 方法:
static final class NonfairSync extends Sync {final void lock() {// 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}// AbstractQueuedSynchronizer.acquire(int arg)public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}
}
/*** Performs non-fair tryLock. tryAcquire is implemented in* subclasses, but both need nonfair try for trylock method.*/
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}
[7] 条件变量Condition实现原理?
Condition的作用和Object.wait()和Object.notify()的作用相同,可以使当前线程阻塞和唤醒。只不过condition需要与reentrantlock配合使用,而wait/notify需要与snychronized配合使用。
通过Lock接口(重入锁实现了这一接口)的new Condition()方法可以生成一个与当前重入锁绑定的Condition实例,每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject。
condition常用的方法:
- await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()或signalAll()方法时,线程会重新获得锁并继续执行。或者当其他线程被中断时,也能跳出等待。这和Object.wait()很相似。
- awaitUninterruptibly()与await()方法基本相同,但他并不会在中断过程中响应中断。
- signal()方法用于唤醒一个在等待中的线程。相对的signalAll()方法会唤醒所有在等待中的线程。这和Object.notifyAll()很类似。
await 流程:
public class ConditionObject implements Condition, java.io.Serializable {private static final long serialVersionUID = 1173984872572414699L;// 第一个等待节点private transient Node firstWaiter;// 最后一个等待节点private transient Node lastWaiter;public ConditionObject() { }// ㈠ 添加一个 Node 至等待队列private Node addConditionWaiter() {Node t = lastWaiter;// 所有已取消的 Node 从队列链表删除, 见 ㈡if (t != null && t.waitStatus != Node.CONDITION) {unlinkCancelledWaiters();t = lastWaiter;}// 创建一个关联当前线程的新 Node, 添加至队列尾部Node node = new Node(Thread.currentThread(), Node.CONDITION);if (t == null)firstWaiter = node;elset.nextWaiter = node;lastWaiter = node;return node;}// 唤醒 - 将没取消的第一个节点转移至 AQS 队列private void doSignal(Node first) {do {// 已经是尾节点了if ( (firstWaiter = first.nextWaiter) == null) {lastWaiter = null;}first.nextWaiter = null;} while (// 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 ㈢!transferForSignal(first) &&// 队列还有节点(first = firstWaiter) != null);}// 外部类方法, 方便阅读, 放在此处// ㈢ 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功final boolean transferForSignal(Node node) {// 如果状态已经不是 Node.CONDITION, 说明被取消了if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;// 加入 AQS 队列尾部Node p = enq(node);int ws = p.waitStatus;if (// 上一个节点被取消ws > 0 ||// 上一个节点不能设置状态为 Node.SIGNAL!compareAndSetWaitStatus(p, ws, Node.SIGNAL) ) {// unpark 取消阻塞, 让线程重新同步状态LockSupport.unpark(node.thread);}return true;}// 全部唤醒 - 等待队列的所有节点转移至 AQS 队列
北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090private void doSignalAll(Node first) {lastWaiter = firstWaiter = null;do {Node next = first.nextWaiter;first.nextWaiter = null;transferForSignal(first);first = next;} while (first != null);}// ㈡private void unlinkCancelledWaiters() {// ...}// 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁public final void signal() {if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignal(first);}// 全部唤醒 - 必须持有锁才能唤醒, 因此 doSignalAll 内无需考虑加锁public final void signalAll() {if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignalAll(first);}// 不可打断等待 - 直到被唤醒public final void awaitUninterruptibly() {// 添加一个 Node 至等待队列, 见 ㈠Node node = addConditionWaiter();// 释放节点持有的锁, 见 ㈣int savedState = fullyRelease(node);boolean interrupted = false;// 如果该节点还没有转移至 AQS 队列, 阻塞while (!isOnSyncQueue(node)) {// park 阻塞LockSupport.park(this);// 如果被打断, 仅设置打断状态if (Thread.interrupted())interrupted = true;}// 唤醒后, 尝试竞争锁, 如果失败进入 AQS 队列if (acquireQueued(node, savedState) || interrupted)selfInterrupt();}北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090// 外部类方法, 方便阅读, 放在此处// ㈣ 因为某线程可能重入,需要将 state 全部释放final int fullyRelease(Node node) {boolean failed = true;try {int savedState = getState();if (release(savedState)) {failed = false;return savedState;} else {throw new IllegalMonitorStateException();}} finally {if (failed)node.waitStatus = Node.CANCELLED;}}// 打断模式 - 在退出等待时重新设置打断状态private static final int REINTERRUPT = 1;// 打断模式 - 在退出等待时抛出异常private static final int THROW_IE = -1;// 判断打断模式private int checkInterruptWhileWaiting(Node node) {return Thread.interrupted() ?(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :0;}// ㈤ 应用打断模式private void reportInterruptAfterWait(int interruptMode)throws InterruptedException {if (interruptMode == THROW_IE)throw new InterruptedException();else if (interruptMode == REINTERRUPT)selfInterrupt();}// 等待 - 直到被唤醒或打断public final void await() throws InterruptedException {if (Thread.interrupted()) {throw new InterruptedException();}// 添加一个 Node 至等待队列, 见 ㈠Node node = addConditionWaiter();// 释放节点持有的锁int savedState = fullyRelease(node);int interruptMode = 0;// 如果该节点还没有转移至 AQS 队列, 阻塞while (!isOnSyncQueue(node)) {// park 阻塞LockSupport.park(this);
北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090// 如果被打断, 退出等待队列if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// 退出等待队列后, 还需要获得 AQS 队列的锁if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;// 所有已取消的 Node 从队列链表删除, 见 ㈡if (node.nextWaiter != null) unlinkCancelledWaiters();// 应用打断模式, 见 ㈤if (interruptMode != 0)reportInterruptAfterWait(interruptMode);}// 等待 - 直到被唤醒或打断或超时public final long awaitNanos(long nanosTimeout) throws InterruptedException {if (Thread.interrupted()) {throw new InterruptedException();}// 添加一个 Node 至等待队列, 见 ㈠Node node = addConditionWaiter();// 释放节点持有的锁int savedState = fullyRelease(node);// 获得最后期限final long deadline = System.nanoTime() + nanosTimeout;int interruptMode = 0;// 如果该节点还没有转移至 AQS 队列, 阻塞while (!isOnSyncQueue(node)) {// 已超时, 退出等待队列if (nanosTimeout <= 0L) {transferAfterCancelledWait(node);break;}// park 阻塞一定时间, spinForTimeoutThreshold 为 1000 nsif (nanosTimeout >= spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);// 如果被打断, 退出等待队列if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;nanosTimeout = deadline - System.nanoTime();}// 退出等待队列后, 还需要获得 AQS 队列的锁if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;// 所有已取消的 Node 从队列链表删除, 见 ㈡if (node.nextWaiter != null)unlinkCancelledWaiters();// 应用打断模式, 见 ㈤if (interruptMode != 0)reportInterruptAfterWait(interruptMode);return deadline - System.nanoTime();}// 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanospublic final boolean awaitUntil(Date deadline) throws InterruptedException {// ...}// 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanospublic final boolean await(long time, TimeUnit unit) throws InterruptedException {// ...}// 工具方法 省略 ...
}
[8] 谈谈 synchronized 和 ReenTrantLock 的区别?
-
synchronized 是和 for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性:等待可中断、可实现公平锁、可实现选择性通知(锁可以绑定多个条件)、性能已不是选择标准。
-
synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API。synchronized 是依赖于 JVM 实现的,JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
- 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
- 性能:新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
- 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。
- 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
- 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象。
答:ReentrantLock是显示锁,其提供了一些内部锁不具备的特性,但并不是内部锁的替代品。显式锁支持公平和非公平的调度方式,默认采用非公平调度。
synchronized 内部锁简单,但是不灵活。显示锁支持在一个方法内申请锁,并且在另一个方法里释放锁。显示锁定义了一个tryLock()方法,尝试去获取锁,成功返回true,失败并不会导致其执行的线程被暂停而是直接返回false,即可以避免死锁**。**
【< volatile 关键字专题>】
[1] 谈一下你对 volatile 关键字的理解?
**答:**volatile 关键字是用来保证有序性和可见性的。
- 保证了不同线程对该变量操作的内存可见性;
- 禁止指令重排序。
我们所写的代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排序,CPU 也会做重排序的,这样做是为了减少流水线阻塞,提高 CPU 的执行效率。这就需要有一定的顺序和规则来保证,不然程序员自己写的代码都不知道对不对了,所以有 happens-before 规则,其中有条就是 volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作、有序性实现的是通过插入内存屏障来保证的。
解析:
- volatile 可以保证主内存和工作内存直接产生交互,进行读写操作,保证可见性
- volatile 仅能保证变量写操作的原子性,不能保证读写操作的原子性。
- volatile可以禁止指令重排序(通过插入内存屏障),典型案例是在单例模式中使用。
volatile变量的开销:
volatile不会导致线程上下文切换,但是其读取变量的成本较高,因为其每次都需要从高速缓存或者主内存中读取,无法直接从寄存器中读取变量。
[2]Volatile如何保证可见性和有序性?
https://blog.csdn.net/duzhe2905/article/details/106038681?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.compare&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.compare
https://blog.csdn.net/qq_35590091/article/details/106986536
1. 可见性
主内存与工作内存
java内存模型规定了所有的变量都存储在主内存。每条线程还有自己的工作内存,线程的工作内存中保存了被改线程使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量传递均需要通过主内存来完成。当多个线程操作的变量涉及到同一个主内存区域,将可能导致各自的工作线程数据不一致,这样就导致变量同步回主内存的时候可能冲突导致数据丢失。
原文链接:https://blog.csdn.net/y124675160/article/details/78310121
volatile修饰的共享变量进行写操作的时候多出一条带lock前缀的指令,lock前缀的指令在多核处理器下会引发两件事情
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但是操作完了不知道什么时候写回内存。而对声明了volatile关键字的变量进行写操作,JVM会向处理器发送一条lock前缀的指令,将这个变量所在的缓存行立即写回系统内存。并且为了保证各个处理器的缓存是一致的,实现了缓存一致性协议,各个处理通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,那么下次对这个数据进行操作,就会重新从系统内存中获取最新的值。对应JMM来说就是:
- Lock前缀的指令让线程工作内存中的值写回主内存中;
- 通过缓存一致性协议,其他线程如果工作内存中存了该共享变量的值,就会失效;
- 其他线程会重新从主内存中获取最新的值;
原文链接:https://blog.csdn.net/y124675160/article/details/78310121
2.有序性的
为了性能优化,JVM会在不改变数据依赖性的情况下,允许编译器和处理器对指令序列进行重排序,而有序性问题指的就是程序代码执行的顺序与程序员编写程序的顺序不一致,导致程序结果不正确的问题。而加了volatile修饰的共享变量,则通过内存屏障解决了多线程下有序性问题。
内次屏障分为以下4类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oKTKcESW-1596599154331)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200723232539965.png)]
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,下面是基于保守策略的JMM内存平展插入策略。
- 在每个volatile写操作的前面插入一个StoreStore屏障,可以保证前面普通的写操作已经对任意处理器可见。
- 在每个volatile写操作的后面插入一个StoreLoad屏障,可以保证前面普通的写操作已经对任意处理器可见。
- 在每个volatile读操作的后面插入一个LoadLoad屏障,确保前面的数据先于后面的指令写入工作内存。
- 在每个volatile读操作的后面插入一个LoadStore屏障,确保前面的数据先于后面的指令写入工作内存。
volatile在写操作前后插入了内存屏障后生成的指令序列,示意图如下:
volatile在读操作后面插入了内存屏障后生成的指令序列示意图如下:
[3] volatile在什么情况下可以替代锁?
volatile是一个轻量级的锁,适合多个线程共享一个状态变量,锁适合多个线程共享一组状态变量。可以将多个线程共享的一组状态变量合并成一个对象,用一个volatile变量来引用该对象,从而替代锁。
理解volatile和CAS配合使用原理
https://blog.csdn.net/liaoxiaolin520/article/details/93711623
【< synchronized专题>】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lsvo1GMZ-1596599154332)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200518105440632.png)]
[1] synchronized 关键字?
synchronized是Java中的一个关键字,通常用于多线程环境下,在同一时刻只允许有一个线程访问共享变量。它会根据情况,将锁升级为不同状态,如偏向锁(可以关掉),轻量级锁(无锁/自旋锁/自适应锁),重量级锁。重量级锁会调用操作系统层面的minotor,这时候获得不到线程的对象会被阻塞在队列中。这也是经常被称为一个重量级锁原因。
另外它可以保证原子性,可见性和有序性。支持可重入,但不可中断(Lock的tryLock方法是可以被中断的)。
内部锁底层实现:
- 进入时,执行monitorenter,将计数器+1,释放锁monitorexit时,计数器-1
- 当一个线程判断到计数器为0时,则当前锁空闲,可以占用;反之,当前线程进入等待状态
https://www.zhihu.com/question/57794716/answer/606126905
[2] synchronized 关键字使用场景
package synchronizedTest;public class SynchronizedTest {// 作用于方法上(或者静态方法上)public synchronized void test(){System.out.println("synchronized test!!!");}// 作用于代码块内public void testBlock(){synchronized (this) {System.out.println("synchronized test!!!");}}
}
//作用于一个类上
class ClassName {public void method() {synchronized(ClassName.class) {// todo}}}
- 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。 因为静态方法是属于类的而不属于对象的 。同样的, synchronized修饰的静态方法锁定的是这个类的所有对象,所有类用它都会有锁的效果。
- 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
同步关键字锁的是对象
[3] synchronized 内部字节码指令
synchronized 内部字节码指令可以保证出现异常时正常解锁。
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {synchronized (lock) {counter++;}
}
public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=10: getstatic #2 // <- lock引用 (synchronized开始)3: dup4: astore_1 // lock引用 -> slot 1 <解锁时候用>5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针 <c实现的>6: getstatic #3 // <- i9: iconst_1 // 准备常数 110: iadd // +111: putstatic #3 // -> i 14: aload_1 // <- lock引用 <解锁>15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList16: goto 24
//19-23: Exception table还在检测范围内检测异常,6 16 19如果6-16出现异常们就会到19行。19行到最后可以保证在异常发生时正常解锁19: astore_2 // e -> slot 2 20: aload_1 // <- lock引用21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList 22: aload_2 // <- slot 2 (e) 23: athrow // throw e24: returnException table: from to target type6 16 19 any19 22 19 anyLineNumberTable:line 8: 0line 9: 6line 10: 14line 11: 24LocalVariableTable:Start Length Slot Name Signature0 25 0 args [Ljava/lang/String;StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame */offset_delta = 19locals = [ class "[Ljava/lang/String;", class java/lang/Object ]stack = [ class java/lang/Throwable ]frame_type = 250 /* chop */offset_delta = 4
方法
[4] synchronized如何保证有序性、可见性、原子性?
https://blog.csdn.net/qq_35590091/article/details/106986641
1. 原子性
synchronized经过编译之后,对应的是class文件中的monitorenter和monitorexit这两个字节码指令。这两个字节码对应的内存模型的操作是lock(上锁)和unlock(解锁)。因为这两个操作之间运行的都是原子的(这个操作保证了变量为一个线程独占的,也就是说只有获得锁的线程才能够操作被锁定的内存区域),所synchronized也具有原子性。
这两个字节码都需要一个对象来作为锁。因此,
1、如果synchronized修饰的是实例方法,则会传入this作为参数,
2、如果修饰的是静态方法,则会传入class类对象作为参数。
3、如果只是一个同步块,那么锁就是括号里配置的对象。
执行monitorenter字节码时,如果这个对象没有被上锁,或者当前线程已经持有了该锁,那么锁的计数器会+1,而在执行monitorexit字节码时,锁的计数器会-1,当计数器为0时,锁被释放。如果获取对象的锁失败,那么该线程会被阻塞等待,直到之前把这个对象上锁的线程释放这个锁为止。
每个对象都有一个monitor(监视器)与之关联,所谓的上锁,就是获得对象的monitor的独占权(因为只用获得monitor才能访问这个对象)。执行monitorenter字节码的时候,线程就会尝试获得monitor的所有权,也就是尝试获得对象的锁。只有获得了monitor,才能进入同步块,或者执行同步方法。独占对象的本质是独占对象的monitor。
2. 可见性
**lock(上锁时)**清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值;
unlock(解锁):这个操作规定,放开对某个变量的锁的之前,需要把这个变量从缓存更新到主内存。
因此它也具有可见性。
3. 有序性
为什么synchronized无法禁止指令重排,却能保证有序性?因为在一个线程内部,他不管怎么指令重排,他都是as if serial的,也就是说单线程即使重排序之后的运行结果和串行运行的结果是一样的,是类似串行的语义。**而当线程运行到同步块时,会加锁,其他线程无法获得锁,也就是说此时同步块内的方法是单线程的,根据as if serial,可以认为他是有序的。**而指令重排序导致线程不安全是多线程运行的时候,不是单线程运行的时候,因此多线程运行时静止指令重排序也可以实现有序性,这就是volatile。
原子性 + 可见性 -> 有序性,即使内部重排序,也不会有影响,可以说是多线程的serif
[5] synchronized 关键字锁升级过程?
锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁,四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程。目的是为了提高获得锁和释放锁的效率。
偏向锁:大多数情况下,锁总是由同一个线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,偏向锁是一个可重入的锁。如果锁对象头的Mark Word里存储着指向当前线程的偏向锁,无需重新进行CAS操作来加锁和解锁。当有其他线程尝试竞争偏向锁时,持有偏向锁的线程(不处于活动状态)才会释放锁。偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定进而升级为轻量级锁。
对于同一时刻只有一个线程访问时,每次进入锁是检查是否是自己的锁,是则执行,不是则升级
轻量级锁:减少无实际竞争情况下,使用重量级锁产生的性能消耗。JVM会现在当前线程的栈桢中创建用于存储锁记录的空间 LockRecord,将对象头中的 Mark Word 复制到 LockRecord 中并将 LockRecord 中的 Owner 指针指向锁对象。然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,成功则当前线程获取到锁,失败则表示其他线程竞争锁当前线程则尝试使用自旋的方式获取锁。自旋获取锁失败则锁膨胀升级为重量级锁。
在少量线程访问同步代码快时,使用CAS操作实现无🔒,如果获取到则存入旧状态值,如果成功则使用自旋来获取锁,如果
重量级锁:通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实 现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。线程竞争不使用自旋,不会消耗CPU。但是线程会进入阻塞等待被其他线程被唤醒,响应时间缓慢。
会调用操作系统层面的moniter
[5] JVM 对 synchronized 的锁优化
[1. 自旋锁](https://cyc2018.github.io/CS-Notes/#/notes/Java 并发?id=自旋锁)
让不满足条件的线程等待一会看能不能获得锁,通过占用处理器的时间来避免线程切换带来的开销。自旋等待的时间或次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
[2. 锁消除](https://cyc2018.github.io/CS-Notes/#/notes/Java 并发?id=锁消除)
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:
public static String concatString(String s1, String s2, String s3) {return s1 + s2 + s3;
}Copy to clipboardErrorCopied
String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作:
public static String concatString(String s1, String s2, String s3) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString();
}Copy to clipboardErrorCopied
每个 append() 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除。
[3.锁粗化](https://cyc2018.github.io/CS-Notes/#/notes/Java 并发?id=锁粗化)
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。
[8] synchronized 和 volatile 的区别是什么?
-
volatile 本质是在告诉 JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
-
volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。
-
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
-
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
-
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
【< AQS>】
[1]什么是AQS
AQS 全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架。
AQS分为独占式同步状态获取与释放,共享式同步状态获取与释放,独占式超时获取同步状态。接下来我先简单介绍下第一种:独占式同步状态获取与释放。AQS从数据结构上说,通过一个int成员变量state来控制同步状态,内部的同步队列(FIFO的双向链表队列)完成 对同步状态(state)的管理。
state:当state = 0时,说明没有任何线程占有共享资源的锁; state = 1时,则说明有线程目前正在使用共享变量
具体实现为同步器的acquire方法,该方法对中断不敏感:
-
首先调用自定义同步器实现的tryAcquire(int arg)方法,尝试获取同步状态。
tryAcquire(int arg)方法,以独占式获取同步状态,实现该方法需要查询当前状态state,然后再使用CAS设置当前状态。无参方法tryAcquire()的作用是尝试的获得1个许可,如果getstate!=0,则说明,此时已经被占用无法获取,返回false,如果getstate==0,则说明,此时共享变量是空闲的,使用CAS(判断当前状态是否为0)设置锁状态为1,并返回ture。
-
如果获取成功,则直接退出,如果同步状态获取失败,则构造同步节点()并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,并阻塞当前线程。
同步节点:独占式node,同一时刻只有一个线程可以获得同步状态;
addWaiter(Node node):首先使用CAS尝试快速在队尾插入,如果入队失败了,则调用enq(),link
compareAndSetTail(pred, node):确保节点可以被线程安全的添加。如果多个线程同时使用添加节点,最终可能导致节点数量和顺序变化。compareAndSetTail会比较pred和tail是否指向同一个节点,如果是,才将tail更新为node。虽然当前线程在声明pred时,为pred赋值了tail,但tail可能会被其他线程改变,而当前线程的本地变量pred是不会感知到这个改变的。
enq(final Node node)方法,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。
-
然后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则节点中的线程则会被阻塞,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
在acquireQueued(final Node node,int arg)方法中,当前**线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态。**首先是维护了先进先出原则,再就是后面节点需要“死循环”来判断自己是否是头结点。
AQS核心思想:
- 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。内部通过一个int成员变量state来控制同步状态。
- 当state = 0时,说明没有任何线程占有共享资源的锁;
- state = 1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待,当然state也 可以继续执行+1操作,比如可重入锁。
- AQS同步器的实现依赖于内部的同步队列(FIFO的双向链表队列)完成 对同步状态(state)的管理。
- 当前线程获取锁(同步状态)失败时,AQS会将该线程以及相关等待信息包装成 一个节点(Node)并将其加入同步队列,同时会阻塞当前线程
- 当同步状态释放时,会将头结点head中的线程唤醒,让其尝试获取同步状态。简单来说,就是同步状态。
AQS实现:
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
AQS功能:
- 阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire
- 获取锁超时机制
- 通过打断取消机制
- 独占机制及共享机制
- 条件不满足时的等待机制
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
-
tryAcquire
-
tryRelease
-
tryAcquireShared
-
tryReleaseShared
-
isHeldExclusively
[2] LCK队列源码及其实现
我们来看看AbstractQueuedSynchronizer类中的acquire方法实现
public final void acquire(int arg) {//尝试获取锁if (!tryAcquire(arg) &&//获取不到,则进入等待队列,返回是否中断acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//如果返回中断,则调用当前线程的interrupt()方法selfInterrupt();}
1. 入队 :如果线程调用tryAcquire(其最终实现是调用上面分析过的nonfairTryAcquire方法)获取锁失败。调用addWaiter(Node.EXCLUSIVE)方法,将自己加入CLH队列的尾部。
-
tryAcquire(int arg)方法,以独占式获取同步状态,实现该方法需要查询当前状态state,然后再使用CAS设置当前状态。无参方法tryAcquire()的作用是尝试的获得1个许可,如果getstate!=0,则说明,此时已经被占用无法获取,返回false,如果getstate==0,则说明,此时共享变量是空闲的,使用CAS(判断当前状态是否为0)设置锁状态为1,并返回ture。
-
同步节点:独占式node,同一时刻只有一个线程可以获得同步状态;
-
addWaiter(Node node):首先使用CAS尝试快速在队尾插入,如果入队失败了,则调用enq(),link
-
compareAndSetTail(pred, node):确保节点可以被线程安全的添加。如果多个线程同时使用添加节点,最终可能导致节点数量和顺序变化。compareAndSetTail会比较pred和tail是否指向同一个节点,如果是,才将tail更新为node。虽然当前线程在声明pred时,为pred赋值了tail,但tail可能会被其他线程改变,而当前线程的本地变量pred是不会感知到这个改变的。
-
enq(final Node node)方法,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
private Node addWaiter(Node mode) {//线程对应的NodeNode node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;//尾节点不为空if (pred != null) {//当前node的前驱指向尾节点node.prev = pred;//将当前node设置为新的尾节点//如果cas操作失败,说明线程竞争if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}//lockfree的方式插入队尾enq(node);return node;}private Node enq(final Node node) {//经典的lockfree算法:循环+CASfor (;;) {Node t = tail;//尾节点为空if (t == null) { // Must initialize//初始化头节点if (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}
入队过程,入下图所示
1 T0持有锁时,其CLH队列的头尾指针为NULL
2 线程T1,此时请求锁,由于锁被T0占有。因此加入队列尾部。具体过程如下所示:
(1) 初始化头节点
(2) 初始化T1节点,入队,尾指针指向T1
3 此时如果有一个T10线程先于T1入队,则T1执行compareAndSetTail(t, node)会失败,然后回到for循环开始处,重新入队。
image.png
2. 由自旋到阻塞
入队后,调用acquireQueued方法,时而自旋,时而阻塞,直到获取锁(或被取消)。
- 在acquireQueued(final Node node,int arg)方法中,当前**线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态。**首先是维护了先进先出原则,再就是后面节点需要“死循环”来判断自己是否是头结点。
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();//其前驱是头结点,并且再次调用tryAcquire成功获取锁if (p == head && tryAcquire(arg)) {//将自己设为头结点setHead(node);p.next = null; // help GCfailed = false;//成功获取锁,返回return interrupted;}//没有得到锁时://shouldParkAfterFailedAcquire方法:返回是否需要阻塞当前线程//parkAndCheckInterrupt方法:阻塞当前线程,当线程再次唤醒时,返回是否被中断if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//修改中断标志位interrupted = true;}} finally {if (failed)//获取锁失败,则将此线程对应的node的waitStatus改为CANCELcancelAcquire(node);}}private void setHead(Node node) {head = node;node.thread = null;node.prev = null;}/*** 获取锁失败时,检查并更新node的waitStatus。* 如果线程需要阻塞,返回true。*/private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;//前驱节点的waitStatus是SIGNAL。if (ws == Node.SIGNAL)/* * SIGNAL状态的节点,释放锁后,会唤醒其后继节点。* 因此,此线程可以安全的阻塞(前驱节点释放锁时,会唤醒此线程)。*/return true;//前驱节点对应的线程被取消if (ws > 0) {do {//跳过此前驱节点node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/*此时,需要将前驱节点的状态设置为SIGNAL。* waitStatus must be 0 or PROPAGATE. Indicate that we* need a signal, but don't park yet. Caller will need to* retry to make sure it cannot acquire before parking.*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}//当shouldParkAfterFailedAcquire方法返回true,则调用parkAndCheckInterrupt方法阻塞当前线程private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}
自旋过程,入下图所示
image.png
image.png
image.png
image.png
image.png
然后线程T2,加入了请求锁的队列,尾指针后移。
image.png
image.png
image.png
终上所述,每个得不到锁的线程,都会讲自己封装成Node,加入队尾,或自旋或阻塞,直到获取锁(为简化问题,不考虑取消的情况)
3. 锁的释放
前文提到,T1,T2在阻塞之前,都回去修改其前驱节点的waitStatus=-1。这是为什么?
我们看下锁释放的代码,便一目了然
public final boolean release(int arg) {//修改锁计数器,如果计数器为0,说明锁被释放if (tryRelease(arg)) {Node h = head;//head节点的waitStatus不等于0,说明head节点的后继节点对应的线程,正在阻塞,等待被唤醒if (h != null && h.waitStatus != 0)//唤醒后继节点unparkSuccessor(h);return true;}return false;}private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling. It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);//后继节点Node s = node.next;//如果s被取消,跳过被取消节点if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)//唤醒后继节点LockSupport.unpark(s.thread);}
如代码所示,waitStatus=-1的作用,主要是告诉释放锁的线程:后面还有排队等待获取锁的线程,请唤醒他!
释放锁的过程,如图所示:
总结
实现锁的关键在于:
- 通过CAS操作与volatile变量互相配合,线程安全的修改锁标志位
- 基于CLH队列,实现锁的排队策略
[3] 独占式同步状态获取
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出,该方法代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cWwmnmpo-1596599154341)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200716092220731.png)]
上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式node,同一时刻只有一个线程可以获得同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
我们看下同步器的addWaiter和enq的实现代码如下:
上述代码通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加。
在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。
节点进入同步队列之后,就进入了一个自旋的过程(acquireQueued方法),每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程),如代码所示:
在acquireQueued(final Node node,int arg)方法中,当前**线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态。**首先是维护了先进先出原则,再就是后面节点需要“死循环”来判断自己是否是头结点。
独占式同步状态获取流程,也就是acquire(int arg)方法调用流程,如图所示:
下面看下如何释放释放同步状态的:
该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport(这里不做介绍)来唤醒处于等待状态的线程。
适当做个总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
[4] 共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态,该方法代码如下:
在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。可以看到,在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出.
与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以释放同步状态,该方法代码:
该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的,因为释放同步状态的操作会同时来自多个线程
[5] 独占式超时获取同步状态
通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。该方法提供了传统Java同步操作(比如synchronized关键字)所不具备的特性。
超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早通知,nanosTimeout计算公式为:nanosTimeout-=now-lastTime,其中now为当前唤醒时间,lastTime为上次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,需要继续睡眠nanosTimeout纳秒,反之,表示已经超时.具体代码这里就不贴出来了。大致的流程图如下:
独占式超时获取同步状态doAcquireNanos(int arg,long nanosTimeout)和独占式获取同步状态acquire(int args)在流程上非常相似,其主要区别在于未获取到同步状态时的处理逻辑。acquire(int args)在未获取到同步状态时,将会使当前线程一直处于等待状态,而doAcquireNanos(int arg,long nanosTimeout)会使当前线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会从等待逻辑中自动返回。
[5]AQS的思想
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-miMIorbJ-1596599154343)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200714235957417.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9YdmCiBV-1596599154343)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715000018721.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VVYXfkao-1596599154344)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715000051324.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNBa5kSQ-1596599154344)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715000112746.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WKrmRcZv-1596599154345)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715000131770.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wJVUGR5A-1596599154345)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200715000156543.png)]
【< J.U.C>】
[1] AQS
见专题
[2] ReentrantLock
见专题
[3] ReentrantReadWriteLock
https://www.cnblogs.com/xiaoxi/p/9140541.html
现实中有这样一种场景:**对共享资源有读和写的操作,且写操作没有读操作那么频繁。**在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁(ReadLock和WriteLock),一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁,描述如下:
- 线程进入读锁(共享)的前提条件:
没有其他线程的写锁,
没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
- 线程进入写锁(互斥)的前提条件:
没有其他线程的读锁
没有其他线程的写锁
- 而读写锁有以下六个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁
(4)不支持锁升级:为了保证内存可见性
(5) 不支持conditon函数
(6) 读写锁允许多个读线程访问,但是写线程时,不允许其他线程。
1. 读写状态的设计
如何用一个state值来表示读写两种状态呢?
同步状态在重入锁的实现中是表示被同一个线程重复获取的次数,即一个整型变量来维护,但是之前的那个表示仅仅表示是否锁定,而不用区分是读锁还是写锁。而读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。
读写锁对于同步状态的实现是在一个整形变量上通过“按位切割使用”:将变量切割成两部分,高16位表示读,低16位表示写。
假设当前同步状态值为S,get和set的操作如下:
(1)获取写状态:
S&0x0000FFFF:将高16位全部抹去
(2)获取读状态:
S>>>16:无符号补0,右移16位
(3)写状态加1:
S+1
(4)读状态加1:
S+(1<<16)即S + 0x00010000
在代码层的判断中,如果S不等于0,当写状态(S&0x0000FFFF),而读状态(S>>>16)大于0,则表示该读写锁的读锁已被获取。
4、写锁的获取与释放
看下WriteLock类中的lock和unlock方法:
public void lock() {sync.acquire(1);
}public void unlock() {sync.release(1);
}
可以看到就是调用的独占式同步状态的获取与释放,因此真实的实现就是Sync的 tryAcquire和 tryRelease。
写锁的获取,看下tryAcquire:
1 protected final boolean tryAcquire(int acquires) {2 //当前线程3 Thread current = Thread.currentThread();4 //获取状态5 int c = getState();6 //写线程数量(即获取独占锁的重入数)7 int w = exclusiveCount(c);8 9 //当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
10 if (c != 0) {
11 // 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;
12 // 如果写锁状态不为0且写锁没有被当前线程持有返回false
13 if (w == 0 || current != getExclusiveOwnerThread())
14 return false;
15
16 //判断同一线程获取写锁是否超过最大次数(65535),支持可重入
17 if (w + exclusiveCount(acquires) > MAX_COUNT)
18 throw new Error("Maximum lock count exceeded");
19 //更新状态
20 //此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。
21 setState(c + acquires);
22 return true;
23 }
24
25 //到这里说明此时c=0,读锁和写锁都没有被获取
26 //writerShouldBlock表示是否阻塞
27 if (writerShouldBlock() ||
28 !compareAndSetState(c, c + acquires))
29 return false;
30
31 //设置锁为当前线程所有
32 setExclusiveOwnerThread(current);
33 return true;
34 }
其中exclusiveCount方法表示占有写锁的线程数量,源码如下:
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
说明:直接将状态state和(2^16 - 1)做与运算,其等效于将state模上2^16。写锁数量由state的低十六位表示。
从源代码可以看出,获取写锁的步骤如下:
(1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。
(2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。
(3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。
(4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。
(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。
方法流程图如下:
写锁的释放,tryRelease方法:
[](javascript:void(0)😉
1 protected final boolean tryRelease(int releases) {2 //若锁的持有者不是当前线程,抛出异常3 if (!isHeldExclusively())4 throw new IllegalMonitorStateException();5 //写锁的新线程数6 int nextc = getState() - releases;7 //如果独占模式重入数为0了,说明独占模式被释放8 boolean free = exclusiveCount(nextc) == 0;9 if (free)
10 //若写锁的新线程数为0,则将锁的持有者设置为null
11 setExclusiveOwnerThread(null);
12 //设置写锁的新线程数
13 //不管独占模式是否被释放,更新独占重入数
14 setState(nextc);
15 return free;
16 }
写锁的释放过程还是相对而言比较简单的:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。
说明:此方法用于释放写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异常,否则,计算释放资源后的写锁的数量,若为0,表示成功释放,资源不将被占用,否则,表示资源还被占用。其方法流程图如下。
5、读锁的获取与释放
类似于写锁,读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared 和 tryReleaseShared方法。
读锁的获取,看下tryAcquireShared方法
1 protected final int tryAcquireShared(int unused) {2 // 获取当前线程3 Thread current = Thread.currentThread();4 // 获取状态5 int c = getState();6 7 //如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级8 if (exclusiveCount(c) != 0 &&9 getExclusiveOwnerThread() != current)
10 return -1;
11 // 读锁数量
12 int r = sharedCount(c);
13 /*
14 * readerShouldBlock():读锁是否需要等待(公平锁原则)
15 * r < MAX_COUNT:持有线程小于最大数(65535)
16 * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
17 */
18 // 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功
19 if (!readerShouldBlock() &&
20 r < MAX_COUNT &&
21 compareAndSetState(c, c + SHARED_UNIT)) {
22 //r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
23 if (r == 0) { // 读锁数量为0
24 // 设置第一个读线程
25 firstReader = current;
26 // 读线程占用的资源数为1
27 firstReaderHoldCount = 1;
28 } else if (firstReader == current) { // 当前线程为第一个读线程,表示第一个读锁线程重入
29 // 占用资源数加1
30 firstReaderHoldCount++;
31 } else { // 读锁数量不为0并且不为当前线程
32 // 获取计数器
33 HoldCounter rh = cachedHoldCounter;
34 // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
35 if (rh == null || rh.tid != getThreadId(current))
36 // 获取当前线程对应的计数器
37 cachedHoldCounter = rh = readHolds.get();
38 else if (rh.count == 0) // 计数为0
39 //加入到readHolds中
40 readHolds.set(rh);
41 //计数+1
42 rh.count++;
43 }
44 return 1;
45 }
46 return fullTryAcquireShared(current);
47 }
[](javascript:void(0)😉
其中sharedCount方法表示占有读锁的线程数量,源码如下:
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
说明:直接将state右移16位,就可以得到读锁的线程数量,因为state的高16位表示读锁,对应的第十六位表示写锁数量。
读锁获取锁的过程比写锁稍微复杂些,首先判断写锁是否为0并且当前线程不占有独占锁,直接返回;否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且比较设置状态成功,若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程为第一个读线程,则增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值。流程图如下。
注意:更新成功后会在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程副本中记录当前线程重入数(23行至43行代码),这是为了实现jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数),加入了这个方法让代码复杂了不少,但是其原理还是很简单的:如果当前只有一个线程的话,还不需要动用ThreadLocal,直接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。
fullTryAcquireShared方法:
final int fullTryAcquireShared(Thread current) {HoldCounter rh = null;for (;;) { // 无限循环// 获取状态int c = getState();if (exclusiveCount(c) != 0) { // 写线程数量不为0if (getExclusiveOwnerThread() != current) // 不为当前线程return -1;} else if (readerShouldBlock()) { // 写线程数量为0并且读线程被阻塞// Make sure we're not acquiring read lock reentrantlyif (firstReader == current) { // 当前线程为第一个读线程// assert firstReaderHoldCount > 0;} else { // 当前线程不为第一个读线程if (rh == null) { // 计数器不为空// rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current)) { // 计数器为空或者计数器的tid不为当前正在运行的线程的tidrh = readHolds.get();if (rh.count == 0)readHolds.remove();}}if (rh.count == 0)return -1;}}if (sharedCount(c) == MAX_COUNT) // 读锁数量为最大值,抛出异常throw new Error("Maximum lock count exceeded");if (compareAndSetState(c, c + SHARED_UNIT)) { // 比较并且设置成功if (sharedCount(c) == 0) { // 读线程数量为0// 设置第一个读线程firstReader = current;// firstReaderHoldCount = 1;} else if (firstReader == current) {firstReaderHoldCount++;} else {if (rh == null)rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;cachedHoldCounter = rh; // cache for release}return 1;}}
}
[](javascript:void(0)😉
说明:在tryAcquireShared函数中,如果下列三个条件不满足(读线程是否应该被阻塞、小于最大值、比较设置成功)则会进行fullTryAcquireShared函数中,它用来保证相关操作可以成功。其逻辑与tryAcquireShared逻辑类似,不再累赘。
1 protected final boolean tryReleaseShared(int unused) {2 // 获取当前线程3 Thread current = Thread.currentThread();4 if (firstReader == current) { // 当前线程为第一个读线程5 // assert firstReaderHoldCount > 0;6 if (firstReaderHoldCount == 1) // 读线程占用的资源数为17 firstReader = null;8 else // 减少占用的资源9 firstReaderHoldCount--;
10 } else { // 当前线程不为第一个读线程
11 // 获取缓存的计数器
12 HoldCounter rh = cachedHoldCounter;
13 if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
14 // 获取当前线程对应的计数器
15 rh = readHolds.get();
16 // 获取计数
17 int count = rh.count;
18 if (count <= 1) { // 计数小于等于1
19 // 移除
20 readHolds.remove();
21 if (count <= 0) // 计数小于等于0,抛出异常
22 throw unmatchedUnlockException();
23 }
24 // 减少计数
25 --rh.count;
26 }
27 for (;;) { // 无限循环
28 // 获取状态
29 int c = getState();
30 // 获取状态
31 int nextc = c - SHARED_UNIT;
32 if (compareAndSetState(c, nextc)) // 比较并进行设置
33 // Releasing the read lock has no effect on readers,
34 // but it may allow waiting writers to proceed if
35 // both read and write locks are now free.
36 return nextc == 0;
37 }
38 }
[](javascript:void(0)😉
说明:此方法表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。其流程图如下。
在读锁的获取、释放过程中,总是会有一个对象存在着,同时该对象在获取线程获取读锁是+1,释放读锁时-1,该对象就是HoldCounter。
要明白HoldCounter就要先明白读锁。前面提过读锁的内在实现机制就是共享锁,对于共享锁其实我们可以稍微的认为它不是一个锁的概念,它更加像一个计数器的概念。一次共享锁操作就相当于一次计数器的操作,获取共享锁计数器+1,释放共享锁计数器-1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。所以HoldCounter的作用就是当前线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其他线程锁就会抛出异常。
先看读锁获取锁的部分:
[](javascript:void(0)😉
if (r == 0) {//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中firstReader = current;firstReaderHoldCount = 1;
} else if (firstReader == current) {//第一个读锁线程重入firstReaderHoldCount++;
} else { //非firstReader计数HoldCounter rh = cachedHoldCounter;//readHoldCounter缓存//rh == null 或者 rh.tid != current.getId(),需要获取rhif (rh == null || rh.tid != current.getId()) cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh); //加入到readHolds中rh.count++; //计数+1
}
[](javascript:void(0)😉
这里为什么要搞一个firstRead、firstReaderHoldCount呢?而不是直接使用else那段代码?这是为了一个效率问题,firstReader是不会放入到readHolds中的,如果读锁仅有一个的情况下就会避免查找readHolds。可能就看这个代码还不是很理解HoldCounter。我们先看firstReader、firstReaderHoldCount的定义:
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
这两个变量比较简单,一个表示线程,当然该线程是一个特殊的线程,一个是firstReader的重入计数。
HoldCounter的定义:
static final class HoldCounter {int count = 0;final long tid = Thread.currentThread().getId();
}
在HoldCounter中仅有count和tid两个变量,其中count代表着计数器,tid是线程的id。但是如果要将一个对象和线程绑定起来仅记录tid肯定不够的,而且HoldCounter根本不能起到绑定对象的作用,只是记录线程tid而已。
诚然,在java中,我们知道如果要将一个线程和对象绑定在一起只有ThreadLocal才能实现。所以如下:
static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();}
}ThreadLocalHoldCounter继承ThreadLocal,并且重写了initialValue方法。
故而,HoldCounter应该就是绑定线程上的一个计数器,而ThradLocalHoldCounter则是线程绑定的ThreadLocal。从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。
总结
通过上面的源码分析,我们可以发现一个现象:
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
综上:
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。
[4] StampedLock
一、StampedLock类简介
StampedLock类,在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。
首先明确下,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。
1.1 StampedLock的引入
先来看下,为什么有了ReentrantReadWriteLock,还要引入StampedLock?
ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的。
但是,读写锁如果使用不当,很容易产生“饥饿”问题:
比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。(在ReentrantLock类的介绍章节中,介绍过这种情况)
1.2 StampedLock的特点
链接:https://segmentfault.com/a/1190000015808032?utm_source=tag-newest
StampedLock的主要特点概括一下,有以下几点:
- 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
- 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
- StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
- StampedLock有三种访问模式:
①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
③Optimistic reading(乐观读模式):这是一种优化的读模式。 - StampedLock支持读锁和写锁的相互转换
我们知道RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。 - 无论写锁还是读锁,都不支持Conditon等待
我们知道,在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会阻塞。
但是,在Optimistic reading中,即使读线程获取到了读锁,写线程尝试获取写锁也不会阻塞,这相当于对读模式的优化,但是可能会导致数据不一致的问题。所以,当使用Optimistic reading获取到读锁时,必须对获取结果进行校验。
[5] Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数,以此限制能同时访问共享资源的线程上限。
public static void main(String[] args) {// 1. 创建 semaphore 对象Semaphore semaphore = new Semaphore(3);// 2. 10个线程同时运行for (int i = 0; i < 10; i++) {new Thread(() -> {// 3. 获取许可try {semaphore.acquire();} catch (InterruptedException e) {e.printStackTrace();}try {log.debug("running...");sleep(1);log.debug("end...");} finally {// 4. 释放许可semaphore.release();}}).start();}}
static final class NonfairSync extends Sync {private static final long serialVersionUID = -2694183684443567898L;NonfairSync(int permits) {// permits 即 statesuper(permits);}// Semaphore 方法, 方便阅读, 放在此处public void acquire() throws InterruptedException {sync.acquireSharedInterruptibly(1);}// AQS 继承过来的方法, 方便阅读, 放在此处public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);}// 尝试获得共享锁protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires);}// Sync 继承过来的方法, 方便阅读, 放在此处final int nonfairTryAcquireShared(int acquires) {for (;;) {int available = getState();int remaining = available - acquires; if (// 如果许可已经用完, 返回负数, 表示获取失败, 进入 doAcquireSharedInterruptiblyremaining < 0 ||// 如果 cas 重试成功, 返回正数, 表示获取成功compareAndSetState(available, remaining)
北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090) {return remaining;}}}// AQS 继承过来的方法, 方便阅读, 放在此处private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {final Node node = addWaiter(Node.SHARED);boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head) {// 再次尝试获取许可int r = tryAcquireShared(arg);if (r >= 0) {// 成功后本线程出队(AQS), 所在 Node设置为 head// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark// 如果 head.waitStatus == 0 ==> Node.PROPAGATE // r 表示可用资源数, 为 0 则不会继续传播setHeadAndPropagate(node, r);p.next = null; // help GCfailed = false;return;}}// 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}}// Semaphore 方法, 方便阅读, 放在此处public void release() {sync.releaseShared(1);}// AQS 继承过来的方法, 方便阅读, 放在此处public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}// Sync 继承过来的方法, 方便阅读, 放在此处
北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090
3. 为什么要有 PROPAGATE
早期有 bug
releaseShared 方法
doAcquireShared 方法protected final boolean tryReleaseShared(int releases) {for (;;) {int current = getState();int next = current + releases;if (next < current) // overflowthrow new Error("Maximum permit count exceeded");if (compareAndSetState(current, next))return true;}}
}
[6] threadlocal
https://www.bilibili.com/video/BV1SJ41157oF
使用ThreadLocal维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。
ThreadLocal内部实现机制:
- 每个线程内部都会维护一个Map对象,称为ThreadLocalMap,里边会包含若干了Entry(K-V键值对),相应的线程被称为这些Entry的属主线程。
- Entry的Key是一个ThreadLocal实例,Value是一个线程特有对象。Entry的作用是为其属主线程建立起一个ThreadLocal实例与一个线程特有对象之间的对应关系
- Entry对Key的引用是弱引用;Entry对Value的引用是强引用(key时弱引用便于垃圾回收)。
可以用于日期处理,随机数,上下文信息。
**当某些数据是以线程为作用域并且不同线程有不同数据副本时,考虑ThreadLocal。**https://www.jianshu.com/p/3a09dd40f8a4
[7] CountDownLatch
答: 两个关键字经常放在一起比较和考察,下边我们分别介绍。
CountDownLatch是一个倒计时协调器,它可以实现一个或者多个线程等待其余线程完成一组特定的操作之后,继续运行。
CountDownLatch的内部实现如下:
-
CountDownLatch内部维护一个计数器,CountDownLatch.countDown()每被执行一次都会使计数器值减少1。
-
当计数器不为0时,CountDownLatch.await()方法的调用将会导致执行线程被暂停,这些线程就叫做该CountDownLatch上的等待线程。
-
CountDownLatch.countDown()相当于一个通知方法,当计数器值达到0时,唤醒所有等待线程。当然对应还有指定等待时间长度的CountDownLatch.await( long , TimeUnit)方法。
[8] CyclicBarrier?
CyclicBarrier是一个栅栏,可以实现多个线程相互等待执行到指定的地点,这时候这些线程会再接着执行,在实际工作中可以用来模拟高并发请求测试。
可以认为是这样的,当我们爬山的时候,到了一个平坦处,前面队伍会稍作休息,等待后边队伍跟上来,当最后一个爬山伙伴也达到该休息地点时,所有人同时开始从该地点出发,继续爬山。
CyclicBarrier的内部实现如下:
- 使用CyclicBarrier实现等待的线程被称为参与方(Party),参与方只需要执行CyclicBarrier.await()就可以实现等待,该栅栏维护了一个显示锁,可以识别出最后一个参与方,当最后一个参与方调用await()方法时,前面等待的参与方都会被唤醒,并且该最后一个参与方也不会被暂停。
- CyclicBarrier内部维护了一个计数器变量count = 参与方的个数,调用await方法可以使得count -1。当判断到是最后一个参与方时,调用singalAll唤醒所有线程。
[9] Atmoic
**答:**介绍Atomic之前先来看一个问题吧,i++操作是线程安全的吗?
i++操作并不是线程安全的,它是一个复合操作,包含三个步骤:
- 拷贝i的值到临时变量
- 临时变量++操作
- 拷贝回原始变量i
这是一个复合操作,不能保证原子性,所以这不是线程安全的操作。那么如何实现原子自增等操作呢?
这里就用到了JDK在java.util.concurrent.atomic包下的AtomicInteger等原子类了。AtomicInteger类提供了getAndIncrement和incrementAndGet等原子性的自增自减等操作。Atomic等原子类内部使用了CAS来保证原子性。
[10] FutureTask
在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。
FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。
[11] ForkJoin
主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。
ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争。例如下图中,Thread2 从 Thread1 的队列中拿出最晚的 Task1 任务,Thread1 会拿出 Task2 来执行,这样就避免发生竞争。但是如果队列中只有一个任务时还是会发生竞争。
【Java中的线程池】
[1] 使用线程池的好处
为了降低资源消耗,提高响应速度,提高线程的可管理性,所以出现了线程池。
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
- 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行;
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
[2] Executor框架
1. 什么是Executor框架?
我们知道线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等。线程用于执行异步任务,单个的线程既是工作单元也是执行机制。从JDK1.5开始,为了把工作单元与执行机制分离开,Executor框架是一个统一创建与运行的接口。Executor框架实现的就是线程池的功能。
2. Executor框架结构图解
Executor框架包括3大部分:
(1)任务:也就是工作单元,包括被执行任务需要实现的接口:Runnable接口或者Callable接口;
(2)任务的执行:也就是把任务分派给多个线程的执行机制,包括Executor接口及继承自Executor接口的ExecutorService接口。
(3)异步计算的结果:包括Future接口及实现了Future接口的FutureTask类。
3. Executor框架成员:
ThreadPoolExecutor实现类、ScheduledThreadPoolExecutor实现类、Future接口、Runnable和Callable接口、Executors工厂类
[3] ThreadPoolExecutor类的参数字段
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
- corePoolSize:核心线程数:线程池中会维护一个最小的线程数量,即使这些线程处于空闲状态,他们也不会被销毁(除非设置了allowCoreThreadTimeOut),而且线程数小于此值的话,仍然会创建新的线程。
- maximumPoolSize:最大线程数:线程池所能容的最多线程数量。一个任务被提交到线程池后,首先会缓存到工作队列中,如果工作队列满了,则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize来指定。
- keepAliveTime :线程空闲的保活时间:一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定。
- unit:时间单位:keepAliveTime的计量单位。
- workQueue:存储线程的队列:新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列。
- threadFactory:创建线程的工厂:创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
- handler:拒绝策略:当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略。
[4] 线程池的排队策略
- 当线程数达到 小于corePoolSize ,即使有空闲队列,线程池也会优先创建一个新线程来执行任务。
- 当线程数达到 corePoolSize 且并没有线程空闲时,加入任务,这时候会有两种情况:
- 如果队列选择了无界队列,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。
- 如果队列选择了有界队列,任务会优先入队,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急,若此范围线程空闲,则有keepAliveTime决定存活时间。
- 针对有界队列,如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现。
- 针对有界队列,当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制。
[6] 拒绝策略
四种线程池拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
框架实现:
- Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方
便定位问题 - Netty 的实现,是创建一个新线程来执行任务
- ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
- PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
[5] 常见的阻塞队列
https://blog.csdn.net/xiewenfeng520/article/details/107142566
前面我们介绍了线程池内部有一个排队策略,任务可能需要在队列中进行排队等候。常见的阻塞队列包括如下的四种,接下来我们一起来看看吧。
-
ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
-
LinkedBlockingQuene:基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
-
SynchronousQuene:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
-
PriorityBlockingQueue:具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
阻塞队列特点及其使用场景:
ArrayBlockingQueue(有界):
- 内部使用一个数组作为其存储空间,数组的存储空间是预先分配的
- 优点是 put 和 take操作不会增加GC的负担(因为空间是预先分配的)
- 缺点是 put 和 take操作使用同一个锁,可能导致锁争用,导致较多的上下文切换。
- 会执行拒绝策略,有救急线程
- ArrayBlockingQueue适合在生产者线程和消费者线程之间的并发程度较低的情况下使用。
LinkedBlockingQueue:
- 是一个无界队列(其实队列长度是Integer.MAX_VALUE)
- 内部存储空间是一个链表,并且链表节点所需的存储空间是动态分配的
- 优点是 put 和 take 操作使用两个显式锁(putLock和takeLock)
- 缺点是增加了GC的负担,因为空间是动态分配的。
- 不会执行拒绝策略,无救急线程
- LinkedBlockingQueue适合在生产者线程和消费者线程之间的并发程度较高的情况下使用。
SynchronousQueue:
- SynchronousQueue可以被看做一种特殊的有界队列。
- 生产者线程生产一个产品之后,会等待消费者线程来取走这个产品,才会接着生产下一个产品。
- 适合在生产者线程和消费者线程之间的处理能力相差不大的情况下使用。
[6] Java提供的四种线程池
Executors只是对线程池一些特定情况的简洁使用,直接用ThreadPoolExecutor构造线程池会获得更为强大的功能。java自带的四种线程池通过 ThreadPoolExecutor 的构造方法实现:
- newCachedThreadPool:是一个根据需要来创建线程的线程池,它的corePoolSize为0,maximumPoolSize设置为Integer.MAX_VALUE,所以maximumPool是无界的,keepAliveTime设置为60L,代表所有空闲线程等待新任务的最长时间为60L。
这意味着,如果主线程提交任务的速度高于maxiunmPool中线程处理任务的速度时,将会不断CachedThreadPool创建新线程。极端情况下会耗尽CPU资源。所以它适用于负载较轻的场景,执行短期异步任务如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}
- newFixedThreadPool:创建一个可重用固定线程数的线程池,可控制线程最大并发数,超出的线程会在队列中等待。因为采用无界的阻塞队列LinkedBlockingQueue,所以maximumPoolSize和keepAliveTime为无效函数,实际线程数量永远不会变化,只会小于等于corePoolSize。
适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
- newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。它的corePoolSize和keepAliveTime设置为1,由于采用无界堵塞队列LinkedBlockingQueue,可以保证无限缓存任务,并且线程固定。
public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
- newScheduledThreadPool:它继承于ThreadPoolExecutor。创建一个定长任务线程池,适用于执行延时或者周期性任务。
public class OneMoreStudy {public static void main(String[] args) {final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);System.out.println("提交时间: " + sdf.format(new Date()));scheduledThreadPool.schedule(new Runnable() {@Overridepublic void run() {System.out.println("运行时间: " + sdf.format(new Date()));}}, 3, TimeUnit.SECONDS);scheduledThreadPool.shutdown();}
}
[7] 手写一个线程池
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z1K2UxhR-1596599154351)(X:\Users\xu\AppData\Roaming\Typora\typora-user-images\image-20200709145932351.png)]
package cn.itcast.n8;import lombok.extern.slf4j.Slf4j;
import org.springframework.core.log.LogDelegateFactory;import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;@Slf4j(topic = "c.TestPool")
public class TestPool {public static void main(String[] args) {ThreadPool threadPool = new ThreadPool(1,1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{// 1. 死等
// queue.put(task);// 2) 带超时等待
// queue.offer(task, 1500, TimeUnit.MILLISECONDS);// 3) 让调用者放弃任务执行
// log.debug("放弃{}", task);// 4) 让调用者抛出异常
// throw new RuntimeException("任务执行失败 " + task);// 5) 让调用者自己执行任务task.run();});for (int i = 0; i < 4; i++) {int j = i;threadPool.execute(() -> {try {Thread.sleep(1000L);} catch (InterruptedException e) {e.printStackTrace();}log.debug("{}", j);});}}
}@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {void reject(BlockingQueue<T> queue, T task);
}@Slf4j(topic = "c.ThreadPool")
class ThreadPool {// 任务队列private BlockingQueue<Runnable> taskQueue;// 线程集合private HashSet<Worker> workers = new HashSet<>();// 核心线程数private int coreSize;// 获取任务时的超时时间private long timeout;private TimeUnit timeUnit;private RejectPolicy<Runnable> rejectPolicy;// 执行任务public void execute(Runnable task) {// 当任务数没有超过 coreSize 时,直接交给 worker 对象执行// 如果任务数超过 coreSize 时,加入任务队列暂存synchronized (workers) {if(workers.size() < coreSize) {Worker worker = new Worker(task);log.debug("新增 worker{}, {}", worker, task);workers.add(worker);worker.start();} else {
// taskQueue.put(task);// 1) 死等// 2) 带超时等待// 3) 让调用者放弃任务执行// 4) 让调用者抛出异常// 5) 让调用者自己执行任务taskQueue.tryPut(rejectPolicy, task);}}}public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, RejectPolicy<Runnable> rejectPolicy) {this.coreSize = coreSize;this.timeout = timeout;this.timeUnit = timeUnit;this.taskQueue = new BlockingQueue<>(queueCapcity);this.rejectPolicy = rejectPolicy;}class Worker extends Thread{private Runnable task;public Worker(Runnable task) {this.task = task;}@Overridepublic void run() {// 执行任务// 1) 当 task 不为空,执行任务// 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
// while(task != null || (task = taskQueue.take()) != null) {while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {try {log.debug("正在执行...{}", task);task.run();} catch (Exception e) {e.printStackTrace();} finally {task = null;}}synchronized (workers) {log.debug("worker 被移除{}", this);workers.remove(this);}}}
}
@Slf4j(topic = "c.BlockingQueue")
class BlockingQueue<T> {// 1. 任务队列private Deque<T> queue = new ArrayDeque<>();// 2. 锁private ReentrantLock lock = new ReentrantLock();// 3. 生产者条件变量private Condition fullWaitSet = lock.newCondition();// 4. 消费者条件变量private Condition emptyWaitSet = lock.newCondition();// 5. 容量private int capcity;public BlockingQueue(int capcity) {this.capcity = capcity;}// 带超时阻塞获取public T poll(long timeout, TimeUnit unit) {lock.lock();try {// 将 timeout 统一转换为 纳秒long nanos = unit.toNanos(timeout);while (queue.isEmpty()) {try {// 返回值是剩余时间if (nanos <= 0) {return null;}nanos = emptyWaitSet.awaitNanos(nanos);} catch (InterruptedException e) {e.printStackTrace();}}T t = queue.removeFirst();fullWaitSet.signal();return t;} finally {lock.unlock();}}// 阻塞获取public T take() {lock.lock();try {while (queue.isEmpty()) {try {emptyWaitSet.await();} catch (InterruptedException e) {e.printStackTrace();}}T t = queue.removeFirst();fullWaitSet.signal();return t;} finally {lock.unlock();}}// 阻塞添加public void put(T task) {lock.lock();try {while (queue.size() == capcity) {try {log.debug("等待加入任务队列 {} ...", task);fullWaitSet.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("加入任务队列 {}", task);queue.addLast(task);emptyWaitSet.signal();} finally {lock.unlock();}}// 带超时时间阻塞添加public boolean offer(T task, long timeout, TimeUnit timeUnit) {lock.lock();try {long nanos = timeUnit.toNanos(timeout);while (queue.size() == capcity) {try {if(nanos <= 0) {return false;}log.debug("等待加入任务队列 {} ...", task);nanos = fullWaitSet.awaitNanos(nanos);} catch (InterruptedException e) {e.printStackTrace();}}log.debug("加入任务队列 {}", task);queue.addLast(task);emptyWaitSet.signal();return true;} finally {lock.unlock();}}public int size() {lock.lock();try {return queue.size();} finally {lock.unlock();}}public void tryPut(RejectPolicy<T> rejectPolicy, T task) {lock.lock();try {// 判断队列是否满if(queue.size() == capcity) {rejectPolicy.reject(this, task);} else { // 有空闲log.debug("加入任务队列 {}", task);queue.addLast(task);emptyWaitSet.signal();}} finally {lock.unlock();}}
}