多线程编程核心

在前面,我们了解了多线程的底层运作机制,我们终于知道,原来多线程环境下存在着如此之多的问题。在JDK5之前,我们只能选择synchronized关键字来实现锁,而JDK5之后,由于volatile关键字得到了升级(具体功能就是上一章所描述的),所以并发框架包便出现了,相比传统的synchronized关键字,我们对于锁的实现,有了更多的选择。

Doug Lea — JUC并发包的作者
如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算机科学系的老大爷。
说他是这个世界上对Java影响力最大的一个人,一点也不为过。因为两次Java历史上的大变革,他都间接或直接的扮演了举足轻重的角色。2004年所推出的Tiger。Tiger广纳了15项JSRs(Java Specification Requests)的语法及标准,其中一项便是JSR-166。JSR-166是来自于Doug编写的util.concurrent包。

那么,从这章开始,就让我们来感受一下,JUC为我们带来了什么。

锁框架

在JDK 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,Lock接口提供了与synchronized关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。

Lock和Condition接口

使用并发包中的锁和我们传统的synchronized锁不太一样,这里的锁我们可以认为是一把真正意义上的锁,每个锁都是一个对应的锁对象,我只需要向锁对象获取锁或是释放锁即可。我们首先来看看,此接口中定义了什么:

public interface Lock {//获取锁,拿不到锁会阻塞,等待其他线程释放锁,获取到锁后返回void lock();//同上,但是等待过程中会响应中断void lockInterruptibly() throws InterruptedException;//尝试获取锁,但是不会阻塞,如果能获取到会返回true,不能返回falseboolean tryLock();//尝试获取锁,但是可以限定超时时间,如果超出时间还没拿到锁返回false,否则返回true,可以响应中断boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//释放锁void unlock();//暂时可以理解为替代传统的Object的wait()、notify()等操作的工具Condition newCondition();
}

这里我们可以演示一下,如何使用Lock类来进行加锁和释放锁操作:

public class Main {private static int i = 0;public static void main(String[] args) throws InterruptedException {Lock testLock = new ReentrantLock();   //可重入锁ReentrantLock类是Lock类的一个实现,我们后面会进行介绍Runnable action = () -> {for (int j = 0; j < 100000; j++) {   //还是以自增操作为例testLock.lock();    //对i自增加锁,加锁成功后其他线程如果也要获取锁,会阻塞,等待当前线程释放i++;testLock.unlock();  //对i自增解锁,释放锁之后其他线程就可以获取这把锁了(注意在这之前一定得加锁,不然报错)}};new Thread(action).start();new Thread(action).start();Thread.sleep(1000);   //等上面两个线程跑完System.out.println(i);}
}

运行结果:200000
可以看到,和我们之前使用synchronized相比,我们这里是真正在操作一个"锁"对象,当我们需要加锁时,只需要调用lock()方法,而需要释放锁时,只需要调用unlock()方法。程序运行的最终结果和使用synchronized锁是一样的。

那么,我们如何像传统的加锁那样,调用对象的wait()notify()方法呢,并发包提供了Condition接口:

public interface Condition {//与调用锁对象的wait方法一样,会进入到等待状态,但是这里需要调用Condition的signal或signalAll方法进行唤醒(感觉就是和普通对象的wait和notify是对应的)同时,等待状态下是可以响应中断的void await() throws InterruptedException;//同上,但不响应中断(看名字都能猜到)void awaitUninterruptibly();//等待指定时间,如果在指定时间(纳秒)内被唤醒,会返回剩余时间,如果超时,会返回0或负数,可以响应中断long awaitNanos(long nanosTimeout) throws InterruptedException;//等待指定时间(可以指定时间单位),如果等待时间内被唤醒,返回true,否则返回false,可以响应中断boolean await(long time, TimeUnit unit) throws InterruptedException;//可以指定一个明确的时间点,如果在时间点之前被唤醒,返回true,否则返回false,可以响应中断boolean awaitUntil(Date deadline) throws InterruptedException;//唤醒一个处于等待状态的线程,注意还得获得锁才能接着运行void signal();//同上,但是是唤醒所有等待线程void signalAll();
}

这里我们通过一个简单的例子来演示一下:

public static void main(String[] args) throws InterruptedException {Lock testLock = new ReentrantLock();Condition condition = testLock.newCondition();new Thread(() -> {testLock.lock();   //和synchronized一样,必须持有锁的情况下才能使用awaitSystem.out.println("线程1进入等待状态!");try {condition.await();   //进入等待状态} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1等待结束!");testLock.unlock();}).start();Thread.sleep(100); //防止线程2先跑new Thread(() -> {testLock.lock();System.out.println("线程2开始唤醒其他等待线程");condition.signal();   //唤醒线程1,但是此时线程1还必须要拿到锁才能继续运行System.out.println("线程2结束");testLock.unlock();   //这里释放锁之后,线程1就可以拿到锁继续运行了}).start();
}

可以发现,Condition对象使用方法和传统的对象使用差别不是很大。

思考:下面这种情况跟上面有什么不同?

public static void main(String[] args) throws InterruptedException {Lock testLock = new ReentrantLock();new Thread(() -> {testLock.lock();System.out.println("线程1进入等待状态!");try {testLock.newCondition().await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1等待结束!");testLock.unlock();}).start();Thread.sleep(100);new Thread(() -> {testLock.lock();System.out.println("线程2开始唤醒其他等待线程");testLock.newCondition().signal();System.out.println("线程2结束");testLock.unlock();}).start();
}

通过分析可以得到,在调用newCondition()后,会生成一个新的Condition对象,并且同一把锁内是可以存在多个Condition对象的(实际上原始的锁机制等待队列只能有一个,而这里可以创建很多个Condition来实现多等待队列),而上面的例子中,实际上使用的是不同的Condition对象,只有对同一个Condition对象进行等待和唤醒操作才会有效,而不同的Condition对象是分开计算的

最后我们再来讲解一下时间单位,这是一个枚举类,也是位于java.util.concurrent包下:

public enum TimeUnit {/*** Time unit representing one thousandth of a microsecond*/NANOSECONDS {public long toNanos(long d)   { return d; }//纳秒public long toMicros(long d)  { return d/(C1/C0); }//微秒public long toMillis(long d)  { return d/(C2/C0); }//毫秒public long toSeconds(long d) { return d/(C3/C0); }//秒public long toMinutes(long d) { return d/(C4/C0); }public long toHours(long d)   { return d/(C5/C0); }public long toDays(long d)    { return d/(C6/C0); }public long convert(long d, TimeUnit u) { return u.toNanos(d); }int excessNanos(long d, long m) { return (int)(d - (m*C2)); }},//....

可以看到时间单位有很多的,比如DAYSECONDSMINUTES等,我们可以直接将其作为时间单位,比如我们要让一个线程等待1秒钟,可以像下面这样编写:

public static void main(String[] args) throws InterruptedException {Lock testLock = new ReentrantLock();new Thread(() -> {testLock.lock();try {System.out.println("等待是否未超时:"+testLock.newCondition().await(1, TimeUnit.SECONDS));} catch (InterruptedException e) {e.printStackTrace();}testLock.unlock();}).start();
}

当然,Lock类的tryLock方法也是支持使用时间单位的,各位可以自行进行测试。TimeUnit除了可以作为时间单位表示以外,还可以在不同单位之间相互转换:

public static void main(String[] args) throws InterruptedException {System.out.println("60秒 = "+TimeUnit.SECONDS.toMinutes(60) +"分钟");System.out.println("365天 = "+TimeUnit.DAYS.toSeconds(365) +" 秒");
}

也可以更加便捷地使用对象的wait()方法:

public static void main(String[] args) throws InterruptedException {synchronized (Main.class) {System.out.println("开始等待");TimeUnit.SECONDS.timedWait(Main.class, 3);   //直接等待3秒System.out.println("等待结束");}
}

我们也可以直接使用它来进行休眠操作:

public static void main(String[] args) throws InterruptedException {TimeUnit.SECONDS.sleep(1);  //休眠1秒钟
}

可重入锁

前面,我们讲解了锁框架的两个核心接口,那么我们接着来看看锁接口的具体实现类,我们前面用到了ReentrantLock,它其实是锁的一种,叫做可重入锁,那么这个可重入代表的是什么意思呢?简单来说,就是同一个线程,可以反复进行加锁操作:

public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock();lock.lock();lock.lock();   //连续加锁2次new Thread(() -> {System.out.println("线程2想要获取锁");lock.lock();System.out.println("线程2成功获取到锁");}).start();lock.unlock();System.out.println("线程1释放了一次锁");TimeUnit.SECONDS.sleep(1);lock.unlock();System.out.println("线程1再次释放了一次锁");  //释放两次后其他线程才能加锁
}

执行结果:

线程1释放了一次锁
线程2想要获取锁
线程1再次释放了一次锁
线程2成功获取到锁Process finished with exit code 0

可以看到,主线程连续进行了两次加锁操作(此操作是不会被阻塞的),在当前线程持有锁的情况下继续加锁不会被阻塞,并且,加锁几次,就必须要解锁几次,否则此线程依旧持有锁。我们可以使用getHoldCount()方法查看当前线程的加锁次数:

public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock();lock.lock();lock.lock();System.out.println("当前加锁次数:"+lock.getHoldCount()+",是否被锁:"+lock.isLocked());TimeUnit.SECONDS.sleep(1);lock.unlock();System.out.println("当前加锁次数:"+lock.getHoldCount()+",是否被锁:"+lock.isLocked());TimeUnit.SECONDS.sleep(1);lock.unlock();System.out.println("当前加锁次数:"+lock.getHoldCount()+",是否被锁:"+lock.isLocked());
}

运行结果:

当前加锁次数:2,是否被锁:true
当前加锁次数:1,是否被锁:true
当前加锁次数:0,是否被锁:false

可以看到,当锁不再被任何线程持有时,值为0,并且通过isLocked()方法查询结果为false

实际上,如果存在线程持有当前的锁,那么其他线程在获取锁时,是会暂时进入到等待队列的,我们可以通过getQueueLength()方法获取等待中线程数量的预估值:

public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock();lock.lock();Thread t1 = new Thread(lock::lock), t2 = new Thread(lock::lock);;t1.start();t2.start();TimeUnit.SECONDS.sleep(1);//睡眠不会释放锁,这行代码的意图就是让线程t1和t2启动起来,不至于主线程都结束了这两个消除还没有尝试去获取锁,结果出来就是0、false、false.System.out.println("当前等待锁释放的线程数:"+lock.getQueueLength());System.out.println("线程1是否在等待队列中:"+lock.hasQueuedThread(t1));System.out.println("线程2是否在等待队列中:"+lock.hasQueuedThread(t2));System.out.println("当前线程是否在等待队列中:"+lock.hasQueuedThread(Thread.currentThread()));
}
当前等待锁释放的线程数:2
线程1是否在等待队列中:true
线程2是否在等待队列中:true
当前线程是否在等待队列中:false
程序不会结束,因为主线程没有释放锁。

我们可以通过hasQueuedThread()方法来判断某个线程是否正在等待获取锁状态。

同样的,Condition也可以进行判断:通过使用getWaitQueueLength()方法能够查看同一个Condition目前有多少线程处于等待状态。

public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock();Condition condition = lock.newCondition();new Thread(() -> {lock.lock();try {condition.await();//等待状态会释放锁,所以只要持有锁的状态才可以使用这个方法} catch (InterruptedException e) {e.printStackTrace();}lock.unlock();}).start();TimeUnit.SECONDS.sleep(1);//休眠就是为了让上面的线程抢到锁,然后进入等待状态(会释放锁)lock.lock();System.out.println("当前Condition的等待线程数:"+lock.getWaitQueueLength(condition));condition.signal();System.out.println("当前Condition的等待线程数:"+lock.getWaitQueueLength(condition));lock.unlock();
}

运行结果:

当前Condition的等待线程数:1
当前Condition的等待线程数:0
Process finished with exit code 0

公平锁与非公平锁

前面我们了解了如果线程之间争抢同一把锁,会暂时进入到等待队列中,那么多个线程获得锁的顺序是不是一定是根据线程调用lock()方法时间来定的呢,我们可以看到,ReentrantLock的构造方法中,是这样写的:

public ReentrantLock() {sync = new NonfairSync();   //看名字貌似是非公平的
}

其实锁分为公平锁和非公平锁,默认我们创建出来的ReentrantLock是采用的非公平锁作为底层锁机制。那么什么是公平锁什么又是非公平锁呢?

  • 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
  • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

简单来说,公平锁不让插队,都老老实实排着;非公平锁让插队,但是排队的人让不让你插队就是另一回事了。

我们可以来测试一下公平锁和非公平锁的表现情况:

public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

这里我们选择使用第二个构造方法,可以选择是否为公平锁实现:

public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock(false);//非公平锁Runnable action = () -> {System.out.println("线程 "+Thread.currentThread().getName()+" 开始获取锁...");//这个例子的意思是如果执行了下面获取锁的代码就用这句代码显示出来。换句话这句话打印的就是申请锁的顺序。lock.lock();System.out.println("线程 "+Thread.currentThread().getName()+" 成功获取锁!");lock.unlock();};for (int i = 0; i < 10; i++) {   //建立10个线程new Thread(action, "T"+i).start();}
}

运行结果:

线程 T0 开始获取锁...
线程 T3 开始获取锁...
线程 T5 开始获取锁...
线程 T2 开始获取锁...
线程 T1 开始获取锁...
线程 T6 开始获取锁...
线程 T0 成功获取锁!
线程 T4 开始获取锁...
线程 T4 成功获取锁!
线程 T8 开始获取锁...
线程 T8 成功获取锁!
线程 T7 开始获取锁...
线程 T3 成功获取锁!
线程 T9 开始获取锁...
线程 T5 成功获取锁!
线程 T2 成功获取锁!
线程 T1 成功获取锁!
线程 T6 成功获取锁!
线程 T7 成功获取锁!
线程 T9 成功获取锁!Process finished with exit code 0

当是“公平锁”时,运行结果:

线程 T0 开始获取锁...
线程 T4 开始获取锁...
线程 T5 开始获取锁...
线程 T3 开始获取锁...
线程 T6 开始获取锁...
线程 T2 开始获取锁...
线程 T7 开始获取锁...
线程 T1 开始获取锁...
线程 T8 开始获取锁...
线程 T0 成功获取锁!
线程 T9 开始获取锁...
线程 T4 成功获取锁!
线程 T5 成功获取锁!
线程 T3 成功获取锁!
线程 T6 成功获取锁!
线程 T2 成功获取锁!
线程 T7 成功获取锁!
线程 T1 成功获取锁!
线程 T8 成功获取锁!
线程 T9 成功获取锁!

这里我们只需要对比开始获取锁...成功获取锁!的顺序是否一致即可,如果是一致,那说明所有的线程都是按顺序排队获取的锁,如果不是,那说明肯定是有线程插队了。

运行结果可以发现,在公平模式下,确实是按照顺序进行的,而在非公平模式下,一般会出现这种情况:线程刚开始获取锁马上就能抢到,并且此时之前早就开始的线程还在等待状态,很明显的插队行为。

那么,接着下一个问题,公平锁在任何情况下都一定是公平的吗?有关这个问题,我们会留到队列同步器中再进行讨论。

【Java八股文之进阶篇(三)】多线程编程核心之锁框架(一)相关推荐

  1. Java面试题-进阶篇(2022.4最新汇总)

    Java面试题-进阶篇 1. 基础篇 1.1 基本数据类型和包装类 1.2 Double转Bigdecimal可能会出现哪些问题?怎么解决? 1.3 equals 与 == 的区别? 1.4 Java ...

  2. Android日志[进阶篇]三-Logcat 命令行工具

    Android日志[进阶篇]一-使用 Logcat 写入和查看日志 Android日志[进阶篇]二-分析堆栈轨迹(调试和外部堆栈) Android日志[进阶篇]三-Logcat命令行工具 Androi ...

  3. 计算机快捷键桌布,桌面改造 篇三:编程娱乐两不误 | 伪程序猿的Windows双屏组建/效率工具/桌面美化指南...

    桌面改造 篇三:编程娱乐两不误 | 伪程序猿的Windows双屏组建/效率工具/桌面美化指南 2020-07-10 11:41:39 153点赞 1107收藏 74评论 哈喽大家好,我是码呆茶!作为一 ...

  4. java多线程基础视频_【No996】2020年最新 Java多线程编程核心基础视频课程

    01.课程介绍.mp4 02.多线程编程基础-进程与线程.mp4 03.多线程编程基础-使用多线程-继承Thread类.mp4 04.多线程编程基础-使用多线程-实现Runnable接口.mp4 05 ...

  5. python的枚举和for循环_python入门与进阶篇(三)之分支、循环、条件与枚举,python枚举...

    python入门与进阶篇(三)之分支.循环.条件与枚举,python枚举 python开发工具 IDE Vscode插件官网 https://marketplace.visualstudio.com/ ...

  6. 【Java】Mybatis进阶篇(一)

    Mybatis进阶篇 核心配置文件 1.mybatis-config.xml 2.Mybatis 的配置文件包含了会深深影响Mybatis行为的设置和属性信息 Configuration(配置) pr ...

  7. 《android进阶之光》——多线程编程(上)

    今天了解了下多线程编程,知识点如下: 进程与线程: 进程是什么?线程是什么? 进程可以看作是程序的实体,是线程的容器,是受操作系统管理的基本运行单元,例如exe文件就是一个进程. 线程是进程运行的一些 ...

  8. Java - 日志(进阶篇)

    一.日志门面 当我们的系统变的更加复杂的时候,我们的日志就容易发生混乱.随着系统开发的进行,可能会更新不同的日志框架,造成当前系统中存在不同的日志依赖,让我们难以统一的管理和控制.就算我们强制要求所有 ...

  9. JUC笔记(三)多线程的核心

    多线程的核心 锁框架 Lock和Condition接口 可重入锁 公平锁与非公平锁 读写锁 锁降级和锁升级 队列同步器AQS 底层实现 公平锁一定公平吗? Condition实现原理 自行实现锁类 原 ...

最新文章

  1. hdu-Calculation 2(欧拉函数)
  2. mysql5.7.32 win7_拯救10年前老爷机:C盘不到3G的Win7官方精简版amp;俄大神精简版分享...
  3. 修改WampServer的默认端口
  4. 租金 预测_如何预测租金并优化租赁期限,从而节省资金
  5. LeetCode 1708. 长度为 K 的最大子数组
  6. python网络验证系统_python3+django2 开发易语言网络验证(下)
  7. 算法导论 思考题9-2
  8. Mac系统下运行Java项目出现Unable to start embedded Tomcat server解决方法
  9. lsof u mysql wc l_lsof命令详解
  10. 如何使用预览在 Mac 上将 HEIC 文件更改为 JPEG
  11. 电视家鸿蒙系统,ZNDS智能电视强烈推荐:机顶盒上好用的四款软件!
  12. 计算机两个账户共享文件,两台电脑如何共享文件,简简单单六步即可实现文件共享...
  13. 滴滴竟然已经投资了这么多公司?
  14. 对比LDA,NCA,PCA
  15. 研究B站个人收藏中已失效的视频
  16. 如何快速融入团队并成为团队核心(四)
  17. 第二章作业题1-顺序表-计算机17级 7-1 jmu-ds-集合的并交差运算 (15 分)
  18. 顶尖量化交易公司 CEO 如何缔造量化金融王国?
  19. 拔毒化腐生肌药题库【1】
  20. 园林工程计算机教程,园林设计全攻略电子教程第1章 园林设计与计算机制图.ppt...

热门文章

  1. Nginx编译安装与配置
  2. Ptr ds 与ptr ss
  3. linux ip命令
  4. AD生成BOM表_材料清单 (Bill of Material)
  5. CRS-4544: Unable to connect to OHAS has启动失败
  6. 主板usb接口全部失灵_主机usb接口没反应,台式电脑usb接口全部失灵
  7. vscode配置php运行环境以及xdebug
  8. 图片img标签设置默认图片
  9. python DEA: 基于非径向距离NDDF的Malmquist-Luenberger 指数及其分解
  10. 【深度强化学习】(6) PPO 模型解析,附Pytorch完整代码