多线程(JavaEE初阶系列3)

news2025/1/19 19:22:22

目录

前言:

1.中断一个线程

2.等待一个线程-join()

2.1join()无参调用的使用

2.2join()有参调用的使用

3.线程的状态

3.1观察线程的所有状态

4.多线程带来的风险—线程安全

4.1观察线程不安全

4.2该问题出现的原因

4.3线程不安全问题的解决

4.3.1synchronized关键字解决

4.3.1.1synchronized的特性

4.3.1.2synchronized的工作过程

4.3.1.3synchronized的使用示例

4.3.2volatile关键字保证内存可见性

4.3.3volatile关键字禁止指令重排序

4.3.4volatile关键字不保证原子性

4.3.5wait()方法与notify()方法

4.3.6notifyAll()方法

4.4wait和sleep对比

结束语:


前言:

上一节中小编与大家分享了多线程的概念、线程与进程之间的区别与联系以及教大家如何创建一个线程和Thread的一些属性,那么这节中小编就带着大家继续来了解线程中的知识点。

1.中断一个线程

中断这里就是字面意思,就是让一个线程停下来,让线程终止。例如下面的这个例子:
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到了我们停止线程的方式了。

目前常见的有以下两种方式:

  • 通过共享的标记阿里进行沟通。
  • 调用interrupt()方法来通知。

①给线程中设置一个结束标志位。

代码展示:

package Thread;
//使用结束标志位
public class ThreadDemo8 {
    public static boolean isQuit = false;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!isQuit) {
                System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t 线程终止!!!");
        });
        t.start();
        //在主线程中修改isQuit
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isQuit = true;
    }
}


结果展示:

 

与我们之前最大的区别就是在while循环的条件上增加了一个标志量这样就可以在主线程中控制标志量,从而结束线程了。

当然在Thread类中内置了一个标志位,让我们更方便实现上述的效果。

代码展示:

package Thread;

public class ThreadDemo9 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            //currentThread 是获取到当前线程实例
            //此处currentThread 得到的对象就是t
            //isInterrupted 就是t对象里面自带的一个标志位
           while (!Thread.currentThread().isInterrupted()){
               System.out.println("hello t");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
//                   try {
//                       Thread.sleep(2000);
//                   } catch (InterruptedException ex) {
//                       ex.printStackTrace();
//                   }
//                   break;
               }
           }
        });
        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //把t内部的标志位给设置成true
        t.interrupt();
    }
}


结果展示: 

在上述中我们是通过Thread的内置类首先调用currentThread来获取到当前线程的对象,然后调用该对象中的isInterrupted来设置标志量,在主线程中我们使用对象.interrupt来将其在设置成true使得该线程在阻塞中(比如在执行sleep)此时就会把阻塞状态唤醒,通过抛出异常的方式让sleep立即结束。

注意:当sleep被唤醒的时候,sleep会自动把isInterrupted标志位给清空(true——>false)这样就会导致我们进入下次的while循环,也就是我们上述看到结果中的报错情况。

这个就像是我们生活中遇到的开关,有的开关就是按下去就按下去了,而有的开关是按下去之后会自动回弹,我们这里的sleep就和后者一样,唤醒之后会自动清空isInterrupted的标志位。那么为什么sleep要清空标志位呢?目的就是为了让线程能够对于线程何时结束,有一个更明确的控制。sleep在这里是一种“通知”而不是“命令”。

所以这里就会涉及到三种情况:

①直接无视“通知”。

 ②立即执行“通知”。

③等一会在做。

这就像是我们在家里,当我们在打游戏的时候,父母让我下楼买菜此时我们就会出现三种情况:一种是直接无视父母的要求,继续打着游戏;第二种是立即下楼去买;第三种是给父母说等一会再去,先把这关打完再去买。 

2.等待一个线程-join()

有时我们需要等待一个线程完成它的工作之后,才能进行自己的下一步工作,例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。

操作系统是并发执行的,操作系统对于线程的调度,是无序的。无法判定两个线程谁先执行结束,谁后执行结束。就像是下面的这个例子,此时我们就不能判断是先输出“hello main”还是“hello t”。

join的方法:

方法

说明

public void join()等待线程结束
public void join(long millis)等待线程结束,最多等millis毫秒
public void join(long millis, int nanos)同理,但可以更高精度

2.1join()无参调用的使用

代码展示:

package Thread;

public class ThreadDemo10 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello t");
        });
        t.start();
        System.out.println("hello main");
    }
}

结果展示:

但是一般来说程序猿是不喜欢不确定的,所以有时候需要明确规定线程的结顺序,这里我们就可以使用线程等待来实现(join方法)。

代码展示:

package Thread;

public class ThreadDemo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello t");
        });
        t.start();
        t.join();
        System.out.println("hello main");
    }
}


结果展示:

分析:此时我们就可以明确的看到先打印的是“hello t”然后再打印“hello main”了,这里在上述的位置上加上t.join()就表示在t线程还没有结束的时候,main线程就进入到阻塞状态等待,直到t线程结束才执行main线程。详细看下述讲解:

  • main线程调用t.join的时候,如果t还在运行此时main线程阻塞,直到t执行完毕(t的run执行完了),main才从阻塞中解除,才继续执行。
  • main线程调用t.join的时候,如果t已经结束了,此时main线程就不会阻塞,就会继续往下执行。

2.2join()有参调用的使用

  • join的无参版本,效果是“死等”(不见不散)。
  • join的有参版本,则是指定最大超时时间,如果等待时间到了上限,还没等到,也就不在等了。

3.线程的状态

3.1观察线程的所有状态

线程的状态是一个枚举类型Thread.State。操作系统里的线程,自身是有一个状态的,但是Java Thread是对系统线程的封装,把这里的状态又进一步的细化了。

  • NEW:安排工作还没开始行动,就是系统中的线程还没创建出来呢,只有一个Thread对象。
  • RUNNABLE:就绪状态,里面又分为两种一种是正在CPU上运行,另一种是准备好随时可以去CPU上运行了。
  • BLOCKED:表示等待锁出现的状态。
  • WAITING:使用with方法出现的状态,表示排队等着。
  • TIMED_WAITHING:在指定时间内等待.sleep方法。
  • TERMINATEND:表示系统中的线程已经执行完了,Thread对象还在。

线程的状态转化图: 

 注意:这里的细节后期小编还会继续补充的!!!

我们可以通过getState()方法来获取状态。

代码展示:

package Thread;
//状态的获取
public class ThreadDemo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                //为了防止hello把线程状态冲没了,先注释掉
                //System.out.println("hello t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //在启动之前,获取线程状态NEW
        System.out.println(t.getState());
        t.start();
        Thread.sleep(2000);
        System.out.println(t.getState());
    }
}


结果展示:

4.多线程带来的风险—线程安全

4.1观察线程不安全

首先我们先来观察一个线程不安全的例子。

代码展示:

package Thread;
//线程不安全
class Counter{
    private int count = 0;
    public void sub() {
        count++;
    }
    public int get() {
        return count;
    }
}
public class ThreadDemo12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        //搞两个线程,两个线程分别对这个counter自增5w次。
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.sub();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.sub();
            }
        });
        t1.start();
        t2.start();
        //等待两个线程执行结束,然后看结果。
        t1.join();
        t2.join();
        System.out.println(counter.get());
    }
}


结果展示: 

 

我们本来预想的是两个都自增5W次那么总数应该是10W,结果我在上述结果中看到的却是一个意想不到的数字。这是什么原因造成的呢?这就出现了实际结果与预想结果不符的情况,就是一个bug,这就是有多线程引起来的一个bug,这里就引发出了两个问题线程不安全/线程安全问题。

4.2该问题出现的原因

上述代码中的count++操作本质上是三个CPU指令构成:

  1. load:把内存中的数据读取到CPU寄存器中。
  2. add:就是把寄存器中的值进加1运算。
  3. save:把寄存器中的值写回到内存中。

下面我们通过画图来解释一番:

注意:此处这两线程的指令排列顺序(执行的先后)有很多种的排列情况!!! 

不同的排列顺序下,执行结果是截然不同的,下面来给大家演示一下具体的执行过程。

在上述的演示中我我们可以看到本来两次自增后的结果应该是2,但是最终写回内存中的结果是1,所以此时就出现bug了,本质是一次自增的结果被覆盖掉了,所以才会出现bug。在上述众多的调用序列中只有②和③的结果最终会是正确的其他的都会出现bug。

所以归根结底,线程安全问题,全是因为,线程的无序调度/随机调度(抢占式执行),导致了执行顺序不确定,结果就变化了!!!

所以通过上述我们可以总结出线程不安全的原因有:

  • 抢占式执行。
  • 多个线程修改同一个变量。
  • 修改操作,不是原子的。
  • 内存可见性,引起的线程不安全,即一个线程对共享变量的修改,能够及时地被其他线程看到。
  • 指令重排序,引起的线程不安全。

解释:

原子是可不分割的最小单位,在上述中我们可以看到在执行调度的时候不是只有一个指令而是由三个指令构成的(load、add、save)。

4.3线程不安全问题的解决

在上述中我们给大家讲解和分析了线程不安全的原因,那么如何解决线程不安全的问题,我们还需要“解铃还须系铃人”,从问题本质出发来解决它。

4.3.1synchronized关键字解决

首先是原子性问题,要想让他变成原子性问题那么我们就可以让其进行加锁,这样我们就可以保证“原子性”的效果了。

这里我们提到了锁的概念,那么的核心操作有两个:加锁和解锁

原理:一旦某个线程加锁之后,其他线程也想加锁,就不能直接加上了,就需要阻塞等待,一直等到拿到锁的线程释放了锁为止。

那么Java中是如何加锁的呢?在Java中有一个关键字“synchronized”,加上这个关键字就达到了加锁的效果。

4.3.1.1synchronized的特性

  • 互斥
  • 刷新内存
  • 可重入

①互斥

synchronized会引起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待。

②刷新内存

当进入到synchronized中的时候会读取内存中的数据,在修改完数据之后,就会有一个刷新内存的操作,将内存中的数据进行修改。

③可重入

synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。

那么这里提到了一个“死锁”的概念,下面给大家解释一下什么是死锁的情况。

按照我们对锁的设定,在第二次加锁的时候,就会出现阻塞等待(第一个锁还没释放的前提下),直到第一次锁释放,才能获取到第二个锁,但是释放第一个锁也是由第一个线程完成的,结果这个线程现在不想动了,已经躺平了,也就无法进行解锁操作了,这时就会出现死锁。这样的锁也叫“不可重入锁”。

 注意:在Java中synchronized是可重入锁,因此就没有上述问题的出现。

4.3.1.2synchronized的工作过程

  1. 获得互斥锁。
  2. 从主内存拷贝变量的最新副本到工作的内存。
  3. 执行代码。
  4. 将更改后的共享变量的值刷新到主内存。
  5. 释放互斥锁。

下面我们使用代码来演示一下:
代码展示:

package Thread;
//synchronized加锁机制
class Counter1{
    private int count = 0;
    private Object locker = new Object();
    public void add() {
        synchronized (locker) {
            count++;
        }
    }
    public void sub() {
        count++;
    }
    public int get() {
        return count;
    }
}
public class ThreadDemo13 {
    public static void main(String[] args) throws InterruptedException {
        Counter1 counter = new Counter1();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(counter.get());
    }
}


结果展示:

在上述代码的结果中我们可以看到两个自增5W次之后得要的结果是10W,与我们预期的结果是一样的,说明代码是正确的。

锁有两个核心操作:加锁和解锁。

此处使用代码块来表示在进入synchronized的{}中就会出发加锁机制。出了代码块之后就会触发解锁机制。上面的locker就是一个锁对象,表示你在针对于那个对象加锁,如果两个线程,针对同一个对象加锁,此时就会出现“锁竞争” (一个线程拿到了锁,另一个线程阻塞等待),如果两个线程针对不同对象加锁,此时就不会出现“锁竞争”各自获取锁即可。

()里面的锁对象,可以是写作任意一个Object对象(内置类不行),如果此处写了this,就相当于是Counter counte = new Counter();这里的counter

下面小编举个例子方便大家对于锁机制的理解。

当t1和t2都竞争同一个锁对象的时候,就像是我们在上厕所的时候,如果此时只有一个厕所,那么其他人就得排队,进去的那个人会给厕所上锁,等到这个人出来之后才会释放掉这个锁,其他人再去竞争这个锁谁先拿到这个锁谁就进去上厕所。 

但是如果是多个厕所的话,就不需要去竞争同一个锁对象了。如果这个锁对象被上锁了,那么其他人就可以去找其他锁对象。 

那么经过上述的这个例子相信大家可以更好的理解锁对象的机制。

通过“锁”就很好的解决了我们原子性的问题。这样就可以保证++操作就是原子性的,不受影响了。其实这里的加锁本质上就是将并行的变成了串行的。 

4.3.1.3synchronized的使用示例

①直接修饰普通方法

代码展示:

package Thread;
//1直接修饰普通方法:锁的是ThreadDemo14对象
public class ThreadDemo14 {
    public synchronized void fun() {

    }
}

②修饰静态方法

代码展示:

package Thread;
//修饰静态方法:锁的ThreadDemo15类的对象
public class ThreadDemo15 {
    public synchronized static void fun() {
        
    }
}

③修饰代码块

修饰代码块的时候它明确指定了锁哪个对象。

  • 锁当前对象

                代码展示:

package Thread;
//3.修饰代码块
//3.1锁当前对象
public class ThreadDemo16 {
    public void fun() {
        synchronized (this) {
            
        }
    }
}
  • 锁类对象

                代码展示:

package Thread;
//3.修饰代码块
//3.2锁类对象
public class ThreadDemo17 {
    public void fun() {
        synchronized (ThreadDemo17.class){
            
        }
    }
}

4.3.2volatile关键字保证内存可见性

另一个线程不安全的场景是由内存可见性,引起的线程不安全问题。

先看下面的这个实例:
代码展示:

package Thread;

import java.util.Scanner;

public class ThreadDemo18 {
    public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {
                //空着
            }
            System.out.println("循环结束!t1结束!");
        });
        Thread t2 = new Thread(() ->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}


结果展示:

分析:

预期效果:应该是t1通过flag == 0作为条件进行循环初识情况将进入循环,t2通过控制台输入一个整数,一旦用户输入了非0的值,此时t1就会立即结束,从而t1线程退出!!!

实际结果:输入非0的值之后t1线程并没有退出循环没有结束通过jconsole可以看到t1线程仍然处于RUNNABLE状态。

此时预期结果 != 实际结果,就产生了bug了。

 那么为什么会产生这种状况呢?这就是内存可见性的锅。

在while循环中,flag == 0,会有两步操作:

  • 首先是进行load操作,先将数据从内存中读取到CPU寄存器中。
  • 然后再进行CMP操作,比较寄存器里的值是否是0。

注意:此处的这两操作,load的时间开销远远高于cmp!!!读内存虽然比读硬盘来的快,但是读寄存器,比读内存又要快几千倍!!!

上述的这些操作电脑一秒钟就要执行上亿次,此时编译器就会发现load的开销很大,而且每次load的结果都是一样的,此时编译器就有一个大胆的操作,把load就给优化掉了,也就是说只有第一次执行的时候,load才真正的执行了,后续循环都只有cmp操作,没有load操作相当于是直复制了前面第一次的load的值。

这里我们提到了编译器优化,那么什么是编译器优化呢?先来给大家讲解一下编译器优化的原理。

编译器优化是一个非常普遍的事情,就是能够智能的调整你的代码逻辑,保证程序结果不变的前提下,通过加减语句,通过语句变换,通过一些列操作,让整个程序执行的效率大大提升。

编译器对于“程序结果不变”在单线程情况下判定是非常准确的,但是在多线程中就不一定了,可能就会导致调整之后,效率是提高了,但是结果却变了,也就是编译器出现了误判从而导致了bug。

所谓的内存可见性,就是在多线程的环境下,编译器对于代码优化,产生了误判,从而引起bug,进一步导致了代码的bug。此时咱们的处理方式就是让编译器针对这种场景暂停优化,此时我们就用到了一个关键字“volatile”

被“volatile”修饰的变量,此时编译器就会禁止上述的优化能够保证每次都是从内存中重新读取数据。

代码展示:

package Thread;

import java.util.Scanner;

public class ThreadDemo18 {
    volatile public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {
                //空着
            }
            System.out.println("循环结束!t1结束!");
        });
        Thread t2 = new Thread(() ->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}


结果展示:

我们在此处加上volatile关键之后就能保证每一次都会进行load操作,编译器每一次都会去内存中重新读取数据到CPU寄存器中了。

此时t2修改flag变量t1就可以感知到了,程序就可以正确退出了。

还需要大家注意的一点是在while循环中是空着的,如果加上我之前的sleep操作,可能就不会出现上述的内存可见性的问题了。

  

上述的volatile这个效果称为“保证内存可见性”。 

volatile还有一个效果是禁止指令重排序。接下来我们就看看是怎么禁止的吧!

4.3.3volatile关键字禁止指令重排序

指令重排序也是编译器优化的策略,他是调整了代码的执行顺序,让程序变得更高效,前提是在保证整体逻辑不变的情况下。

例如下面的这个例子:
在家里的时候父母可能会经常让大家去超市里买菜,会有一个购物清单比如是:1.西红柿 2.鸡蛋 3.黄瓜 4.芹菜。

那么我进入超市之后就按照上述的顺序采买,采买路线如下图所示。

会发现上述的路线采买图效率并不高。

那么如果我们调整顺序先买黄瓜、鸡蛋、芹菜最后再买西红柿这样效率就会大大的提高。如下所示:
 谈到优化,都得保证调整之后的结果和之前的结果是不变的,单线程下容易保证,如果是多线程,就不好说了。

下述代码就可能出现指令重排序。

伪代码:
Student s;

t1 :

        s = new Student();

t2:

        if(s != null) {
                s.learn();

        }

上述t1中的 s = new Student(); 大体可以分为三步操作:

  1. 申请空间。
  2. 调用构造方法(初始化内存的数据)。
  3. 把对象的引用赋值给s(内存地址的赋值)。

如果是在单线程的情况下此处就可以进行指令重排序了,1可定是最先执行的,但是2 和 3谁先谁后都可以。

但是在多线程的情况下如果t1是先执行1再执行3最后执行2,按照这样的顺序执行的话,当t2开始执行的时候由于t1的3已经执行过了,这个引用就已经是非空了,所以t2就会尝试调用s.learn(),但是由于t1只是赋值了,还没有初始化,此时的learn会成啥样,就不知道了,很可能就会产生bug!!!

所以我们使用关键字“volatile”就会禁止指令重排序,就会避免上述的问题了。

4.3.4volatile关键字不保证原子性

我们上述学习了synchronized,他可以保证原子性,但是volatile是不保证原子性的,它只是保证了内存的可见性。

4.3.5wait()方法与notify()方法

由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知。但是由于实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。

我们之前学习了join算是一种控制顺序的方式,但是工序有限。所以我我们现在又提出来了wait和notify。

  • wait就是让某个线程先暂停下来等一等。
  • notify就是把该线程唤醒,能够继续执行。

举个例子:
滑稽1号现在在取钱进去ATM机之后发现没有钱,1号就出来了(解锁),接下来哪个滑稽会进去取钱说不定,1号滑稽也可能会再次进去,也就是具体哪个线程能够拿到锁不一定这就会导致一种极端情况,1号滑稽一直处于一个进进出出的状态,就会使得线程饿死

代码展示:

package Thread;

public class ThreadDemo19 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("wait 之前");
        synchronized (object) {
            object.wait();
        }
        System.out.println("wait 之后");
    }
}


结果展示:

此时我们可以借助jconsole来查看线程的运行状态是处于WAITING。

 

但是我们如果使用wait/notify就可以有效的解决上述问题了。

1号滑稽,发现ATM机没有钱就wait,wait就会释放锁,并进行阻塞等待。(暂时不参与CPU的调度,不参与锁竞争了),当另外一个线程给ATM机里充上钱,就可以唤醒1号了。 

  • wait:发现条件不满足/时机不成熟,就先阻塞等待。
  • notify:其他线程构造了一个成熟的条件,就可以唤醒1号了。

代码展示:

package Thread;

public class ThreadDemo20 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println("wait 之前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("wait 之后");
        });
        t1.start();

        Thread.sleep(1000);

        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        });
        t2.start();
    }
}


结果展示:

解释:
t1先执行到wait就进入了阻塞状态,1s之后t2开始执行,执行到notify就会通知t1线程唤醒(注意:notify是在synchronized内部,所以就需要等t2释放了锁之后才能继续往下走!!!

wait做的事情:

  • 使用当前执行的代码的线程进行等待(把线程放到等待队列中)。
  • 释放当前锁。
  • 满足一定条件时被唤醒,重新尝试获取这个锁。

注意:wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常!!!

wait结束等待的条件:

  • 其他线程调用该对象的notify方法。
  • wait等待时间超时(wait方法提供了一个带有timeout参数的版本,来指定等待的时间)。
  • 其他线程调用该等待线程的interrupt方法,导致wait抛出InterruptedException异常。

notify方法是唤醒等待线程:

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象锁的其他线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则由线程调度器随机挑选出一个呈wait状态的线程。(并没有先来后到)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

4.3.6notifyAll()方法

唤醒操作除了上述的notify还有一个是notifyAllnotify只是会随机唤醒一个等待线程,而使用notifyAll方法可以一次唤醒所有的等待线程。

下面举个例子让大家更好理解一下。

notify:只唤醒等待队列中的一个线程,其他的线程还是得乖乖的等着。

 

 

 notifyAll:一下子全部都唤醒,需要这些线程重新竞争锁。

4.4wait和sleep对比

wait有一个带参数的版本,用来体现超时时间,这个时候,感觉好像和sleep差不多,wait也能提前唤醒,sleep也能提前唤醒。

但是它两最大的区别就是在于“初心不同”。

  • wait解决的是线程之间的顺序控制问题。
  • sleep单纯是让当前线程休眠一会。

②同时在使用上也有所不同wait需要搭配synchronized来使用,而sleep不需要。

③wait是Object的方法,sleep是Thread的静态方法。

结束语:

这节中小编主要与大家分享了有关于如何中断一个线程,线程的状态、线程的安全问题以及他的解决方法,希望这节对大家学习JavaEE有一定的帮助,想要学习的同学记得关注小编和小编一起学习吧!如果文章中有任何错误也欢迎各位大佬及时为小编指点迷津(在此小编先谢过各位大佬啦!)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/779645.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【PostgreSQL内核学习(九)—— 查询执行(数据定义语句执行)】

数据定义语句执行 概述数据定义语句执行流程执行示例 声明&#xff1a;本文的部分内容参考了他人的文章。在编写过程中&#xff0c;我们尊重他人的知识产权和学术成果&#xff0c;力求遵循合理使用原则&#xff0c;并在适用的情况下注明引用来源。 本文主要参考了《PostgresSQL…

【Python数据分析】Python基础知识篇

&#x1f389;欢迎来到Python专栏~Python基础知识篇 ☆* o(≧▽≦)o *☆嗨~我是小夏与酒&#x1f379; ✨博客主页&#xff1a;小夏与酒的博客 &#x1f388;该系列文章专栏&#xff1a;Python学习专栏 文章作者技术和水平有限&#xff0c;如果文中出现错误&#xff0c;希望大…

vue中使用原生的table合并行

完整的代码&#xff1a; <template><table border"1"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody><template v-for"(item, index) in tableData"><templat…

Hbase基本原理剖析

一、基本原理 数据存储使用HBase来承接&#xff0c;HBase是一个开源的、面向列&#xff08;Column-Oriented&#xff09;、适合存储海量非结构化数据或半结构化数据的、具备高可靠性、高性能、可灵活扩展伸缩的、支持实时数据读写的分布式存储系统。更多关于HBase的信息&#…

fpga4fun—发光二极管

发光二极管电子基础知识 LED&#xff08;发光二极管&#xff09;是一种半导体器件&#xff0c;当电流通过它时会产生光。 LED 符号看起来像一个二极管&#xff0c;带有阳极 &#xff08;&#xff09; 和阴极 &#xff08;-&#xff09;。 LED 的作用类似于二极管 - 单向导电&…

电脑新装系统优化,win10优化,win10美化

公司发了新的笔记本&#xff0c;分为几步做 1.系统优化,碍眼的关掉。防火墙关掉、页面美化 2.安装必备软件及驱动 3.数据迁移 4.开发环境配置 目录 目录复制 这里写目录标题 目录1.系统优化关掉底部菜单栏花里胡哨 2.安装必备软件及驱动新电脑安装360 1.系统优化 关掉底部菜单…

linux驱动开发入门(学习记录)

2023.7.6及7.7 概述了解 一 1.驱动框架 2. 字符设备 块设备&#xff0c;存储相关 网络设备驱动 不一定属于某一种类型二 1.获取外设或传感器数据&#xff0c;控制外设&#xff0c;数据会提交给应用程序 2.编写一个驱动&#xff0c;及测试应用程序 app。驱动和应用完全分开 3.驱…

程序员进阶之路:程序环境和预处理

目录 前言 程序的翻译环境和执行环境 翻译环境 运行环境 预处理&#xff08;预编译&#xff09; 预定义符号 #define #define 定义标识符 #define 定义宏 #define 替换规则 #和## #的作用 ##的作用 带副作用的宏参数 宏和函数对比 命名约定 #undef 命令行定义 条件…

通过v-for生成的input无法连续输入

部分代码&#xff1a;通过v-for循环生成el-form-item&#xff0c;生成多个描述输入框 更改之前的代码&#xff08;key绑定的是item&#xff09;&#xff1a; <el-form-item class"forminput" v-for"(item,index) in formdata.description" :key"…

【Linux -- 优先级 -- nice,renice 】

Linux – 优先级 – nice,renice 文章目录 Linux -- 优先级 -- nice,renice一、优先级1.Priority(PRI)2.nice(NI) 二、nice命令三、renice命令总结 一、优先级 CPU调度是指每个进程被CPU运行的规则,如果休眠的进程同时被唤醒,那CPU应该先处理哪个进程呢? 1.Priority(PRI) L…

Langchain 和 Chroma 的集成

Langchain 和 Chroma 的集成 1. Chroma2. 基本示例​3. 基本示例(包括保存到磁盘)4. 将 Chroma Client 传递到 Langchain ​5. 基本示例(使用 Docker 容器)6. 更新和删除7. 带分数的相似性搜索​ 1. Chroma Chroma 是一个人工智能原生开源矢量数据库&#xff0c;专注于开发人员…

Unity游戏源码分享-三国群英传

Unity游戏源码分享-三国群英传 完整版 工程地址&#xff1a;https://download.csdn.net/download/Highning0007/88069201

vue-element-template管理模板(二)

vue-element-admin框架 动态路由&#xff08;二&#xff09; 修改代码&#xff1a; import { asyncRoutes, constantRoutes } from "/router"; import { getMenu } from "/api/user"; import Layout from "/layout";/*** Use meta.role to det…

PCB 封装焊盘盖油了,什么原因?

PCB 封装焊盘盖油了&#xff0c;什么原因&#xff1f; 背景&#xff1a;当PCB切换到3D视图检查错误时&#xff0c;突然发现某个别芯片的封装管脚竟然是处于盖油状态&#xff0c;这肯定是个bug。制板厂家EQ&#xff0c;在审核生成稿时&#xff0c;也回打来电话确认“焊盘是否需…

python数据分析05—Pandas数据处理

目录 1.缺失数据处理 1.1 DataFrame自身产生的缺失数据 1.2 缺失数据判断和统计 ​1.3 缺失数据清理 2. 多源数据操作 2.1 合并函数&#xff1a;merge() 2.2 连接函数&#xff1a;join() 2.3 指定方向合并&#xff1a;concat() 3. 数据分组和聚合运算 3.1 groupby()方…

使用Nacos将单体服务注册成微服务的步骤以及相关问题解决

目录 1.改造单体服务的配置文件。 2.添加Nacosw相关的pom依赖 3.在nacos的配置列表中创建配置列表 4.相关问题的解决 1.改造单体服务的配置文件。 &#x1f516;创建一个bootstrap.yml的配置文件该文件通常放置在src/main/resources目录中&#xff0c;并且优先于applicati…

C语言学习笔记 码云及git使用教程-05

目录 一、码云简介 二、码云注册 1.点击右上角的注册按钮 2.填写相应的注册信息 3.使用账号密码进行登陆 三、创建仓库 1.如图新建 2.定义仓库相应参数 3.初始化readme文件 4.效果 5.开源设置 四、git管理 1.安装git 2.打开桌面上的Git bash 3.进行仓库克隆 4. 在其他盘…

信息与通信工程面试准备——专业知识提问

1.无线通信&#xff1a;依靠电磁波在空间传播以传输信息。 2.通信的目的&#xff1a;传输信息。 3.通信系统&#xff1a;将信息从信源发送到一个或多个目的地。 4.本书中通信一般指电信&#xff1a;利用电信号传输信息&#xff08;光通信属于电信&#xff0c;因为光也是一种…

Java中的队列

队列的理解 队列&#xff08;Queue&#xff09;是一种特殊的线性表&#xff0c;它只允许在表的前端进行删除操作&#xff0c;而在表的后端进行插入操作。 LinkedList类实现了Queue接口&#xff0c;因此我们可以把LinkedList当成Queue来用。 常用方法 实例 import java.util…

zabbix监控-钉钉webhook告警并使用markdown格式

zabbix监控-企业微信webhook告警并使用markdown格式 最终告警样式&#xff1a; markdown格式与text格式的优点&#xff1a;文字排版清晰&#xff0c;可对不同文字标识颜色&#xff0c;大小等。 此方法使用zabbix提供的webhook方式&#xff0c;不需要建立脚本文件。 zabbix版…