1、为什么要引入线程阻塞机制?
为了解决共享存储区域的访问冲突,java引入了同步机制。 现在让我们检查多线程对共享资源的访问。 显然,同步机制是不够的,因为任何时候所需的资源都可能没有准备好。 为了依次访问,可能有多个资源同时准备就绪。 为了解决这种情况下的访问控制问题,Java引入了对阻塞机制的支持。
阻塞是指暂停线程的执行,等待某个条件的发生(比如某个资源准备好),学过操作系统的同学一定很熟悉。 Java提供了大量的方法来支持阻塞,我们来一一分析。
二、Java中线程阻塞的方法:
(1)线程睡眠:.sleep(long)方法使线程进入阻塞状态。 该参数设置睡眠时间,以毫秒为单位。 当sleep结束后,转入ready()状态。 sleep()具有良好的平台可移植性。
(2)线程等待:类中的wait()方法使当前线程等待,直到其他线程调用该对象的()唤醒方法。 这两个唤醒方法也是类中的方法,它们的行为相当于调用wait()。 wait()和()方法:两个方法一起使用,wait()使线程进入阻塞状态,它有两种形式,一种允许指定一段以毫秒为单位的时间作为参数,另一种没有参数,前者当调用相应的()或者超过指定时间线程重新进入可执行状态时,后者必须由相应的()调用。
(3)线程礼让,.yield()方法,挂起当前正在执行的线程对象,将执行机会让给相同或更高优先级的线程。 Yield()使线程放弃当前分配的CPU时间,但不会阻塞线程,即线程仍处于可执行状态,随时可能再次分配CPU时间。 调用yield()的效果相当于调度程序认为线程已经执行了足够的时间来切换到另一个线程。
(4)线程是自关闭的,join()方法等待其他线程终止。 如果当前线程中调用了另一个线程的join()方法,则当前线程会转入阻塞状态,直到另一个进程运行完毕,然后当前线程才会从阻塞状态转为就绪状态。
(5)()和()方法:两种方法一起使用。 ()使线程进入阻塞状态,并且不会自动恢复。 必须调用相应的()才能使线程重新进入可执行状态。 通常,()和()用于等待另一个线程产生结果:测试后发现结果还没有产生,让该线程阻塞,等另一个线程产生结果后,调用()使其产生恢复。 ()和()中的两个方法在JDK1.5中已经废除,不再引入。 因为有死锁倾向。
这里,作者放了一张线程生命周期的经典图来帮助读者理解,展示了一个线程从创建->运行->阻塞->运行->死亡的全过程:
3.常用线程术语解释
主线程:JVM调用程序main()产生的线程。
当前线程:这是一个令人困惑的概念。 一般指通过.()得到的过程。
后台线程:指为其他线程提供服务的线程,也称为守护线程。 JVM的垃圾收集线程是后台线程。用户线程和守护线程的区别在于是否等待主线程结束取决于主线程的结束
前台线程:指接受后台线程服务的线程。 事实上,前台和后台线程是环环相扣的,就像幕后的傀儡和操纵者的关系一样。 傀儡是前台线程,幕后操纵器是后台线程。 前台线程创建的线程默认也是前台线程。 可以使用()和()方法来判断和设置一个线程是否为后台线程。
可见进程:可见进程是指一些不在前台,但对用户仍然可见的进程,例如:各种、输入法等,都属于它。 这部分进程虽然不在前台,但与我们的使用密切相关,我们不希望它被系统终止。
“前台的可见进程为后台的空进程服务”——这就是记录线程重要性的公式,
重要性下降一次,即前台进程>可见进程>服务进程>后台进程>空进程。
线程类的一些常用方法:
sleep():强制线程休眠 N 毫秒。
():判断线程是否存活。
join():等待线程终止。
():程序中活动线程的数量。
():枚举程序中的线程。
():获取当前线程。
():线程是否为守护线程。
():设置一个线程为守护线程。 (用户线程和守护线程的区别在于是否等待主线程结束取决于主线程的结束)
():为线程设置一个名称。
wait():强制线程等待。
():通知线程继续运行。
():设置线程的优先级。
补充:java中处理线程阻塞的技巧
在java中,我们使用多线程来处理一些业务。 如果业务比较复杂,并发量相当大,很有可能会出现线程阻塞的问题。
案件:
有一个触发接口。 根据触发的信息,内部开启多个线程执行业务。 每个线程会执行两类业务:私有业务(如调用不同接口)、公共业务(如执行存储、mq发送等),当私有业务处理时间很快而公共业务处理时间为比较长,在这样的场景下,私有业务和公共业务可以分为不同的线程来执行。
例如:
当这个接口被触发时,根据接口触发的信息,需要开启10个线程,那么可以创建10个线程来执行其私有业务,然后再创建一个线程来获取该线程的执行返回结果前 10 个线程 并处理公共事务。
这样做的好处是线程池可以快速回收线程,可以有效防止线程阻塞
量化:
单个私有业务可在1秒内执行,单个公共业务可在5秒内执行。 如果接口触发后发现需要创建100个线程来执行,那么线程池需要等待至少6秒才能回收这些线程池。 如果按照前面说的分成两个线程,那么需要创建101个线程,1秒后可以回收100个执行完毕的线程
但这里需要权衡。 如果触发该接口,发现需要开启的线程较多,公共服务比较耗时。 在这种情况下,只有一个线程同步执行公共服务。 那么这个线程就会执行比较长的时间。 因此,在进行公共服务时,也可以根据实际情况开启多个线程。
这是一个小演示:
1. 私营企业类别:
@
班级 {
校准(要求,整数a,整数b){
.out.("请求id:" + req + "结果:" + (a + b));
新(要求,a + b);
2. 公共业务类别:
@
班级 {
空白 ( ) {
尝试 {
.out.(.().() + " :开始做其他事情,请求数量:" + .() + " ,请求结果:" + .());
.睡眠(2000);
.out.(.().() + " :完成其他事情,请求编号:" + .() + " ,请求结果:" + .());
} 捕获 (e) {
3、私有业务的线程类:
班级 {
要求;
整数a;
整数b;
@
称呼() {
= 主要..(.class);
.cal(请求, a, b);
(Strin req, int a, int b) {
这。 请求=请求;
这个.a = a;
这个.b = b;
// 等等。
4.公共业务的线程类:
班级 {
CS;
整数;
(CS,整数){
这个.cs = cs;
这。 = ;
@
无效运行(){
一些=主要..(.class);
();
空白 ( ) {
尝试 {
for (int i = 0; i < ; i++) {
take = cs.take();
= 采取。 得到();
.();
} 捕获 (e) {
// 等等。
6、测试main方法:
@
类主要有{
= 空;
无效主([]参数){
=新的(“.xml”);
= .(100);
cs = new rvice();
//这里启动执行计算的线程
cs.(new("", 0, 1));
cs.(new("", 0, 2));
cs.(new("", 0, 3));
cs.(new("", 0, 4));
cs.(new("", 0, 5));
//专用监控线程,并执行其他耗时线程
.(新(cs,5));
.();
.();
@
无效t(){
这。 = ;
执行结果如下:
核心思想:将多线程公共业务提取出来(前提是公共业务比较耗时,否则没有必要),在其他线程中执行。