java多线程

76 阅读8分钟

1.终止线程

  • 通过改变循环的条件结束线程
 package classTest6;
 ​
 public class Test {
     private static boolean isQuit = false;
     public static void main(String[] args) {
         
         Thread thread = new Thread(() ->{
             while (!isQuit) {
                 System.out.println("gggg");
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
 ​
 ​
         });
         thread.start();
         try {
             Thread.sleep(3000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("线程结束");
         isQuit = true;
         //通过该方法结束线程1
 ​
 ​
     }
 }
 ​
  • 通过调用interrupt方法

  • 注意

    1. 如果代码中有sleep,会存在一定得问题
    2. 在执行sleep得过程中,调用interrupt大概率sleep得休眠时间还没到,会被提前唤醒
    3. 唤醒得同时抛出interruptedException,紧接着就会被catch获取到,就会清除Thread对象得isinterrupted标志位
    4. 就会把标志位又返回false 代码就会报错
    5. 此时循环就会一直执行
 package classTest7;
 ​
 public class Test {
     public static void main(String[] args) throws InterruptedException {
         Thread t  = new Thread(() ->{
             while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hhhhh");
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();//打印异常
                     //抛出异常,结束线程(tiao'ch
                     break;
                 }
             }
            System.out.println("gggggg");
         });
         t.start();
             Thread.sleep(3000);
         System.out.println("game over");
         t.interrupt();
         //调用interrupt方法,来修改刚才标志位得值
     }
 }
 ​

2.等待线程

  • 有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。例如,排队的时候只有等前面的人离开了我们才能再上去(当然你也可以非常粗暴的插队,但是不建议)。

  • 多个线程的执行顺序是不确定的(随机调度,抢占式执行)

  • 可以通过一些api来影响线程执行的顺序 -> join

  • ps:看不懂线程的创建方式可以看我的上一篇文章,有详细的介绍

     package classTest9;
     ​
     public class Test {
         public static void main(String[] args) throws InterruptedException {
             Thread t = new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    System.out.println("hhhhh工作中");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                 System.out.println("执行完毕");
             });
             t.start();
             t.join();
             /*
             * 让main线程等待t线程结束(谁调用谁等待)
             * 如果t运行中,main线程就会阻塞,直到t运行结束*/
     ​
             System.out.println("主线程 ,t执行过后开始打印");
         }
     }
     ​
    
    • 结果:t线程调用join,join就会等待t线程执行结束

image.png

image.png

  • 死等(一直等待,不干其他的事情)
  • 带有超时时间的等待,过了超时时间就不等待
  • 带有超时时间的等待,有着更高的精度

3.线程的状态

  • NEW: Thread创建好了,还没调用start方法创建线程
  • RUNNABLE: 就绪状态
  • BLOCKED: 由于锁竞争引起的阻塞
  • WAITING: 死等
  • TIMED_WAITING: 时间的阻塞,到达一定时间过后自动解除阻塞,sleep和带有超时时间的join也会进入这个状态
  • TERMINATED: 线程执行完毕,对象仍然存在

4.线程的安全

  • 某个代码,单线程下执行没有问题,多线程执行下出现bug(线程调度是随机的)
 package classTest10;
 ​
 public class Test {
     private static int count = 0;
     public static void main(String[] args) throws InterruptedException {
         Thread t1 = new Thread(() -> {
             for (int i = 0; i < 500000; i++) {
                 count++;
             }
         });
         Thread t2 = new Thread(() -> {
             for (int i = 0; i < 500000; i++) {
                 count++;
             }
         });
         t1.start();
         t2.start();
         t1.join();
         t2.join();
         System.out.println("count = " + count);
         //count 的结果是不确定的,存在线程的安全
 ​
         /*
     
         * count++ 本质上是由三个cpu指令构成的
         * load:把内存数据加载到寄存器中
         * add 进行+1运算
         * save 把寄存器数据写到内存中
          
       */
 ​
     }
 }
 ​

原因

image.png

  1. 根据时间线,t1线程执行load操作后就被t2线程抢过去,t2执行过后结果自增为1,count的结果为1
  2. 然后又回到t1线程,load的操作再t2之前已经执行了,此时内存中的count的值为0,所以t1执行过后count的值还是为1
  • 正确的执行方法

image.png t1执行完过后内存中count的值为1,t2继续读取内存中的值进行自增操作,此时count的值为2

  • 根本原因:操作系统上的线程是抢占式执行,随机调度,线程之间执行的顺序带来了很多的变数
  • 代码结构:代码中多个线程,同时修改同一变量
  • 直接原因:上述多线程修改操作,本身不是原子的(每个cpu指令都是原子的),一个线程执行这些指令,执行到一半,可能就会被调走(加锁进行解决)
  • 利用 synchronized进行加锁操作,此时t1就不会像之前那样进行插队,就不会有线程的安全问题(之前说过不建议插队,这下信了吧,可是有大问题的哦)
 package classTest10;
 ​
 public class Test {
     private static int count = 0;
     public static void main(String[] args) throws InterruptedException {
         /*
         * java中任意对象都可以当作锁对象
         * 通过锁对象进行加锁
         * 当t1被加锁过后,t2会阻塞,直到t1结束过后才可以对其加锁
         */
         Object locker = new Object();
         Thread t1 = new Thread(() -> {
             for (int i = 0; i < 500000; i++) {
                 synchronized (locker) {
                     count++;
                 }
             }
         });
         Thread t2 = new Thread(() -> {
             for (int i = 0; i < 500000; i++) {
                 synchronized (locker) {
                     count++;
                 }
             }
         });
         t1.start();
         t2.start();
         t1.join();
         t2.join();
         System.out.println("count = " + count);
 ​
 ​
     }
 }
 ​
  • 内存可见性问题
 package classTest14;
 ​
 import java.util.Scanner;
 ​
 public class Test {
     private static int flag = 0;
     public static void main(String[] args) {
         Thread t1 = new Thread(() -> {
             /*
             * 此时进行的核心指令有两条
             * 1.load读取内存中flag的值到cpu寄存器中
             * 2.拿到寄存器的值和0进行比较(条件跳转指令)
             * 此时频繁的执行load和条件跳转
             * load的开销大,并且load没有变化
             * 此时jvm就可能进行代码优化,对load进行优化,
             * jvm会只智能的对代码进行优化 */
             while (flag == 0) {
                 /*
                 * 相当于t2修改了内存 但是t1没有看到这个内存的变化
                 * 当循环里面有代码的时候,这个问题就会得到解决
                 * 可见内存可见性高度依赖编译器优化 什么时候触发是不确定的
                 * */
 ​
             }
             System.out.println("t1循环结束");
         });
         Thread t2 = new Thread(() -> {
             System.out.println("输入flag的值 :");
             Scanner sc = new Scanner(System.in);
             flag = sc.nextInt();
         });
         /*
         * t2要等待用户输入 此时t1已经开始循环了很多次
         * 结果:输入flag的值过后 循环并没有结束*/
         t1.start();
         t2.start();
 ​
     }
 }
 ​
  • 上述过程在jvm中的情况

    1. 编译器发现,每次循环都要读取主内存(内存)
    2. 就会把数据从主内存复制到工作内存(cpu寄存器)中,后续读取的都是工作内存
  • 解决办法:java提供了volatile 可以将上面的优化强制关闭,确保每次循环条件都会从内存中读取数据

  • 指令重排序问题

5.死锁

 package classTest12;
 public class Test {
     public static void main(String[] args) {
         Object A = new Object();
         Object B = new Object();
         Thread t1 = new Thread(() -> {
             synchronized (A) {
                 //sleep一下,给t2时间,让t2也能拿到B
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 //尝试获取B  没有释放A
                 synchronized (B) {
                     /*
                     * 彼此都在等待对方的锁
                     * 但是双放的锁都没有释放
                     * 出现死锁*/
                     System.out.println("t1拿到了两把锁");
                 }
             }
         });
         Thread t2 = new Thread(() -> {
             synchronized (B) {
                 //sleep一下,给t1时间,让t1也能拿到A
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 synchronized (A) {
                     System.out.println("t2拿到了两把锁");
                 }
             }
         });
         t1.start();
         t2.start();
 ​
     }
 }
 ​
  • 死锁产生的四个必要条件

    1. 互斥使用:一个线程拿到锁过后,另外一个锁想要获得,只能阻塞等待
    2. 不可抢占:一个线程拿到锁之后,只能主动解锁,不能让别的线程强行把锁抢走
    3. 请求保持:一个线程拿到锁A之后吗,在持有A的前提下,尝试获取B
    4. 循环等待(环路等待)
  • 解锁:破坏其中一个条件即可 (最好的方式,避免循环等待)

6.wait 和 notify

  • wait() / wait(long timeout): 让当前线程进⼊等待状态.

wait 做的事情:

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

• 释放当前的锁

• 满⾜⼀定条件时被唤醒, 重新尝试获取这个锁.

wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常.

wait 结束等待的条件: •

其他线程调⽤该对象的 notify ⽅法.

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

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

 package classTest15;
 ​
 public class TEst {
     public static void main(String[] args) throws InterruptedException {
         Object ob = new Object();
         synchronized (ob) {
             System.out.println("wait 之前");
             ob.wait();//释放ob对象的锁 此时线程进入等待状态 
             //同时wait被唤醒后,还是可以参与枪锁
             System.out.println("wait 之后");
         }
     }
 }
 ​
 
 
 

结果(当前线程进入了等待状态,并没有被唤醒,所以不会执行后面的代码)

image.png

  • wait的几个版本

image.png 0. 死等

  1. 带有时间的等待 最多等待timeoutmillis 过了这个时间就不在等待(这个时间是兜底的)
  2. sleep 提前唤醒 通过异常的方式,正常的业务逻辑不应该依赖于此
  • notify() 只唤醒其中一个线程
  • notifyAll(): 唤醒在当前对象上等待的所有线程
 package classTest16;
 ​
 public class Test {
     public static void main(String[] args) {
         Object ob = new Object();
         Thread t1 = new Thread(() -> {
             synchronized (ob) {
                 System.out.println("t1 wati 之前 ");
                 try {
                     ob.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("t1 wait 之后");
             }
         });
         Thread t2 = new Thread(() -> {
                 /*
                 * wait放到 synchronized里面是因为要释放锁,首先需要加锁
                 * notify可以不放到 synchronized,
                 * 因为不需要加锁,但是java中约定要放入其中
                 * 同时两个对象应该一致 */
                 try {
 ​
                     Thread.sleep(5000);//写到 synchronized外面
                     synchronized (ob) {
 ​
                         System.out.println("t2 notify 之前");
                         ob.notify();
                         System.out.println("t2 notify 之后");
                     }
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             });
         t1.start();
         t2.start();
     }
 }
 /*
 * 结果
 * t1 wati 之前
 t2 notify 之前
 t2 notify 之后
 t1 wait 之后
 notify唤醒了等待的线程,线程会重新加锁,继续执行
 注意:此时t1被唤醒过后,t2线程还没有释放锁,所以t1要进行等待,直到t2释放锁,t1线程才会继续。这就是为什么t2 notify 之后 在  t1 wait 之后 先执行的原因。
 */