多线程(JavaEE初阶系列3)

目录

前言:

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();//在主线程中修改isQuittry {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内部的标志位给设置成truet.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();}}});//在启动之前,获取线程状态NEWSystem.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.mzph.cn/news/7413.shtml

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

相关文章

解决JMeter+Grafana+influxdb 配置出现transaction无数据情形

问题描述 JMeterGrafanainfluxdb 配置时&#xff0c;Darren洋发现jmeter中明明已经配置好了事务条件以及接口实例信息&#xff0c;但就是在grafana的头部导航栏中的transaction按钮下来没有相应事务数据信息&#xff0c;经过相关资料查询&#xff0c;Darren洋发现执行以下两个步…

搭建基于Nginx+Keepalived的高可用web集群并实现监控告警

目录 搭建相关服务器DNS服务器配置WEB服务器配置配置静态IP编译安装nginx 负载均衡器配置lb1lb2高可用配置 NFS服务器配置配置静态IP安装软件包新建共享目录web服务器挂载 监控服务器配置安装node-exporter编写prometheus.yml安装alertmanager和钉钉插件获取机器人webhook编写a…

Python实战之数据挖掘详解

一、Python数据挖掘 1.1 数据挖掘是什么&#xff1f; 数据挖掘是从大量的、不完全的、有噪声的、模糊的、随机的实际应用数据中&#xff0c;通过算法&#xff0c;找出其中的规律、知识、信息的过程。Python作为一门广泛应用的编程语言&#xff0c;拥有丰富的数据挖掘库&#…

Nginx配置解析

server {listen 80;server_name example.com;location / {proxy_pass http://backend;}location / 是 Nginx 的一个匹配规则&#xff0c;用于匹配所有请求路径。proxy_pass 指令则用于将匹配到的请求转发给指定的后端服务器。下面是关于 location / 和 proxy_pass 的详细介绍&a…

解决嵌入式中QTableWidget双击出现空白QTableWidgetItem输入

目录 所说BUG现象解决方式1方式2全部内容 效果 今天突然想起在上个公司解决的一个BUG 嵌入式中QTableWidget一般只能看数据不能编辑&#xff0c;或者是选择 所以双击出现空白QTableWidgetItem是不允许的 所说BUG现象 解决 在空白的单元格中&#xff0c;添加不可编辑的QTableWid…

数字孪生:未来科技的新前沿

数字孪生作为一项新兴的研究方向&#xff0c;正逐渐成为科技界的焦点。它是将现实世界中的实体、系统或过程通过数字化手段进行建模、仿真和分析&#xff0c;形成与实体相对应的数字化副本。数字孪生的发展为我们带来了无限的想象空间&#xff0c;以及解决现实问题的新途径。 在…

Zabbix监控安装grafana并配置图形操作

第三阶段基础 时 间&#xff1a;2023年7月20日 参加人&#xff1a;全班人员 内 容&#xff1a; Zabbix监控安装grafana 目录 安装并配置grafana 一、安装Grafana 二、下载安装插件 三、配置grafana 四、Web访问并配置&#xff1a; 安装并配置grafana 一、安装Graf…

【团队协作开发】将Gitee项目导入到本地IDEA中出现根目录不完整的问题解决(已解决)

前言&#xff1a;在团队协作开发过程中&#xff0c;通常我们的Gitee完整项目中会包含很多内容&#xff1a;后端代码、前端代码、项目结构图、项目文档等一系列资产。 将Gitee项目导入到本地IDEA中&#xff0c;通常会出现根目录不完整的问题。这是因为项目里面包含了后端代码、前…

基于Java+SpringBoot+vue前后端分离甘肃非物质文化网站设计实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

DAY51:动态规划(十五)买卖股票最佳时机Ⅲ+买卖股票最佳时期Ⅳ

文章目录 123.买卖股票最佳时机Ⅲ&#xff08;注意初始化&#xff09;思路DP数组含义递推公式初始化遍历顺序最开始的写法&#xff1a;初始化全部写成0debug测试&#xff1a;解答错误&#xff0c;第0天实际上是对应prices[0]和dp[0] 完整版总结 188.买卖股票最佳时机Ⅳ思路DP数…

09.计算机网络——套接字编程

文章目录 网络字节序socket编程socket 常见APIsockaddr结构 UDP编程创建socket绑定socketsendto发送数据recvform接收数据关闭socket TCP编程创建socket绑定socketlisten监听套接字accept服务端接收连接套接字connect客户端连接套接字send发送数据recv接收数据关闭socket 工具n…

【flink】ColumnarRowData

列式存储 在调试flink读取parquet文件时&#xff0c;读出来的数据是ColumnarRowData&#xff0c;由于parquet是列式存储的文件格式&#xff0c;所以需要用一种列式存储的表示方式&#xff0c;ColumnarRowData就是用来表示列式存储的一行数据&#xff0c;它包含多个数组的数据结…

从电商指标洞察到运营归因,只需几句话?AI 数智助理准备好了!

Lily 是名入职不久的电商运营助理&#xff0c;最近她想要根据 2022 年的客单价情况&#xff0c;分析品牌 A 在不同电商渠道的用户行为和表现&#xff0c;并提供一些有价值的洞察和建议给客户。然而在向技术人员提报表需求后&#xff0c;技术人员以需求排满为借口拒绝了。 Lily …

5分钟,结合 LangChain 搭建自己的生成式智能问答系统

伴随大语言模型&#xff08;LLM&#xff0c;Large Language Model&#xff09;的涌现&#xff0c;人们发现生成式人工智能在非常多领域具有重要意义&#xff0c;如图像生成&#xff0c;书写文稿&#xff0c;信息搜索等。随着 LLM 场景的多样化&#xff0c;大家希望 LLM 能在垂直…

记一次容器环境下出现 Address not available

作者&#xff1a;郑明泉、余凯 困惑的源地址 pod 创建后一段时间一直是正常运行&#xff0c;突然有一天发现没有新的连接创建了&#xff0c;业务上是通过 pod A 访问 svc B 的 svc name 的方式&#xff0c;进入 pod 手动去 wget 一下&#xff0c;发现报错了 Address not avai…

jar 更新 jar包内的 class,以及如何修改class

一、提取Jar 内文件 #提取jar内的配置文件jar -xvf a.jar META-INF\plugin.xml-已解压: META-INF/plugin.xml#提取jar内的class文件&#xff0c; 提示&#xff1a;反编译为java文件&#xff0c;修改后再使用javac xxx.java编译为class&#xff0c;jar -xvf a.jar io.config.**…

单例模式类设计|什么是饿汉模式和懒汉模式

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量干货博客汇总https://blog.csdn.net/yu_cblog/c…

在Vue-Element中引入jQuery的方法

一、在终端窗口执行安装命令 npm install jquery --save执行完后&#xff0c;npm会自动在package.json中加上jquery 二、在main.js中引入&#xff08;或者在需要使用的页面中引入即可&#xff09; import $ from jquery三、使用jquery

【Ansible 自动化配置管理实践】01、Ansible 快速入门

目录 一、Ansible 快速入门 1.1 什么是 Ansible ​1.2 Ansible 主要功能 1.3 Ansible 的特点 1.4 Ansible 基础架构 二、Ansible 安装与配置 2.1 Ansible 安装 2.2 确认安装 三、Ansible 配置解读 3.1 Ansible 配置路径 3.2 Ansible 主配置文件 3.3 Ansi…

Spring系列一:spring的安装与使用

文章目录 &#x1f49e; 官方资料&#x1f34a;Spring5下载&#x1f34a;文档介绍 &#x1f49e;Spring5&#x1f34a;内容介绍&#x1f34a;重要概念 &#x1f49e;快速入门&#x1f34a;Spring操作演示&#x1f34a;类加载路径&#x1f34a;Debug配置&#x1f34a;Spring容器…