javaEE之多线程(2)

线程等待join:

本质上是让一个线程等待另一个线程结束,再继续执行自己的任务

java 复制代码
public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(true){
                System.out.println("hello thread");
                try{
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
      t.join(); 
        System.out.println("main线程结束");
    }
}

这里t.join就是让main线程阻塞等待,直到t线程执行完毕后,再执行main线程。

另外,我们也可以自己设定等待时间,超出这个等待时间后,线程便不会再继续等待。

获取当前线程的引用:

public static Thread currentThread(); //哪个线程调用这个方法,就返回哪个线程的引用

线程的状态:

线程的状态是一个枚举类型**:Thread.State();**

共有六种状态:

1.new: 安排了工作,但还没有开始行动(创建了thread对象但还没有开始start)

2.terminated:工作已经完成

3.Runnable:正在工作中:分为正在工作和即将开始工作

4.Timed_Waiting: 指定时间的阻塞(阻塞时间有上限)

5.Waiting:死等,没有超时时间的等待,死等

6.Blocked:由锁导致的阻塞

java 复制代码
public class Demo13 {
    public static void main(String[] args)throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 2; i++) {
                try{
                    Thread.sleep(1000); //将此线程休眠2s
                }catch (InterruptedException e){
                    throw new RuntimeException(e);
                }
            }
            });



        System.out.println(t.getState());//线程已创建但还没有开始执行(start),为new
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println(t.getState());//线程正在运行,是runnable
            Thread.sleep(1000);
        }
        t.join();
        System.out.println(t.getState());
    }
}

在第一段获取线程状态时,线程t 创建但还没有start,所以是new

之后主线程进行五次打印(打印T线程的状态),第一次循环打印t状态,t线程状态是runnable的(此时t线程已经进入任务但还没有执行到sleep),剩下的四次打印,期间t线程处于sleep状态(6s),所以为timed_waiting。 此后通过join等待t线程结束,打印t线程状态为terminated。

运行结果如下:

线程安全问题:

我们先来看一个线程不安全的案例:

java 复制代码
public class Demo14 {
    public static int count =0;
    public static void main(String[] args) throws InterruptedException{
         Thread t1 = new Thread(()->{
             for (int i = 0; i < 50000; i++) {
                count++;
             }
         });
         Thread t2 = new Thread(()->{
             for (int i = 0; i < 50000; i++) {
                 count++;
             }
         });

         t1.start();
         t2.start();

         t1.join();
         t2.join();
        System.out.println(count);
    }
}

我们设置了两个线程t1,t2,for循环自增count,并设置了join()来让一个线程等待另一个线程执行完再执行自己的。 count的预期结果应该是100000,

但是几次的运行结果都不一样,差距过大。

在多线程中代码并发执行,出现这样的bug,这样的bug称为线程不安全/线程安全问题。

为什么会这样呢?我们先来看count++这个操作:

count++这个代码,实际上对应了三个cpu执行指令:

1.load:把内存中的count值,加载到cpu的寄存器。

2.add:把寄存器中的内容+1

3. save:把寄存器中的内容保存到内存上

下面用图示来更直白的表示:

以上的这些执行顺序是正确的,要么先执行t1,再执行t2,要么先执行t2,再执行t1。

但是操作系统对于线程的调度是随机的,很可能t1执行到1、2的时候,被调走去执行t2的1、2了。

如下:

此时回如何进行计算呢?

  1. t1执行load操作:读取count值(0)到cpu寄存器上

  2. t2执行load操作:读取count的值(0)到cpu寄存器上

  3. t2执行add:把寄存器上的count+1(变为1)

  4. t2执行save:把寄存器中的内容保存到内存上

  5. t1执行add:把寄存器上的count+1(变为1)

  6. t1执行save:把寄存器中的内容保存到内存上。

如上,执行了两次相加操作但实际只加了一次,这种类似的情况还存在由好多种,就不一一列举了。

线程安全问题产生的原因:

1.根本原因:操作系统对于线程的调度是随机的,抢占式执行

2.多个线程同时修改同一个变量

3.修改操作不是原子的

4.内存可见性问题引发的线程不安全

5.指令重排序引起的线程不安全

注:原子性:修改操作只对应到一个cpu指令,就可以认为是原子的,如果对应到多个cpu指令,就不是原子的。

如何解决线程不安全呢?我们就需要用到 "锁" 这一概念,通过加锁来解决线程不安全问题

通过加锁操作,让不是原子的操作,打包成一个原子的操作。

一旦把锁加上了,其他人想要加锁,就必须要阻塞等待

我们可以使用锁,把先前不是原子的count++包裹起来,在count++之前,先加锁,之后再进行count++,等计算完毕后,再进行解锁。这样在执行过程中,就不会由别的线程插队了。

注:加锁操作不是把线程锁死到cpu上,禁止这个线程被调度走,而是禁止其他线程重新加上这个锁,避免其他线程的操作、插队。

Synchronized

java中,使用Synchronized关键字来搭配代码块来实现相关逻辑。

java 复制代码
public class Demo15 {
     public static int count = 0;
    public static void main(String[] args)throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000 ; i++) {
                 synchronized (locker1){
                     count++;
                 }
            }
        });
         Thread t2 = new Thread(()->{
             for (int i = 0; i <50000 ; i++) {
                 synchronized (locker1){
                       count++;
                 }
             }
         });
         t1.start();
         t2.start();

      t1.join();
      t2.join();//如果不加join,会导致线程还未执行完就打印count

        System.out.println(count);
    }
}

加锁之后,我们看到,结果正好符合预期。

这个小括号里,我们要填的是锁对象,这个对象的类型不重要,重要的是是否有多个线程针对这一个对象加锁(竞争同一个锁)。

两个线程,针对同一个线程加锁,才会产生互斥效果(一个线程加上锁,另一个线程就要阻塞等待直到那个线程将锁释放),如果针对的是不同的锁对象,不会有互斥效果,线程安全问题不会得到改变。

synchronized的其他变种写法:

使用synchronized 修饰方法,针对this进行加锁

java 复制代码
class Counter{
    private int count = 0;
    synchronized public void add(){
        count++;

    }

    public int get(){
        return count;
    }

synchronized修饰static方法,相当于针对类对象进行加锁

java 复制代码
  public synchronized static void func(){
        synchronized (Counter.class){

        }
    }
}

这两种方式都是实现synchronized的方式。

可重入锁:同一个线程在持有锁的情况下,多次获取同一个锁,而不会导致死锁。

不可重入锁:不能对已持有的锁重复获取。

代码示例:

java 复制代码
public class Demo17 {
    public static void main(String[] args) throws InterruptedException {
        Counter2 counter2 = new Counter2();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (counter2) {
                    synchronized (counter2) {
                        synchronized (counter2) {
                            counter2.add();

                        }
                    }
                }
            }
        });
             t1.start();
             t1.join();
        System.out.println("counter2 =" +counter2.get());
    }

synchronized就是可重入锁。

死锁:

两个或多个线程在执行过程中因为争抢资源造成的一种互相等待的现象,导致线程永远阻塞,永远无法执行。

以一段代码为例:

java 复制代码
public class Demo18 {
    public static void main(String[] args)throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                //拿起酱油
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2) {
                    System.out.println("t1线程两个锁都获取到");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                //拿起醋
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1) {
                    System.out.println("t1线程两个锁都获取到");
                }
            }

        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

    }
}

这里创建了两个线程t1、t2以及两个锁对象locker1和locker2,

当两个线程同时运行时,可能会发生一下情况:

1.t1拿到locker1锁

2.t2拿到locker2锁

3.t1休眠1s后尝试获取locker2,但locker2已经被t2获取,t1线程阻塞

4.t2休眠1s后尝试获取locker1,但locker1已经被t1获取,t2线程阻塞

这样便形成了死锁,程序会卡死,无法继续运行,也不会打印代码上的结果。

Volatile:

内存可见性:

代码在写入Volatile修饰的变量的时候

改变线程工作内存中volatile变量的值,

将改变后的副本值从工作内存刷新到主内存。

在读取Volatile修饰的变量的时候,
从主内存中读取volatile变量的最新值到线程的⼯作内存中

从⼯作内存中读取volatile变量的副本
核心作用:

  1. 保证变量的修改对所有线程立即可见,解决工作内存副本不同步的问题;
  2. 禁止编译器和CPU对volatile变量的读写操作做指令重排序,保证操作顺序符合代码逻辑。

代码案例:

java 复制代码
public class Demo19 {
    private volatile  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的值");
                     flag = scanner.nextInt();
                 });
                 t1.start();
                 t2.start();

    }
}

通过volatile赋予变量的内存可见性,t2线程对flag的修改,会立刻同步到主内存,同时让t1线程的工作内存中缓存的flag失效,t1下一次读取flag时一定会拿到最新的值,不会一直卡在死循环里。

wait和notify

wait():使当前执⾏代码的线程进⾏等待.

工作过程:

使当前执⾏代码的线程进⾏等待. (把线程放到等待队列中)

释放当前的锁

满⾜⼀定条件时被唤醒, 重新尝试获取这个锁
wait结束等待的条件:
其他线程调⽤该对象的 notify ⽅法.

wait 等待时间超时 (wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间).

其他线程调⽤该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常

Notify():唤醒wait()的等待的线程

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

java 复制代码
public class Demo24 {
    public static void main(String[] args)throws InterruptedException {
        Object locker = new Object();
        Object locker2 = new Object();
         Thread t1 = new Thread(()->{
             try{
                 Thread.sleep(3000);
                 System.out.println("wait之前");
                 synchronized (locker){
                     locker.wait();
                 }
                 System.out.println("wait之后");

             }catch (InterruptedException e){
                  throw new RuntimeException(e);
             }
         });

         Thread t2 = new Thread(()->{
             Scanner scanner = new Scanner(System.in);
             System.out.println("输入任意内容唤醒t1");
             scanner.next();

             synchronized (locker){
                 locker.notify();
             }
         });
         t1.start();
         t2.start();

    }
}


这里t1线程休眠3s进入等待,直到t2线程输入任意内容后,通过notify唤醒t1线程
NotifyAll:唤醒所有wait等待的线程。

wait和sleep的区别:

1. wait 需要搭配 synchronized 使⽤. sleep 不需要.
2. wait 是 Object 的⽅法 sleep 是 Thread 的静态⽅法

相关推荐
Devin~Y1 小时前
从内容社区到AIGC客服:Spring Boot、Redis、Kafka、K8s、RAG的三轮大厂Java面试对话(附标准答案)
java·spring boot·redis·spring cloud·kafka·kubernetes·micrometer
KaMeidebaby1 小时前
卡梅德生物技术快报|生信实操:ChIP 染色质免疫共沉淀技术流程、短板与替代方案详解
前端·人工智能·物联网·百度·新浪微博
weixin199701080161 小时前
[特殊字符] 【性能提升300%】仿1688首页的Webpack优化全记录(附构建分析Python脚本)
前端·python·webpack
それども1 小时前
怎么理解TCP的状态
java·网络·网络协议·tcp/ip·dubbo
Xzh04231 小时前
Redis黑马点评 实战复盘与面试高频考点详解
java·数据库·redis·面试
YOU OU1 小时前
案例综合练习-博客系统
java·开发语言
海兰1 小时前
【文字三国志:第五篇】天命重构,游戏前端UI设计
前端·人工智能·游戏·语言模型
海鸥-w1 小时前
前端学习python第二天手敲笔记整理
前端·python·学习
爱吃提升1 小时前
Figma 组件库搭建清单(含命名规范+常用组件模板)
前端·javascript·figma