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

news2025/4/27 18:35:36

文章目录

    • 一、第一个多线程程序
      • 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{
    @Override
    public 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 {
    @Override
    public 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() {
            @Override
            public 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() {
            @Override
            public 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);
        //把上述的标志位设置为true
        t.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) {
        
        //线程b
        Thread 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 结束了");
        });
        
        //线程a
        Thread 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-catch
                b.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 。

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

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

相关文章

基于React实现日历组件详细教程

前言 日历组件是常见的日期时间相关的组件&#xff0c;围绕日历组件设计师做出过各种尝试&#xff0c;展示的形式也是五花八门。但是对于前端开发者来讲&#xff0c;主要我们能够掌握核心思路&#xff0c;不管多么奇葩的设计我们都能够把它做出来。 本文将详细分析如何渲染一…

windows系统activemq一键安装启动

前言 官网下载的mq提供了启动和卸载服务器的脚本&#xff0c;但是安装的时候不会自动启动服务&#xff0c;需要去手动比较麻烦&#xff0c;执行脚本的时候也需要去右键选择管理员执行做起来比较啰嗦。 优化方案 修改脚本权限为自动判断并获取 在脚本开头加入下面的代码 …

店铺收款系统开源_商城收款分账系统_OctShop

近来&#xff0c;很多客户对OctShop店铺收款分账系统有很大的需求&#xff0c;下面详细介绍一下&#xff0c;门店收款分账系统是什么&#xff0c;以及其作用与意义是什么&#xff1f; 店铺收款分账系统实质上是一个解决门店各种收款场景&#xff0c;如&#xff1a;扫码付款&…

零基础学习,初学者也能轻松制作高质量母婴行业小程序

现如今&#xff0c;随着移动互联网的发展&#xff0c;小程序成为了各行各业的新宠。对于母婴行业来说&#xff0c;拥有一个功能完善、用户友好的小程序&#xff0c;无疑是提升企业形象和服务质量的重要一环。然而&#xff0c;对于初学者来说&#xff0c;如何轻松掌握母婴行业小…

微服务中间件--http客户端Feign

http客户端Feign http客户端Feigna.Feign替代RestTemplateb.自定义Feign的配置c.Feign的性能优化d.Feign的最佳实践分析e.Feign实现最佳实践(方式二) http客户端Feign a.Feign替代RestTemplate 以前利用RestTemplate发起远程调用的代码&#xff1a; String url "http:…

【面试题】你理解中JS难理解的基本概念是什么?

前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 作用域与闭包 作用域 作用域是当前的执行上下文&#xff0c;值和表达式在其中“可见”或可被访问。如果一个变量或表达式不在当前的作用域中&#xff0…

SpringBoot利用ConstraintValidator实现自定义注解校验

一、前言 ConstraintValidator是Java Bean Validation&#xff08;JSR-303&#xff09;规范中的一个接口&#xff0c;用于实现自定义校验注解的校验逻辑。ConstraintValidator定义了两个泛型参数&#xff0c;分别是注解类型和被校验的值类型。在实现ConstraintValidator接口时&…

【业务功能篇77】微服务-OSS对象存储-上传下载图片

3. 图片管理 文件存储的几种方式 单体架构可以直接把图片存储在服务器中 但是在分布式环境下面直接存储在WEB服务器中的方式就不可取了&#xff0c;这时我们需要搭建独立的文件存储服务器。 3.1 开通阿里云服务 针对本系统中的相关的文件&#xff0c;图片&#xff0c;文本等…

JavaSE 数组

定义&#xff1a; int []arr; int arr[]; 初始化 // 完整格式 int arr[] new int[]{1, 2, 3}; // 简单格式 int arr[] {1, 2, 3}; 数组的元素访问、遍历 按照下标访问即可。数组的长度函数为 arr.length()。idea快速生成遍历的方法&#xff1a;数组名.fori 静态初始化 &a…

9.Sentinel哨兵

1.Sentinel Sentinel&#xff08;哨兵&#xff09;是由阿里开源的一款流量控制和熔断降级框架&#xff0c;用于保护分布式系统中的应用免受流量涌入、超载和故障的影响。它可以作为微服务架构中的一部分&#xff0c;用于保护服务不被异常流量冲垮&#xff0c;从而提高系统的稳定…

【神州数码】BGP路由器案例

SwitchB、SwitchC和SwitchD位于AS200中&#xff0c;SwitchA位于AS100中。SwitchA和SwitchB共享一个相同的网络段11.0.0.0。而SwitchB和SwitchD彼此物理上不相邻。 则SwitchA的配置如下&#xff1a; SwitchA(config)#router bgp 100SwitchA(config-router-bgp)#neighbor 11.1.1…

指针(初阶)

1. 指针是什么&#xff1f; 指针是什么&#xff1f; 指针理解的2个要点&#xff1a; 1. 指针是内存中一个最小单元的编号&#xff0c;也就是地址 2. 平时口语中说的指针&#xff0c;通常指的是指针变量&#xff0c;是用来存放内存地址的变量 总结&#xff1a;指针就是地址&…

Wlan——锐捷零漫游网络解决方案以及相关配置

目录 零漫游介绍 一代零漫游 二代单频率零漫游 二代双频率零漫游 锐捷零漫游方案总结 锐捷零漫游方案的配置 配置无线信号的信道 开启关闭5G零漫游 查看配置 零漫游介绍 普通的漫游和零漫游的区别 普通漫游 漫游是由一个AP到另一个AP或者一个射频卡到另一个射频卡的漫…

jquery实现单独使用laydate时间控件设置开始时间,结束时间最大最小值以及设置默认时分秒

因项目内 会话时间所用框架为layui 里面的laydate时间控件 具体的设置文档里面都有些 我所用的这个不是日期时间范围 而是单独的日期时间的控件 意思就是两个是单独的 但是需要设置的是 开始最大 时间为结束时间的最小值 结束最小时间为开始结束的最大值 其余不能点击 当我选择…

leetcode54. 螺旋矩阵(java)

螺旋矩阵 题目描述解题 收缩法 上期经典算法 题目描述 难度 - 中等 原题链接 - leecode 54 螺旋矩阵 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7…

潮湿对电子元器件有哪些影响?如何选择电子防潮柜?

随着科技的飞速发展&#xff0c;电子设备的应用越来越广泛&#xff0c;无论是生活、工业、农业领域随处可见各种各样的电子设备。电子设备在稳定的环境中是可以短时间存放的&#xff0c;但如果放置环境的湿度和温度发生较大改变&#xff0c;其性能会受到影响。电子设备受潮会有…

开学后运营校园跑腿小程序行不行?

校园跑腿小程序的运营是完全可行的&#xff0c;它为学生提供了便捷的校园代办服务。随着社会的发展和生活节奏的加快&#xff0c;越来越多的学生需要在学业之余处理个人事务&#xff0c;如购买日常用品、快递代取、打印复印文件等。传统的校园跑腿服务通常由个别学生或者组织提…

LeetCode3.无重复字符的最长子串

虽然是一道中等题&#xff0c;但我5分钟就写完了&#xff0c;而且是看完题就知道怎么写&#xff0c;这一看就知道双指针&#xff0c;一个左一个右&#xff0c;右指针往后移如果没有重复的长度1&#xff1b;如果有重复的&#xff0c;左指针往右移&#xff0c;那如何判断重复呢&a…

MyBatis相关知识

什么是MyBatis&#xff1f; MyBatis 是一个开源、轻量级的数据持久化框架&#xff0c;是 JDBC 和 Hibernate 的替代方案。MyBatis 内部封装了 JDBC&#xff0c;简化了加载驱动、创建连接、创建 statement 等繁杂的过程&#xff0c;开发者只需要关注 SQL 语句本身。 什么是持久…

SpringBoot案例-修改员工-查询回显

根据页面原型&#xff0c;明确需求 页面原型 需求 在员工信息栏的右侧存在一个编辑按钮&#xff0c;点击该按钮可以对员工信息进行修改&#xff0c;但是修改之前&#xff0c;会出现上述页面&#xff0c;将员工原有的信息进行展示回显。 阅读接口文档 接口文档的链接如下&am…