文章目录
引言
- 今天主要是将剩下的几个关键字基本原理讲一下,以及对应编程题联系一下,分别是
- join
- reentranlock
- 线程池
- AtomicInteger
基础知识
Lock接口(reentranlock)
特点
- 需要显示的进行加锁和解锁,增加了操作的灵活性
- 提供了可中断获取锁的特性
- 提供了超时获取锁的特性
- 实现Lock接口的API都是基于AQS提供能力实现的
- ReentrantLock是继承了lock接口
- 内部聚合了一个AQS同步器,通过调用AQS的方法来包装实现的
AQS同步器和Lock锁之间的差异
- lock是面向锁的使用者,定义了使用者和锁的交互接口,隐藏了底层实现细节
- AQS是面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态的管理、线程排队等底层操作
- 锁和同步器很好的隔离了使用者和实现者所关注的领域
我们作为锁的使用者AQS了解就行
基本使用
- 需要声明特定的对象ReentrantLock,然后使用try-catch确保锁必须被释放
Lock lock = new ReentrantLock();
lock.lock()
try{
}finally{
lock.unlock();
}
finally一定要确保锁释放
提供的API
-
lock()
- 正常锁,加锁
-
lockInterruptibly()
- 可中断锁,会响应中断,在锁的获取过程中可以中断该线程
- 会抛出InterruptedException异常
-
tryLock()
- 尝试非阻塞获取锁,调用该方法后的立刻返回
- 能够获取返回true,不能获取返回false
-
tryLock(long time,TimeUnit unit)
- 存在以下三种情况,会抛出异常InterruptedException
- 当前线程在超时时间内获得了锁
- 当前线程在超时时间内被中断
- 超时时间结束,返回false
- 存在以下三种情况,会抛出异常InterruptedException
-
unlock()
- 正常解锁
-
Condition newCondition()
- 有点难懂了这个,没有代码和样例
- 获取等待通知组件,该组件和当前的锁绑定
- 当前线程只有获取了锁,才能调用该组件的wait方法
- 调用后,当前线程将释放锁
Lock和Synchronized的对比
相同点
- 都是用来保护资源线程安全,都可以保证共享变量的可见性
- 都拥有可重入的特点
不同点
-
加解锁控制的差别
- synchronized关键字是通过JVM实现的内置锁
- lock是需要手动控制,并且unlock一定要放入finally中
-
synchronized不够灵活
- synchronized只能一个线程获取锁,其他线程只能等待
- lcok可以通过lockInterruptibly,在获取锁等待过程中中断退出,或者使用trylock尝试获取锁,能获取就获取,不能获取就干别的事,不会阻塞
-
锁被线程拥有的数量
- synchornized只能被一个线程拥有
- lock可以被多个线程拥有,针对读写锁,就是多个锁拥有
是否支持公平锁和非公平锁
* synchronized只支持非公平锁
* lock都支持
如何选择
- 能不用锁就不用锁,推荐使用线程安全的工具类,java.util.concurrent进行操作
- 如果要用先选synchronized,更加安全,实在不行,用lock
重入锁ReentrantLock
如何实现可重入
- 已经获取一个锁的线程,可以再次获取该锁而不被阻塞
- 线程再次获取锁
- 锁需要识别再次获取锁的线程是否是已经获取的锁的线程相同。
- 锁的最终释放
- 通过AQS中同步状态state实现,当计数为零,释放成功。
公平锁和非公平锁
- reentrantlock有三个内部类分别是的FairSync和NonFairSync,这两个分别实现公平锁和非公平锁,然后的Sync是另外两个类的父类
Sync类 - 继承自AQS抽象方法
- 重要方法说明
- lock加锁方法,抽象方法并没有具体实现,留给子类实现的,FairSync和NonFairSync会根据自己的要求实现对应的公平锁和非公平锁
- nonfairTryAcquire:非公平锁获取锁
- isHeldExclusively:判断资源是否被当前线程占用
- isLocked:资源是否被占用
FairSync类和NonfairSync类
- 两者方法唯一不同的位置,判断条件多了hasQueuedPredecessors()方法
- 加入队列中当前节点是否有前驱节点的判断
- 返回true,说明是队列中第一个元素,直接获取对应的元素
- 返回false,必须要等前面的线程获取并释放锁,才能操作
构造函数
- 构造函数中,默认是使用非公平锁,如果要使用公平锁,需要传入true
读写锁ReentrantReadWriteLock
- 读写锁通过维护的一对锁,一个读锁和一个写锁,通过分离锁和写锁,并发性更高
- 读锁允许多个线程同时读,然后写锁会阻塞其他读线程和其他写线程
特性
- 公平性选择:同时支持公平锁和非公平锁
- 支持重入:读锁和写锁都支持多重获取
- 锁降级:通过特定的顺序,能够将写锁降级为读锁
- 获取写锁,在获取读锁,在释放写锁,写锁就降级为读锁了
读写锁接口和实例
- getReadLockCount
- 返回当前读锁被线程获取的次数
- getReadHoldCount
- 返回当前线程获取读锁的次数
- isWriteLocked
- 判定写锁是否被获取
- getWriteHoldCount
- 返回当前写锁被获取的次数
读写锁的实现分析
读写状态设计
- 基于AQS同步器实现同步功能,使用state维护重入性
- 高16位,维护读状态
- 低16位,维护写状态
支持锁降级,不支持锁升级
使用方式
- 后续的加锁和解锁是相同的
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Main{
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
Lock r = rwl.readLock();
Lock w = rwl.writeLock();
}
Condition接口
- 同监视器方法,也实现了类似wait、wait(long timeout)、notify(),notifyAll()等方法,和lock配合,可以实现等待\通知模式
Condition接口与实例
-
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法需要获取到Condition对象关联的锁。
- condition对象是由Lock对象创建出来的,Condition依赖Lock对象。
-
使用示例如下:必须使用lock.newCondition()创建对应的condition语句
- 和lock对象一样,都需要创建对应对象。
import java.awt.*;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class Main{
// 定义一个lock对象
ReentrantLock lock = new ReentrantLock();
Condition cd = lock.newCondition();
public void conditionAwait(){
lock.lock();
try{
cd.await();
}
catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void conditionSignal(){
lock.lock();
cd.signal();
lock.unlock();
}
}
condition使用方法
-
await()throws InterruptedException
- 当前线程进入等待状态,直到被同时signal或者中断
-
awaitUninterruptibly
- 当前线程等待被通知,并且不响应中断
-
awaitNanos(long nanosTimeout)
- 当前线程进入等待状态,直到被通知或者超时或者中断
- 返回的剩余的时间,返回0或者负数,说明超时了
-
awaitUntil(Date deadline) throws Interrupted Exception
- 当前线程进入等待状态直到被通知、中断或者到达预期时间
- 返回true,没到指定时间就被通知
- 返回false,到了指定时间,返回false
-
signal
- 唤醒一个等待在condition上的线程,
-
signalAll
- 唤醒所有等待在condition上的线程
实现原理
- 是AQS的内部类,每一个condition都维护一个队列
- 等待队列
- 一个condition一个等待队列,维护头节点和尾节点
- 一个同步器拥有一个同步队列和多个等待队列,condition对象可以创建多个
- 通知和等待
- 通知和等待都是通过移动condition队列和同步队列的头节点和尾节点实现
Join关键字
- Thread类的一个方法,让一个线程等待另一个线程的完成
- 如果线程A调用了线程B的join方法,线程A会被暂停执行,直到线程B执行完毕后,线程A才会继续执行!
具体使用
单使用一个join(),无限期等待,直到线程结束
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Main{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
System.out.println("Thread 1 is Running");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread 1 is Finished");
});
thread.start();
thread.join();
System.out.println("Main Thread is Running");
}
}
- 没加join
- 加了join
- 在main线程内部,thread调用的join方法,所以main要等待thread线程执行完毕
指定等待时间
- 这里就是等待了3ms,但是线程运行需要的执行10ms,所以出现了交叉输出的情况。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Main{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
System.out.println("Thread 1 is Running");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread 1 is Finished");
});
thread.start();
thread.join(3);
System.out.println("Main Thread is Running");
}
}
原子类
CAS
- CAS是Compare-And-Swap,对比交换。
- 一条CPU原子指令,先让CPU先进行比较两个值是否相等,然后原子地更新值。
- 基于硬件实现的,JMM将其封装为AtomicInteger类调用。
- 需要输入两个数值,分别是旧的和新的,操作期间,先比较一下旧的有没有发生变化,如果没发生就用新的替换,如果发生了,啥都不变
具体使用
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
class Main{
public static AtomicInteger count = new AtomicInteger(0);
public static void increment(){
count.getAndIncrement();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(Main::increment);
Thread t2 = new Thread(Main::increment);
Thread t3 = new Thread(Main::increment);
t1.start();
t2.start();
t3.start();
Thread.sleep(10);
System.out.println(count);
}
}
CAS问题
- CAS是乐观锁,然后synchronized是悲观锁
- CAS面临的问题
ABA问题
- 原来是A,然后改成了B,后来又变成了A,检查不出来是不是变过了。
- 场景,两次扣款,中途汇款
- 解决方法
- 使用版本号
循环开销大
- 自旋CAS如果长时间不成功,会给CPU带来非常大的开销的。
只能保证一个共享变量的原子操作
原子操作类
三个常用的Atomic原子类
- AtomicBoolean
- AtomicInteger
- AtomicLong
常用的方法
- get:获取当前值
- getAndSet:获取当前值并且设置一个新的值
- getAndIncrement:获取当前值并且自增1
- getAndDecrement:获取当前值并且自减1
编程练习
使用读写锁ReentrantLock实现一个Cache
- 使用ReentantLock实现一个cache,保证get读、put写(更新,并且返回旧的值)以及clear清楚等操作的安全性。
- 通过读锁来实现保证get方法,通过写锁来保证put方法
static class Cache{
public static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static ReentrantReadWriteLock.ReadLock r = rwl.readLock();
public static ReentrantReadWriteLock.WriteLock w = rwl.writeLock();
public static HashMap<Integer,Integer> map = new HashMap<>();
public int get(int key){
r.lock();
try {
return map.get(key);
}finally {
r.unlock();
}
}
public int set(int key,int val){
w.lock();
try{
return map.put(key,val);
} finally {
w.unlock();
}
}
public void clear(){
w.lock();
try{
map.clear();
}finally {
w.unlock();
}
}
}
- 就是一个读写锁的获取还有使用问题,记住了就好了!
线程交叉打印12A34B56C使用ReentrantLock实现
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Main{
public static ReentrantLock lock = new ReentrantLock();
public static Condition cd = lock.newCondition();
public static boolean printNum = true;
static class PrintNum implements Runnable{
@Override
public void run(){
for(int i = 1;i <= 52;i ++){
lock.lock();
try {
while (!printNum) cd.await();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.print(i);
System.out.print(++i);
printNum = false;
cd.signalAll();
lock.unlock();
}
}
}
static class PrintChar implements Runnable{
@Override
public void run(){
for(char i = 'A';i <= 'Z';i ++){
lock.lock();
try {
while (printNum) cd.await();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.print(i);
printNum = true;
cd.signalAll();
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new PrintNum());
Thread t2 = new Thread(new PrintChar());
t1.start();
t2.start();
}
}
- 这里基本上使用的方式和synchronized的一摸一样,都是使用同样的对象锁进行测试。
参考实现
- 这里是创建了两个condition,相当于一个读者一个写者进程,两者相互通信。
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Main{
public static ReentrantLock lock = new ReentrantLock();
public static Condition numCondition = lock.newCondition();
public static Condition charCondition = lock.newCondition();
public static boolean printNum = true;
static class PrintNum implements Runnable{
@Override
public void run(){
for(int i = 1;i <= 52;i ++){
lock.lock();
try {
while (!printNum) numCondition.await();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.print(i);
System.out.print(++i);
printNum = false;
// 唤醒沉睡的输出字母的线程
charCondition.signalAll();
lock.unlock();
}
}
}
static class PrintChar implements Runnable{
@Override
public void run(){
for(char i = 'A';i <= 'Z';i ++){
lock.lock();
try {
while (printNum) charCondition.await();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.print(i);
printNum = true;
numCondition.signalAll();
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new PrintNum());
Thread t2 = new Thread(new PrintChar());
t1.start();
t2.start();
}
}
这里就相当于是一个读者和写者的过程,同一个lock可以创建多个condition
T1、T2、T3三个线程按照顺序执行
- 这里使用的是join关键字,创建三个线程实现一下,然后使用join连接一下就行
class Main{
static class Task implements Runnable{
int taskId;
public Task(int taskId){
this.taskId = taskId;
}
@Override
public void run(){
System.out.println("Task " + taskId + " is running");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Task " + taskId + " finished");
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Task(1));
Thread t2 = new Thread(new Task(2));
Thread t3 = new Thread(new Task(3));
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
- 好吧,这里又不会了,这里只能保证三个线程在main线程之前完成,难道要创建位全局变量,然后在顺次传入他们的构造函数中。
一下子又不知道了!
参考实现
- 这里要确保上一个线程执行完毕之后,下一个线程在启动!
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Main{
static class Task implements Runnable{
int taskId;
public Task(int taskId){
this.taskId = taskId;
}
@Override
public void run(){
System.out.println("Task " + taskId + " is running");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Task " + taskId + " finished");
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Task(1));
Thread t2 = new Thread(new Task(2));
Thread t3 = new Thread(new Task(3));
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
}
}
这里对于join的理解有深刻了
- join之前的线程必须要执行完毕,然后才会执行下面的,如果要保证顺次执行,就要安排他们的启动顺序也是顺次的!
- 如果不加join,顺次启动,还是会有先后顺序,如果把join按照顺序插入在start中,就会保证在下一个启动之前,上一个已经完成了!
总结
- 明天在花时间的去看看AQS同步器、线程池的相关基础知识,这个看完Java多线程编程就算是完全学完了,题目也练习完了,然后在重新背一遍八股。
- 好吧,我承认,这个相当于又拖了一天,主要是今天有点太浪了,没怎么看!明天周一了,得加把劲了!开始继续刷题,并且把项目继续推进!
- 总算是写完了,今天早点睡吧,明天早点起!新的周一开始了,没有面试就好好学习!