乐趣区

多线程基础知识回顾

多线程的基础知识复习

1)进程和线程的区别

 进程:程序运行后,一个 QQ,微信等就是一个进程。线程:线程是进程中的最小单元。说简单的话说,线程就是程序中不同的执行路径。程序:QQ 是一个程序,是一个硬盘上的程序,

2) 线程 run 方法和 start 方法的区别

public class T01_WhatIsThread {
    // 新建了静态内部类继承 Thrread, 线程修改 1 秒,输入 T1
    private static class T1 extends Thread {
        @Override
        public void run() {for(int i=0; i<10; i++) {
               try {TimeUnit.MICROSECONDS.sleep(1);
               } catch (InterruptedException e) {e.printStackTrace();
               }
               System.out.println("T1");
           }
        }
    }
    //main 方法
    public static void main(String[] args) {new T1().run();
        //new T1().start();
        for(int i=0; i<10; i++) {
            try {TimeUnit.MICROSECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println("main");
        }

    }
}
输出
T1
T1
main
main
// 这个时候注释 18run 方法 开始 start 方法 执行结果大不一样
T1
main
T1
main

#结论 therad 的 start 方法 执行路径是分支的形式,而 run 方法是重上到下依次执行。

3)多线程的常用实现方法

// 线程主要实现的方法有 3 种
public class T02_HowToCreateThread {
    // 集成 Thread 的方法
    static class MyThread extends Thread {
        @Override
        public void run() {System.out.println("Hello MyThread!");
        }
    }
    // 实现 Runnable 接口
    static class MyRun implements Runnable {
        @Override
        public void run() {System.out.println("Hello MyRun!");
        }
    }
    // 三种线程不同的运行方式
    public static void main(String[] args) {new MyThread().start();
        new Thread(new MyRun()).start();
        //lamda 表达式来执行一个线程
        new Thread(()->{System.out.println("Hello Lambda!");
        }).start();}
}
// lamda 表达式也是一种方式,线程池也是一种方式
//Executors.newCachedThreadPool();
// 通过线程池去拿到一个线程,而这个线程还是要执行 runable 或者 start 的方法。

4)线程最基本的方法

  • 4.1 sleep

    • 当前线程睡眠多少时间,由其他线程来执行
  • 4.2 join

    • 常用于等待另外一个线程结束,也就是保证线程有序执行。
     // 测试 join 线程
    static void testJoin() {Thread t1 = new Thread(()->{
            // 线程 1 创建了 10 个线程
            for(int i=0; i<10; i++) {System.out.println("A" + i);
                try {
                    // 每个线程休眠 500 毫秒
                    Thread.sleep(500);
                    //TimeUnit.Milliseconds.sleep(500)
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
        });
        // 线程 2
        Thread t2 = new Thread(()->{
            try {
                // 线程 1 的线程加入。等待线程 1 结束
                t1.join();} catch (InterruptedException e) {e.printStackTrace();
            }
            // 线程 1 结束后,才开始执行此代码
            for(int i=0; i<10; i++) {System.out.println("B" + i);
                try {Thread.sleep(500);
                    //TimeUnit.Milliseconds.sleep(500)
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
        });
        // 分别启动
        t1.start();
        t2.start();}
    #执行结果
    A0
    A1
    A2
    A3
    A4
    A5
    A6
    A7
    A8
    A9
    B0
    B1
    B2
    B3
    B4
    B5
    B6
    B7
    B8
    B9
  • 4.3 yield

    • 当前线程先离开,返回到就绪的状态,至于这个线程是否会被 CPU 马上执行,还是先执行等待队列中已经等待的线程,这个不一定。看竞争
线程的状态图

  • Ready(就绪)Running(正在运行) 都属于 Runnable 状态。
  • TimedWaiting 的相关方法是指时间一到,就会自动恢复 runnable 状态
  • Waiting 状态是指必须被唤醒才能进入 Runnabel 状态
  • synchronized 得到同步代码块的锁 之前会进入阻塞状态,得到锁之后,线程运行
  • Terminated 线程停止
  • 不建议用 Thread 的 stop 方法来强行停止线程,有安全问题。
  • 对于 interrupt 的一些说明:

    # 线程通过 wait() 进入阻塞状态,此时通过 interrupt() 中断该线程;#调用 interrupt() 会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个 InterruptedException 的异常。#我们会 catch 这个异常,再根据业务逻辑去处理线程的后续行为。#代码示例
    @Override
    public void run() {
        try {// 1. isInterrupted() 保证,只要中断标记为 true 就终止线程。while (!isInterrupted()) {// 执行任务...}
        } catch (InterruptedException ie) {// 2. InterruptedException 异常保证,当 InterruptedException 异常产生时,线程被终止。}
    }
    //interrupt 用于控制业务场景的用法极少,正常用法一般是某一个线程阻塞时间很长很长,通过 interrupt 来打断线程。
  • 线程状态的小例子

    static class MyThread extends Thread {
            @Override
            public void run() {System.out.println(this.getState());
    
                for(int i=0; i<4; i++) {
                    try {Thread.sleep(500);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
    
                    System.out.println(i);
                }
            }
        }
    
        public static void main(String[] args) {Thread t = new MyThread();
    
            System.out.println(t.getState());
    
            t.start();
    
            try {t.join();
            } catch (InterruptedException e) {e.printStackTrace();
            }
    
            System.out.println(t.getState());
    
        }
        #输出结果
        NEW
        RUNNABLE
        0
        1
        2
        3
        TERMINATED
synchronize 锁
  • 需要注意的是 锁,所的是对象,而不是代码

    • 比如:

public class T {
    private int count = 10;
    private Object o = new Object();
    public void m() {synchronized(o) { // ** 任何线程要执行下面的代码,必须先拿到 o 的锁 **
        count--;
    System.out.println(Thread.currentThread().getName() + "count =" + count);
        }
    }
}
#问题 
上述代码如果 new 了 2 个不同的 object,o 和 o1,那 synchronize(o) 是锁誰呢,当然锁 o。```
  • synchronize 的几种锁的形式

    • 锁 this

      private int count = 10;
      public void m() {synchronized(this) { // 任何线程要执行下面的代码,必须先拿到 this 的锁
              count--;
              System.out.println(Thread.currentThread().getName() + "count =" + count);
          }
      }
    • 锁方法

      private int count = 10;
      public synchronized void m() { // 等同于在方法的代码执行时要 synchronized(this)
          count--;
          System.out.println(Thread.currentThread().getName() + "count =" + count);
      }
    • 锁静态方法

      private static int count = 10;
      public synchronized static void m() { // 这里等同于 synchronized(T.class)
          count--;
          System.out.println(Thread.currentThread().getName() + "count =" + count);
      }
      
      public static void mm() {synchronized(T.class) {// 考虑一下这里写 synchronized(this) 是否可以?(不可, 因为没有 new,)
              count --;
          }
      }
    • 小思考

      1) 下面的线程如何输出?2)加上 synchronize 后又有什么区别
      3)加上 volatile 后又什么区别 
      private /*volatile*/ int count = 100;
      
      public /*synchronized*/ void run() { 
          count--;
          System.out.println(Thread.currentThread().getName() + "count =" + count);
      }
      
      public static void main(String[] args) {T t = new T();
          for(int i=0; i<100; i++) {new Thread(t, "THREAD" + i).start();}
      }
      #1 打印列会出现重复或者实际减的数和打印的数不一致。// 打印结果抽取异常部分
      THREAD81 count = 37
      THREAD70 count = 37
      #2 synchronize 既保证可见又保证一致性
      #3 volatile 保证可见性,这个变量改后立马被线程发现。#4 加了 synchronize 就不必加 volatile。
    • 同一个线程可以同时掉加锁和不加锁的方法,并不会导致一个方法有锁,而导致不加锁的方法无法执行。
    • 读方法和写方法是否都需要上锁来保持一致,看具体的业务逻辑。如果写上锁,读不上锁,有可能脏读数据,这个根据业务来定。读上锁会让读取的效率大大降。
    • synchronize 是可重入锁,如果上锁的方法 1,调用上锁的方法 2,如果不可以重入,会产生死锁。
    • $\color{red}{程序如果出现异常,默认情况下锁会被释放}$

      public class T {
      int count = 0;
      synchronized void m() {System.out.println(Thread.currentThread().getName() + "start");
          while(true) {
              count ++;
              System.out.println(Thread.currentThread().getName() + "count =" + count);
              try {TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {e.printStackTrace();
              }
              
              if(count == 5) {
                  int i = 1/0; // 此处抛出异常,锁将被释放,要想不被释放,可以在这里进行 catch,然后让循环继续
                  System.out.println(i);
              }
          }
      }
      }}
      public static void main(String[] args) {T t = new T();
          Runnable r = new Runnable() {
      
              @Override
              public void run() {t.m();
              }
              
          };
          new Thread(r, "t1").start();
          
          try {TimeUnit.SECONDS.sleep(3);
          } catch (InterruptedException e) {e.printStackTrace();
          }
          
          new Thread(r, "t2").start();}
      }
      #// 打印结果 人为异常不补货,t2 得到锁。继续执行
      t1 count = 2
      t1 count = 3
      t1 count = 4
      t1 count = 5
      t2 start
      Exception in thread "t1" t2 count = 6
      java.lang.ArithmeticException: / by zero
      at com.mashibing.juc.c_011.T.m(T.java:27)
      at com.mashibing.juc.c_011.T$1.run(T.java:39)
      at java.base/java.lang.Thread.run(Thread.java:844)
      t2 count = 7
    • synchronize 的底层升级概

      推荐文章 锁升级的小段子
      偏向锁(谁来谁第一,谁偏向)—-》
      竞争 自旋锁,(自旋 10 次或者 JDK 目前规定为自旋线程超过 CPU 内核数的一半)
      如果超过了自旋的上限,就升级重量级锁,重锁是 OS(操作系统)级别的锁,并进入等待队列。

    • 锁使用的场景:

      $\color{red}{线程少, 上锁代码执行快,用自旋锁}$
      $\color{red}{线程多, 上锁代码执行长,用 OS 重量锁}$
      
  • 总结
    线程的概念、线程的启用方式,常用的方法介绍。
    线程的状态机、
    synchronize 锁的是对象,不是代码。
    synchronize 锁的集中方式
    synchronize 的锁的升级。
    异常锁,默认放弃锁。除非 catch 跳过循环。
    偏向锁、自旋锁(公平、非公平)重量级锁。
  • 作者 QQ 68335397 有问题指正或者讨论学习,留言也可。
退出移动版