当前位置: 首页 > news >正文

多线程基础篇(包教包会)

文章目录

    • 一、第一个多线程程序
      • 1.Jconsole观察线程
      • 2.线程休眠-sleep
    • 二、创建线程
    • 三、Thread类及常见方法
      • 1. Thread 的常见构造方法
      • 2. Thread 的几个常见属性
      • 3. 启动线程 - start
      • 4. 中断线程
      • 5. 等待一个线程
    • 四、线程状态
    • 五、线程安全问题(synchronized)(重点)
      • 1. 观察线程不安全问题
      • 2.线程安全问题分析
      • 3.线程安全问题的原因
      • 4.解决线程不安全问题
      • 5.synchronized 关键字
      • 6.总结
    • 六、内存可见性问题(volatile)
      • 1.观察内存不可见问题
      • 2.问题分析
      • 3.volatile关键字
      • 4.总结
    • 七、wait 和 notify
      • 1.wait 和 sleep 之间的区别

前言:平时我们敲的代码,当点击运行程序的时候,就会先创建出一个java进程。这个进程中就包含了至少一个线程。这个线程也叫做主线程。也就是负责执行main方法的线程.

一、第一个多线程程序

Java中为了实现多线程,提供了thread类。

创建一个类来继承thread,重写thread中的run方法,这里的run方法就相当于线程的入口,当程序运行后,此线程要做什么事情,都是通过run方法来实现的。

创建完后,我们要在main中来调用这个myThread线程,这里通过start方法来启动线程。(start会调用系统api,在系统内核中把线程对应的pcb给创建出来并管理好,由此新的线程就会参与调度了)

为什么不用 myThread.run() ? run只是上面的入口方法(普通的方法)。并没有调用系统 api,也没有创建出真正的线程来.不会执行并发操作,只是按顺序执行代码。

class myThread extends Thread{@Overridepublic void run() {while (true) {System.out.println("hello Thread");}}
}
public class Main {public static void main(String[] args) {Thread myThread = new myThread();myThread.start();//myThread.run();while (true) {System.out.println("Hello world!");}}
}

主线程和新线程是并发执行的关系. 操作系统怎么调度?

每个线程,都是一个独立的执行流.每个线程都可以执行一段代码.多个线程之间是并发的关系~~

image-20230818120859577

1.Jconsole观察线程

当创建出线程之后,也是可以通过一些方式,直观的观察到的~~

  1. idea 的调试器

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r6psratB-1692793532590)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818121703133.png)]

  2. jconsole

    此为官方在 jdk 中给程序猿提供的一个调试工具。

    我们可以按照之前 jdk 下载的路径在 bin 目录下找到 jconsole.exe

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W07h3eZm-1692793532590)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818121840694.png)]

先运行java程序然后点击 jconsole.exe ,就会发现我们用 java 写的多线程正在运行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cS0ocn03-1692793532591)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818122754374.png)]

这里就列出了当前进程中所有的线程不仅仅是主线程和自己创建的新线程. 剩下的线程,都是JVM里自带的,负责完成一些其他方面的任务。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5mqZLaq0-1692793532591)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818123329413.png)]


2.线程休眠-sleep

这里先介绍Thread类中的一个sleep方法,顾名思义,就是让线程暂时睡一会、暂时停滞 不进行工作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DUw7Zu3t-1692793532592)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230819173807151.png)]

在sleep()中我们可以设置休眠多长时间,其单位是ms。

但是 sleep 本身也是存在一些误差的.

设置 sleep(1000) ,不一定是精确的就休眠 1000ms,会存在误差!!原因是线程的调度,也是需要时间的。

冷知识:sleep(0) 是让当前线程放弃 CPU 重新去队列中排队,准备下一轮的调度。由于这个操作看起来比较抽象,因此java有封装了一个方法yield,和sleep(0)功能一样。

二、创建线程

创建线程的方式还有很多,包括:

1.创建一个类,继承自 Thread.重写run方法. (已介绍)

2.创建一个类,实现Runnable.重写run方法.

3.继承 Thread ,重写run,基于匿名内部类.

4.实现 Runnable ,重写run,基于匿名内部类.

5.使用 lambda表达式,表示run方法的内容.(推荐常用)

6.基于Callable

7.基于线程池


上述第一种方法已介绍,接着介绍第二种方法。

创建一个类,实现 Runnable.重写 run 方法.

Runnable这里,则是分开了,把要完成的工作放到Runnable 中,再让Runnable和Thread 配合.

这里是把要完成的工作放到 Runnable 中,再让 Runnable 和 Thread 配合.

class myRunnable implements Runnable {@Overridepublic void run() {while (true) {System.out.println("hello world");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
public class Demo2 {public static void main(String[] args) throws InterruptedException {myRunnable myRunnable = new myRunnable();Thread i = new Thread(myRunnable);i.start();while (true) {System.out.println("hello main");Thread.sleep(1000);}}
}

继承 Thread ,重写run,基于匿名内部类

1.创建了一个子类,这个子类继承自Thread. 但是,这个子类,是没有名字的!!(匿名)另一方面,这个类的创建,是在Demo3这个类里面.

2.在子类中,重写了run方法.

3创建了该子类的实例.并且使用t这个引用来指向.

public class Demo3 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread() {@Overridepublic void run() {while (true) {System.out.println("hello world");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};t.start();while (true) {System.out.println("hello main");Thread.sleep(1000);}}
}

实现 Runnable ,重写run,基于匿名内部类.

1.创建了一个Runnable的子类(类,实现 Runnable)

2.重写了run方法

3.把子类,创建出实例,把这个实例传给Thread的构造方法.

public class Demo4 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(new Runnable() {@Overridepublic void run() {while (true) {System.out.println("hello world");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t.start();while (true) {System.out.println("hello main");Thread.sleep(1000);}}
}

使用 lambda表达式,表示run方法的内容.(推荐常用)

lambda表达式,本质上就是一个"匿名函数”。这样的匿名函数,主要就可以用来作为回调函数来使用.

经常会用到回调函数的场景:

  • 服务器开发:服务器收到一个请求,触发一个对应的回调函数.
  • 图形界面开发:用户的某个操作,触发一个对应的回调.

类似于lambda这样的写法,本质上并没有新增新的语言特性,而是把以往能实现的功能,换了一种更简洁的方式来编写.(新瓶装旧酒->语法糖)

public class Demo5 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (true) {System.out.println("hello world");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();while (true) {System.out.println("hello main");Thread.sleep(1000);}}
}

三、Thread类及常见方法

1. Thread 的常见构造方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CLVf210M-1692793532592)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818153818246.png)]

我们可以给创建的线程进行命名

public class Demo5 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (true) {System.out.println("hello a");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"THREAD");t.start();
/*        while (true) {System.out.println("hello b");Thread.sleep(1000);}*/}
}

把主线程中的循环注释掉,当程序运行时,查看 jconsole,发现只剩HTREAD线程,main线程没有了。因为main已经执行完了!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YXExGiCl-1692793532593)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818155951217.png)]


2. Thread 的几个常见属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MLLoDLGn-1692793532593)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818160111857.png)]

  • isDaemon是否后台线程

    JVM会在一个进程的所有非后台线程结束后,才会结束运行。

    创建的线程,默认是前台线程。可以通过setDaemon(true)显式的设置成后台。

  • islive是否存活

    Thread对象的生命周期,并不是和系统中的线程完全一致的!!

    一般,都是Thread对象,先创建好,然后手动调用start,内核才真正创建出线程。

    消亡的时候,可能是thread对象,先结束了生命周期(没有引用指向这个对象)。也可能是 thread对象还在,内核中的线程把run执行完了,就结束了。


3. 启动线程 - start

start 方法是系统中,真正创建出线程。此方法是调用系统中的 api 完成线程的创建

如何创建的呢? 在内核中完成创建pcb,并把pcb加入到对应的链表中。

start方法本身的执行是一瞬间就完成的.只是告诉系统,你要创建个小线程出来。调用start完毕之后,代码就会立即继续执行 start 后续的逻辑。


4. 中断线程

在线程执行 run 方法的时候,不完成是不会结束的。但有时候,因为特殊原因,需要终止一个正在执行的程序,该如何操作嘞??

常见的方式有以下两种:

  1. 程序猿手动设定标志位
  2. 调用 interrupt() 方法来通知

设定标志位

public class Demo8 {public static boolean isQuit = false;public static void main(String[] args) throws InterruptedException {//boolean isQuit = false;Thread t = new Thread(()->{while (!isQuit) {System.out.println("hello world");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();//主线程执行一些其他逻辑后,要让 t 线程结束.Thread.sleep(3000);//这个代码就是在修改前面设置的标志位.isQuit = true;System.out.println("把 t 线程中断");}
}

以上代码就是通过设定标志位来终止线程的。

思考 如果我们现在把 isQuit 定义在 main 内,代码就会开始报错!!这是为什么呢?

是因为 lambda 所触发的“变量捕获”机制。变量捕获这里有个限制,要求捕获的变量得是final (至少是看起来是final),我们都知道被final修饰后面是不可以修改的。

如果这个变量想要进行修改,就不能进行变量捕获了~~因此上述代码就会进行报错。

什么是变量捕获:lambda内部看起来是在直接访问外部的变量,其实本质上是把外部的变量给复制了一份,到 lambda里面.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QeLCyBvq-1692793532593)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818171855839.png)]

为啥java这么设定??

java是通过复制的方式来实现"变量捕获",如果外面的代码要对这个变量进行修改,就会出现一个情况:外面的变量变了,里面的没变~~代码更容易出现歧义.


使用 interrupt()方法

使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eFmNg6N8-1692793532594)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818173551575.png)]

public class Demo9 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{//Thread.currentThread()其实就是 t//这里不能用t是因为,lambda表达式还没构造完t,因此看到不到。while (!Thread.currentThread().isInterrupted()) {System.out.println("hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();Thread.sleep(3000);//把上述的标志位设置为truet.interrupt();}
}

执行程序后,并没有让我们的程序结束,而是出现了一个异常。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZidjOXRf-1692793532594)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818175816376.png)]

我们可以理解成sleep被唤醒

一个程序可能处于正常运行状态,也可能处于Sleep状态,也可以称为阻塞状态,意思就是代码暂时不执行了。

重点:线程在阻塞过程中,如果其他线程调用interrupt方法,就会立即唤醒一个正在被阻塞的程序。但是sleep在被唤醒的同时,也会自动清除前面设置的标志位!! 唤醒后会给程序猿留下更多的操作空间.

此时,如果想添加其他的操作就可以在 catch 中编写新代码。如果想直接终止掉程序,只需要在 catch 中屏蔽掉异常,另加一个 break 即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-61pIwJOT-1692793532594)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230819171729689.png)]

这几种处理方式,都是比较温和的方式。另一个线程提出请求,本线程自己决定,是否要终止。更激进的做法是,这边提出请求,那边立即就结束,线程根本来不及反应。完全不考虑本线程的实际情况,就可能会造成一些负面的影响~

5. 等待一个线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zHwDcSRL-1692793532595)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230818184421648.png)]

多个线程是并发执行的.具体的执行过程,都是由操作系统负责调度的!!!操作系统调度线程的过程,是"随机"的。无法确定线程执行的先后顺序。因此等待线程,就是一种规划 线程结束顺序 的手段。

回过头来再解释一下阻塞状态,顾名思义就是代码暂时不继续执行了(该线程暂时不去CPU上参与调度)

join 的阻塞,则是“死等” -> "不见不散"的那种。例如:t.join()表示t程序如果没执行完,则阻塞t.join所在的程序。

在计算机中,更推荐有时间限制的版本 join(long milis),留有余地。只要时间到了,不管来没来,都不等了。

join能否被interrupt唤醒?? 答案是可以的!!

sleep, join, wait…产生阻塞之后,都是可能被interrupt方法唤醒的,这几个方法都会在被唤醒之后自动清除标志位(和sleep类似的)

public class Demo10 {public static void main(String[] args) {//线程bThread b = new Thread(()->{for (int i = 0; i < 5; i++) {System.out.println("hello b");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("b 结束了");});//线程aThread a = new Thread(() ->{for (int i = 0; i < 3; i++) {System.out.println("hello a");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}try {//这里运用 b.join 来堵塞a程序//如果 b 此时还没执行完毕,b.join 就会产生阻塞的情况。//这里的join也会产生受查异常,需要try-catchb.join(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("a 结束了");});b.start();a.start();}
}

四、线程状态

之前谈到过线程的两个状态,一个是阻塞状态,另一个是就绪状态。这两个状态都是系统所设定的两个状态。在java中,把上述状态又进一步的细分出了6个状态。

  1. NEW: 安排了工作, 还未开始行动
  2. RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
    • 正在工作中:线程正在 CPU 上运行
    • 即将开始工作:线程正在排队,随时可以去 CPU 上执行
  3. BLOCKED: 这几个都表示排队等着其他事情(因锁产生的阻塞,后文后讲到)
  4. WAITING: 这几个都表示排队等着其他事情(因调用wait产生阻塞,后文会讲到)
  5. TIMED_WAITING: 这几个都表示排队等着其他事情(用 sleep(millis) 和 join(millis) 带时间参数的版本都会触发)
  6. TERMINATED: 工作完成了.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HFO4MbyF-1692793532595)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821114442954.png)]

public class Demo1 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}//System.out.println("执行完毕!!");});//安排了线程,但还未工作System.out.println("状态1;" + t.getState());t.start();//开始工作,正在执行中System.out.println("状态2:" + t.getState());Thread.sleep(1000);//排队等待中 System.out.println("状态3:" + t.getState());t.join();//线程结束,工作完成了System.out.println("状态4:" + t.getState());}
}
/*输出状态1;NEW状态2:RUNNABLE状态3:TIMED_WAITING状态4:TERMINATED
*/

五、线程安全问题(synchronized)(重点)

1. 观察线程不安全问题

观察下列代码

static class Counter {public int count = 0;public void increase() {count++;}
}
public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();  //等待线程t1结束t2.join();	//等待线程t2结束System.out.println(counter.count);
}
/*输出64821       //输出任意数小于10W
*/

我们发现,如果按照正常逻辑来,两个线程针对同一个变量,进行循环自增,各自增 5w 次,预期最终结果应该是 10w,但实际上并不是!! 说明我们的代码有 bug!!

这里的 bug 是一个非常广义的概念,只要是实际运行效果和预期效果(需求效果)不一致,就可以称之是一个 bug.

在多线程下,发现由于多线程执行,导致的 bug,统称为“线程安全问题”如果某个代码,在单线程下执行没有问题,多个线程下执行也没问题,则称为“线程安全”,反之也可以称为“线程不安全”。


2.线程安全问题分析

那为啥会出现上述的 bug 呢??

问题出现在这里,count++ 看上去是一行代码,实际上在CPU角度上来说是执行了三步操作。

  1. 把内存中的数据,加载到CPU的寄存器中(load)
  2. 把寄存器中的数据进行+1 (add)
  3. 把寄存器中的数据写回到内存中(save)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wYpeVtV3-1692793532596)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821122538075.png)]

如果上述的操作,在两个线程,或者更多个线程并发执行的情况下,就可能会出现问题!!

接下来我们可以通过时间轴,具体看一下问题出现在哪。

预期情况下,t1、t2 线程串行执行,t1完事后 t2 才开始。执行结果为正确。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zdMyEnSg-1692793532596)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821173212806.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t3jxtfmX-1692793532596)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821125422184.png)]

若通常情况下,CPU针对这些线程的调度,是按照抢占式的方式进行调度的,因此这些命令的执行顺序可能会存在很多中方式。 因此这两组执行操作的相对顺序会存在很大差异!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bYlyHuHY-1692793532597)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821130049255.png)]

取其中的一个执行方法为例,虽然是自增两次,但是由于两个线程并发执行,就可能在一定的执行顺序下,导致运算的中间结果就被覆盖了。

在这5w次的循环过程中,有多少次,这俩线程执行++是"串行的”,有多少次会出现覆盖结果的??不确定!!线程的调度是随机的,抢占式执行的过程。

此处这里的结果就会出现问题,而且得到的这个错误值,一定是小于10w。

因此很多代码都会涉及到线程安全问题,不仅仅只是 count++.

3.线程安全问题的原因

1.[根本原因]多个线程之间的调度顺序是“随机的”,操作系统使用"抢占式"执行的策略来调度线程。

和单线程不同的是,多线程下,代码的执行顺序,产生了更多的变化。

以往只需要考虑代码在一个固定的顺序下执行,执行正确即可。现在则要考虑多线程下,N种执行顺序下,代码执行结果都得正确。

2.多个线程同时修改同一个变量.容易产生线程安全问题. 代码的结构

3.进行的修改,不是“原子的”。 此为切入线程安全问题,最主要的手段。

如果修改操作,能够按照原子的方式来完成,此时也不会有线程安全问题。

例如上述例子中,count++ 操作不是原子的。需要考虑到CPU 中的三步操作。

4.内存可见性,引起的线程安全问题。(后文讲解)

5.指令重排序,引起的线程安全问题。(后文讲解)

4.解决线程不安全问题

为了解决线程不安全问题,我们引入加锁这不操作。

其原理就相当于,把一组操作打包成一个“原子的操作”。但是与事务的那个原子不同。事务原子性,主要体现在“回滚”的操作。而这里的原子,则是通过锁,进行“互斥”,相当于我这个线程工作的时候,其他线程无法工作

通过这个锁,就限制了,同一时刻,只有一个线程能使用当前资源。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xycEZ7Zz-1692793532597)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821195025880.png)]

此时当t1线程进行访问时,就会对increase方法加锁。若果在t1加完锁后,t2又来试图访问加锁,t2就会阻塞等待!!这个阻塞一直会持续到t1把锁解放后,t2才能够加锁成功。

按照上述加锁方法,就相当于把 increase 方法中的 count++ 操作“打包成一个原子”。

因此就实现了把“穿插执行”变成了串行执行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TfjxFO75-1692793532597)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821200320853.png)]

这里提出个问题:通过加锁使并发执行变为串行化执行,此时多线程还有存在的意义吗??

必然是有的,我们要知道串行化执行针对的是 count++ 操作,也就是线程中的 counter.increase() 方法,但是线程中不仅仅包含了这一句代码,还有 for 循环,因此线程之间还是存在并发执行的操作,也就是说多线程还是有意义的。

5.synchronized 关键字

java 给我们提供的加锁的方式(关键字)是搭配 代码块 来完成的~(进入代码块就加锁,出了代码块就解锁)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QUrhfAtG-1692793532598)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822155614255.png)]

synchronized 进行加锁 解锁,其实是以"对象"为维度进行展开的!!!

以下是 synchronized 锁的两种用法,的一种是第二种的简化,直接修饰方法,就相当于对 this 加锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jbJ3vAyE-1692793532598)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821215343664.png)]

这里非常关键:**只要两个线程针对同一个对象进行加锁,就会出现 锁竞争/锁冲突,一个线程加锁成功,另一个线程阻塞等待。 **这里的锁对象,是任意对象都可以。锁对象和要访问的对象没有必然关联

反之两个线程针对不同对象进行加锁,就不会出现锁竞争。会出现“穿插执行”的线程不安全问题

线程安全案例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gAdQyexN-1692793532598)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821224302685.png)]

线程不安全案例

这里面锁对象是不同的,此时,就不会出现有阻塞等待,也不会有两个线程按照串行的方式执行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wknIsRbL-1692793532599)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821224620693.png)]

6.总结

利用synchronized 锁的时候,代码执行流程如下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awgiWV8t-1692793532599)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230821214537504.png)]

六、内存可见性问题(volatile)

1.观察内存不可见问题

观察下面代码

public class Demo1 {public static int isQuite = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (isQuite == 0) {;}System.out.println("程序t1执行结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);isQuite = scanner.nextInt();});t1.start();t2.start();}
}

我们所期望的代码逻辑:t1始终在进行while循环,t2则是要让用户通过控制台输入一个整数,作为isQuit的值。当用户输入的仍然是0的时候,,t1线程继续执行。如果用户输入的非0,则t1线程就应该循环结束。

而实际上:即使t2线程修改了isQuite值,代码也不会结束,而是陷入无限循环状态。

问题出现了,当输入非0值的时候,已经修改了isQuit的值了。但是t1线程仍然在继续执行。这就是不符合预期的,也是bug

2.问题分析

为什么会出现上述 bug 呢??? 其根本原因就是 java 编译器的优化机制。

当我们写出来的代码程序运行时,java编译器和 jvm可能会对代码做出一些“优化”。

编译器优化,本质上是靠代码,智能的对你写的代码进行分析判断,进行调整。这个调整过程大部分情况下都是ok,都能保证逻辑不变但是,如果遇到多线程了,此时的优化可能就会出现差错!!! 会使用程序中原有的逻辑发生改变

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GRZQuchT-1692793532600)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822153418460.png)]

对于上述代码中的 isQuite == 0 本质上其实是两步指令

  1. 第一步加载(load),读取到内存中的数据。 ->读内存操作,速度非常慢
  2. 第二步放在寄存器中操作(与0进行比较是否相等) ->寄存器操作,速度极快

此时,编译器/JVM就发现,这个逻辑中,代码要反复的,快速的读取同一个内存的值。并且,这个内存的值,每次读出来还是一样的~~
因此,编译器就做出一个大胆的决策,直接把 load 操作优化掉了,只是第一次执行load 。后续都不再执行load,直接拿寄存器中的数据进行比较了。

但是,万万没想到,程序猿有点不讲武德,搞偷袭,在另一个线程 t2 中,把内存中的 isQuite 给改了!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9zpVyQx2-1692793532600)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822154339711.png)]

另一个线程中,并没有重复读取isQuit的值,而是只读寄存器中的值。因此 t1线程就无法感知到 t2 的修改。因此也就出现了上述内存不可见问题。

3.volatile关键字

编译器优化在上述代码中好心办坏事,算是编译器的 bug 吧。为了弥补这样的 bug ,volatile就由此诞生喽。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QJr1iGgv-1692793532600)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822155210428.png)]

把volatile用来修饰一个变量之后,编译器就明白,这个变量是"易变"的,就不能按照上述方式,把读操作优化到读寄存器中.(编译器就会禁止上述优化)于是就能保证t1在循环过程中,始终都能读取内存中的数据!!

volatile本质上是保证变量的内存可见性.(禁止该变量的读操作被优化到读寄存器中).

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22GGVB5q-1692793532601)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822155349148.png)]

4.总结

编译器优化后的 java 线程只能从寄存器中读数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7sYQr2TZ-1692793532601)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822160220898.png)]

加上 volatile 后,就可以保证内存的可见性(非原子性)。从而线程就可以正常从内存中读数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JovNcO56-1692793532602)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822160126084.png)]

编译器优化,其实是一个"玄学问题"。啥时候进行优化,啥时候不优化,咱们作为外行,有些摸不到规律~~

代码稍微改动一下,可能就不会触发上述优化~~ 比如说在while内加上个sleep就不会触发优化机制。(这里不给演示了)

七、wait 和 notify

wait 和 notify 也是多线程编程中的重要工具。多线程调度是随机的,有时候希望多个线程能够按照咱们规定的顺序来执行,完成线程间的配合工作。由此,wait 和 notify就闪亮登场了。wait 和 notify 通常都是搭配成对使用。

wait:等待. notify:通知. 我们可以按照字面意思来理解。

wait 和 notify ,都是由Object所提供的方法。因此随便找个对象,都可以使用 wait 和 notify.

在尝试使用 wait 的时候编译器出现提示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1giwhauV-1692793532602)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822204100728.png)]

大概意思就是,在 wait 运行阻塞时,可能被 interrupted 给唤醒,需要捕获异常。

当我们添加完 try-catch 运行后,编译器报错:非法监视器状态(这里的监视器是指 synchronized 可以称为监视器锁)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2eDMtFRC-1692793532603)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822204355344.png)]

这里为啥会报错呢??

wait 在执行的时候,会做三件事:

  • 解锁。 object.wait 会尝试针对object 对象解锁。
  • 阻塞等待。
  • 当被其他线程唤醒之后,就会尝试重新加锁。加锁成功, wait 执行完毕,继续往下执行其他逻辑。

在锁中无非就两种状态,要么加锁,要么解锁。

这里 wait 操作要解锁的前提,那就是把 wait 加锁。

核心思路:先加锁,然后在synchronized里头再进行wait!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-33ZG4pqi-1692793532603)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822211120870.png)]

在运行过程中,我们通过 t1.getState() 观察线程状态发现,此线程正在 WAITING,阻塞等待中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hg82LhOX-1692793532603)(C:\Users\28779\AppData\Roaming\Typora\typora-user-images\image-20230822211312672.png)]

这里的 wait 就是一直阻塞到其他线程进行 notify 了。

notify 使用方法和 wait 差不多。直接上代码。

public class Demo2 {//public static Object locker;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {synchronized (locker) {System.out.println("t1 wait 开始");try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1 wait 结束");}});t1.start();Thread.sleep(1000);System.out.println(t1.getState());Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker) {System.out.println("t2 notify 开始");locker.notify();System.out.println("t2 notify 结束");}});t2.start();}
}

几个注意事项:

  1. 要想让 notify 能够顺利唤醒 wait,就需要确保 wait 和 notify 都是使用同一个对象调用的.

  2. wait 和 notify 都需要放到synchronized之内的。虽然 notify 不涉及"解锁操作"但是 java 也强制要求 notify 要放到 synchronized 中(系统的原生api中,没有这个要求)

  3. 如果进行 notify 的时候,另一个线程并没有处于wait状态。此时, notify 相当于"空打一炮",不会有任何副作用

🌳 小技巧:

如果就想唤醒某个指定的线程。就可以让不同的线程,使用不同的对象来进行wait 。想唤醒谁,就可以使用对应的对象来 notify。

1.wait 和 sleep 之间的区别

sleep是有一个明确的时间的。到达时间,自然就会被唤醒。也能提前唤醒,使用interrupt就可以。

wait 默认是一个死等,一直等到有其他线程notify。wait 也能够被 interrupt 提前唤醒。

notify 的唤醒是顺理成章的唤醒。唤醒之后该线程还需要继续工作后续还会进入到 wait 状态。

interrupt 的唤醒就相当于,告知线程要结束了。接下来线程就要进入到收尾工作了。

因此,协调多个线程之间的执行顺序,当然还是优先考虑使用 wait notify 而不是 sleep 。

相关文章:

多线程基础篇(包教包会)

文章目录 一、第一个多线程程序1.Jconsole观察线程2.线程休眠-sleep 二、创建线程三、Thread类及常见方法1. Thread 的常见构造方法2. Thread 的几个常见属性3. 启动线程 - start4. 中断线程5. 等待一个线程 四、线程状态五、线程安全问题(synchronized)&#xff08;重点&#…...

Android/Java中,各种数据类型之间的互相转换,给出各种实例,附上中文注释

目录 1.字符串&#xff08;String&#xff09;转整数&#xff08;int&#xff09;&#xff1a; 2.整数&#xff08;int&#xff09;转字符串&#xff08;String&#xff09;&#xff1a; 3.字符串&#xff08;String&#xff09;转浮点数&#xff08;float&#xff09;&…...

机器学习知识点总结:什么是EM(最大期望值算法)

什么是EM(最大期望值算法) 在现实生活中&#xff0c;苹果百分百是苹果&#xff0c;梨百分白是梨。 生活中还有很多事物是概率分布&#xff0c;比如有多少人结了婚&#xff0c;又有多少人有工作&#xff0c; 如果我们想要调查人群中吸大麻者的比例呢&#xff1f;敏感问题很难得…...

漏洞挖掘和安全审计的技巧与策略

文章目录 漏洞挖掘&#xff1a;发现隐藏的弱点1. 源代码审计&#xff1a;2. 黑盒测试&#xff1a;3. 静态分析工具&#xff1a; 安全审计&#xff1a;系统的全面评估1. 渗透测试&#xff1a;2. 代码审计&#xff1a;3. 安全策略审查&#xff1a; 代码示例&#xff1a;SQL注入漏…...

[SpringBoot3]Web服务

五、Web服务 基于浏览器的B/S结构应用十分流行。SpringBoot非常适合Web应用开发&#xff0c;可以使用嵌入式Tomcat、Jetty、Undertow或Netty创建一个自包含的HTTP服务器。一个SpringBoot的Web应用能够自己独立运行&#xff0c;不依赖需要安装的Tomcat、Jetty等。SpringBoot可以…...

构建系统自动化-autoreconf

autoreconf简介 autoreconf是一个GNU Autotools工具集中的一个命令&#xff0c;用于自动重新生成构建系统的配置脚本和相关文件。 Autotools是一组用于自动化构建系统的工具&#xff0c;包括Autoconf、Automake和Libtool。它们通常用于跨平台的软件项目&#xff0c;以便在不同…...

Mysql之InnoDB和MyISAM的区别

InnoDB和MyISAM是MySQL数据库中两种常见的存储引擎&#xff0c;它们在功能和性能方面有一些明显的区别。下面是它们之间的详细解释和说明&#xff1a; 底层数据 存数据的时候&#xff0c;MyISAM是数据和索引分开存储&#xff0c;分为MYD和MYI 而InnoDB是数据即索引&#xff0…...

Unity 之 Transform.Translate 实现局部坐标系中进行平移操作的方法

文章目录 Translate 默认使用局部坐标也可以转换成世界坐标 Translate 默认使用局部坐标 在Unity中&#xff0c;Transform.Translate是用于在游戏对象的局部坐标系中进行平移操作的方法。这意味着它将游戏对象沿着其自身的轴进行移动&#xff0c;而不是世界坐标轴。这在实现物…...

PostgreSQL Error: sorry, too many clients already

Error PG的默认最大连接数是100. 如果超过100就会报错sorry, too many clients already Find show max_connections; SELECT COUNT(*) from pg_stat_activity; SELECT * FROM pg_stat_activity;Solution 提高最大连接数 ALTER SYSTEM SET max_connections 然后重启pg查看…...

Vue2(路由)

目录 一&#xff0c;路由原理&#xff08;hash&#xff09;二&#xff0c;路由安装和使用&#xff08;vue2&#xff09;三&#xff0c;路由跳转四&#xff0c;路由的传参和取值五&#xff0c;嵌套路由六&#xff0c;路由守卫最后 一&#xff0c;路由原理&#xff08;hash&#…...

中介者模式-协调多个对象之间的交互

在深圳租房市场&#xff0c;有着许多的“二房东”&#xff0c;房主委托他们将房子租出去&#xff0c;而租客想要租房的话&#xff0c;也是和“二房东”沟通&#xff0c;租房期间有任何问题&#xff0c;找二房东解决。对于房主来说&#xff0c;委托给“二房东”可太省事了&#…...

Python框架【自定义过滤器、自定义数据替换过滤器 、自定义时间过滤器、选择结构、选择练习、循环结构、循环练习、导入宏方式 】(三)

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小王&#xff0c;CSDN博客博主,Python小白 &#x1f4d5;系列专栏&#xff1a;python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发 &#x1f4e7;如果文章知识点有错误…...

红黑树遍历与Redis存储

引言 在计算机科学领域&#xff0c;红黑树&#xff08;Red-Black Tree&#xff09;是一种自平衡的二叉查找树&#xff0c;它能在O(log n)的时间复杂度内完成插入、删除和查找操作。由于其高效性和可预测性的性能&#xff0c;红黑树在许多领域都得到广泛应用。本文将重点介绍红…...

前端处理图片文件的方法

在项目开发过程中&#xff0c;有一个需求&#xff0c;需要前端对上传的图片进行处理&#xff0c;以字符串的形式传给后端&#xff0c;实现效果如下&#xff1a; 1.上传图片的组件 在该项目中&#xff0c;使用了element plus组件库 <el-uploadv-model:file-list"fileL…...

「Java」《深入解析Java多线程编程利器:CompletableFuture》

《深入解析Java多线程编程利器&#xff1a;CompletableFuture》 一、 引言1. 对多线程编程的需求和挑战的介绍2. 介绍CompletableFuture的作用和优势 二. CompletableFuture简介1. CompletableFuture是Java中提供的一个强大的多线程编程工具2. 与传统的Thread和Runnable相比的优…...

Docker容器与虚拟化技术:容器运行时说明与比较

目录 一、理论 1.容器运行时 2.容器运行时接口 3.容器运行时层级 4.容器运行时比较 5.强隔离容器 二、问题 1.K8S为何难以实现真正的多租户 三、总结 一、理论 1.容器运行时 &#xff08;1&#xff09;概念 Container Runtime 是运行于 k8s 集群每个节点中&#xff…...

vue导出文件流获取附件名称并下载(在response.headers里解析filename导出)

导出文件流下载&#xff0c;拦截器统一处理配置 需求以往实现的方法&#xff08;各自的业务层写方法&#xff09;现在实现的方法&#xff08;axios里拦截器统一配置处理&#xff09;把文章链接复制粘贴给后端&#xff0c;让大佬自己赏阅。 需求 之前实现的导出都是各自的业务层…...

​山东省图书馆典藏《乡村振兴战略下传统村落文化旅游设计》鲁图中大许少辉博士八一新书

​山东省图书馆《乡村振兴战略下传统村落文化旅游设计》鲁图中大许少辉博士八一新书...

2023-08-19力扣每日一题-水题/位运算解法

链接&#xff1a; 2235. 两整数相加 题意&#xff1a; ab 解&#xff1a; ab 补一个位运算写法&#xff0c;进位是(a&b)<<1&#xff0c;不进位的计算结果为a^b 实际代码&#xff1a; #include<iostream> using namespace std; int sum(int num1, int n…...

Hadoop学习:深入解析MapReduce的大数据魔力之数据压缩(四)

Hadoop学习&#xff1a;深入解析MapReduce的大数据魔力之数据压缩&#xff08;四&#xff09; 4.1 概述1&#xff09;压缩的好处和坏处2&#xff09;压缩原则 4.2 MR 支持的压缩编码4.3 压缩方式选择4.3.1 Gzip 压缩4.3.2 Bzip2 压缩4.3.3 Lzo 压缩4.3.4 Snappy 压缩4.3.5 压缩…...

LRU淘汰策略执行过程

1 介绍 Redis无论是惰性删除还是定期删除&#xff0c;都可能存在删除不尽的情况&#xff0c;无法删除完全&#xff0c;比如每次删除完过期的 key 还是超过 25%&#xff0c;且这些 key 再也不会被客户端访问。 这样的话&#xff0c;定期删除和堕性删除可能都彻底的清理掉。如果…...

Kotlin 高阶函数详解

高阶函数 在 Kotlin 中&#xff0c;函数是一等公民&#xff0c;高阶函数是 Kotlin 的一大难点&#xff0c;如果高阶函数不懂的话&#xff0c;那么要学习 Kotlin 中的协程、阅读 Kotlin 的源码是非常难的&#xff0c;因为源码中有太多高阶函数了。 高阶函数的定义 高阶函数的…...

DL——week2

要学明白的知识点&#xff1a; np.dot()的作用 两个数组的点积&#xff0c;即对应元素相乘 numpy.dot(a,b,outNone) a: ndarray 数组 b: ndarray 数组 out: ndarray, 可选&#xff0c;用来保存dot&#xff08;&#xff09;的计算结果 numpy Ndarray对象 N维数组对象ndarray&am…...

如何撰写骨灰级博士论文?这是史上最全博士论文指导!

博士论文的写作是博士研究生主要要完成的工作。由于存在着较高的难度&#xff0c;较长的写作周期&#xff0c;以及在创新&#xff0c;写作规范&#xff0c;实际及理论意义等方面有着比较高的要求&#xff0c;博士论文的完成一般说来是有相当难度的。一篇好的博士论文不仅是一本…...

08.SpringBoot请求相应

文章目录 1 请求1.1 Postman1.2 简单参数1.2.1 原始方式1.2.2 SpringBoot方式1.2.3 参数名不一致 1.3 实体参数1.3.1 简单实体对象1.3.2 复杂实体对象 1.4 数组集合参数1.4.1 数组1.4.2 集合 1.5 日期参数1.6 JSON参数1.7 路径参数 2 响应2.1 ResponseBody注解2.2 统一响应结果…...

C#详解-Contains、StartsWith、EndsWith、Indexof、lastdexof

目录 简介: 过程: 举例1.1 举例1.2 ​ 总结: 简介: 在C#中Contains、StarsWith和EndWith、IndexOf都是字符串函数。 1.Contains函数用于判断一个字符串是否包含指定的子字符串&#xff0c;返回一个布尔值&#xff08;True或False&#xff09;。 2.StartsWith函数用于判断一…...

FATE框架中pipline基础教程

目录 1. 用pipline上传数据2. 用 Pipeline 进行 Hetero SecureBoost 的训练和预测3. 用 Pipeline 构建神经网络模型3.1 Homo-NN Quick Start: A Binary Classification Task3.2 Hetero-NN Quick Start: A Binary Classification Task 4. 自定义数据集示例&#xff1a;实现一个简…...

Atlas 元数据管理

Atlas 元数据管理 1.Atlas入门 1.1概述 元数据原理和治理功能&#xff0c;用以构建数据资产的目录。对这个资产进行分类和管理&#xff0c;形成数据字典。 提供围绕数据资产的协作功能。 表和表之间的血缘依赖 字段和字段之间的血缘依赖 1.2架构图 导入和导出&#xff1…...

编程题练习@8-23

分享8月23日两道编程题&#xff1a; 1 开幕式排列 题目描述 导演在组织进行大运会开幕式的排练&#xff0c;其中一个环节是需要参演人员围成一个环形。 演出人员站成了一圈&#xff0c;出于美观度的考虑&#xff0c;导演不希望某一个演员身边的其他人比他低太多或者高太多。 现…...

static相关知识点详解

文章目录 一. 修饰成员变量二. 修饰成员方法三. 修饰代码块四. 修饰类 一. 修饰成员变量 static 修饰的成员变量&#xff0c;称为静态成员变量&#xff0c;该变量不属于某个具体的对象&#xff0c;是所有对象所共享的。 public class Student {private String name;private sta…...

文做网站/新闻头条今日要闻国内

自助选座步骤分为&#xff1a;1、选择区域 ——> 2、 选择座位 ——> 3、 结账详细介绍如下&#xff1a;步骤一&#xff1a;选择区域在演出信息页选择场次(图1)&#xff0c;并点击在线选座&#xff0c;进入选择区域页面(图2)&#xff1b;选择观看演出的所需区域&#xff…...

专门做实习计算机项目的网站平台/西安网站建设比较好的公司

问题&#xff1a;如何在一张图上面绘制多组散点图和折线图&#xff1f;例如在下面的评分数据中&#xff0c;每轮数据绘制散点图&#xff0c;平均值绘制折线图。 数据&#xff08;excel&#xff09;&#xff1a;营业厅评分数据 做法&#xff1a;R语言ggplot函数 1 library(readx…...

婚纱摄影类网站模板/天津百度快照优化公司

Vue入门2与SpringBoot跨域请求Vue项目没有config目录怎么办: 1、引入axios 请参考我的另一篇博客 传送门 2、新建我们自己的测试页面 在views下新建目录test目录&#xff0c;test目录下新建Test.vue 3、然后再路由中心配置router\index.js 1、在单页面应用中&#xff0…...

王野天 演员/北京seo优化排名推广

1.应用场景 主要用于了解App架构的演进过程&#xff0c;以及对比端上架构与后端架构的区别&#xff0c;联系。 2.学习/操作 1.文档阅读 49 | 谈谈App架构的演进-极客时间 [转]Web 研发模式演变——玉伯 - 知乎 2.整理输出 49 | 谈谈App架构的演进-极客时间 专栏截止到上一期&a…...

网站建设运营费用包括哪些/网络营销好找工作吗

阅读本文大概需要 3.5 分钟。 本篇是设计模式系列的开篇&#xff0c;虽然之前也写过相应的文章&#xff0c;但是因为种种原因后来断掉了&#xff0c;而且发现之前写的内容也很渣&#xff0c;不够系统。 所以现在打算重写&#xff0c;加上距离现在也有一段时间了&#xff0c;也算…...

阿里备案成功后怎么做网站/怎样才能注册自己的网站

最近更新的博客 华为OD机试题 - 字符串加密(JavaScript) 华为OD机试题 - 字母消消乐(JavaScript) 华为OD机试题 - 字母计数(JavaScript) 华为OD机试题 - 整数分解(JavaScript) 华为OD机试题 - 单词反转(JavaScript) 华为OD机试题 最近更新的博客使用说明符合条件的子…...