Java-Day-23( 线程二:线程终止 + 常用方法 + 用户线程 + 守护线程 + 线程生命周期 + Synchronized + 互斥锁 + 死锁 + 释放锁 )

发布时间 2023-05-20 23:27:55作者: 朱呀朱~

Java-Day-23

线程终止

  • setLoop()

基本说明

  • 当线程完成任务后,会自动退出
  • 还可以通过使用变量来控制 run 方法退出的方式停止线程,即通知方式

练习使用

public class test1 {
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        t1.start();
//        如果希望main线程控制t1线程的终止,必须可以修改loop
//        让t1退出run方法,从而终止t1线程 —> 通知方式

//        让主线程休眠 10 秒,再通知t1线程退出
        System.out.println("线程main休眠10s,休眠结束就执行false操作");
        Thread.sleep(10 * 1000);
        t1.setLoop(false); // 
    }
}

// 使用 Thread 方式
class T extends Thread {
    int count = 0;
//    设置一个控制变量
    private boolean loop = true;
    @Override
    public void run() {
        while (loop) { // 控制循环
            try {
                Thread.sleep(50); // 休眠50ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T 运行中......" + (++count));
        }
    }
    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

常用方法

常用方法第一组

  • setName:设置线程名称,使之与参数 name 相同
  • getName:返回该线程的名称
  • start:使该线程开始执行 ( Java 蓄奴就底层调用该线程的 start0 方法 )
  • run:调用该线程对象 run 方法
  • setPriority:更改线程的优先级
  • getPriority:获取线程的优先级
  • sleep:在指定的毫秒数内让当前正在执行的线程休眠 ( 暂停执行 )
  • interrupt:中断线程
public class test {
    public static void main(String[] args) throws InterruptedException {
//        测试相关的方法
        T t = new T();
        t.setName("zyz 的你");
        t.setPriority(Thread.MIN_PRIORITY); // 设置其优先级为最低:1
        t.start(); // 启动子线程

//        System.out.println(t.getName());
        for (int i = 0; i < 5; i++) {
            Thread.sleep(1000); // 每隔一秒钟输出一个 hi
            System.out.println("hi ~ " + i);
        }
        System.out.println(t.getName() + "线程的优先级 = " + t.getPriority());
//        中断线程
        t.interrupt(); // t线程在吃包子休眠还没到10秒就被终止了休眠状态,while又开始循环了
    }
}

// 使用 Thread 方式
class T extends Thread {
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 100; i++) {
    //            Thread.currentThread().getName() 获取当前线程的名称
                System.out.println(Thread.currentThread().getName() + "恰饭" + i);
            }
            try {
                System.out.println(Thread.currentThread().getName() + "睡了");
                Thread.sleep(10000); // 输出100个后,休眠10秒
            } catch (InterruptedException e) {
    //            当该线程执行到一个 interrupt 方法时,就会 catch 一个异常,可以加入自己的业务代码
    //            InterruptedException 是捕获到一个中断异常
                System.out.println(Thread.currentThread().getName() + "被 interrupt 了");
            }
        }
    }
}

注意细节

  • start 底层会创建新的线程,调用 run,run 就是一个简单的方法调用,不会启动新线程 ( start0 才是真正实现 )

  • 线程优先级的范围 ( public final static int )

    • MIN_PRIORITY = 1
    • NORM_PRIORITY = 5
    • MAX_PRIORITY = 10
  • interrupt:中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠线程

  • sleep:线程的静态方法,使当前线程休眠

常用方法第二组

  • yield:线程的礼让 ( 调用自己的 )。让出 cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功 ( 主是操作系统 cpu 决定的,看资源是否紧张,资源够多就不会执行礼让 )

  • join:线程的插队 ( 调用对方的 )。插队的线程一旦插队成功,则肯定先执行完插入的线程的所有的任务 ( 然后才会回到一开始执行的线程 )

    • 创建一个子线程,每隔 1s 输出 hello,输出 10 次,主线程每隔 1s 输出 hi,输出 10 次 ( 要求:两个线程同时执行,当主线程输出 5 次后,就让子线程运行完毕,主线程再继续 )

      public class test {
          public static void main(String[] args) throws InterruptedException {
              T t = new T();
              t.start(); // 启动子线程
      
              for (int i = 1; i <= 10; i++) {
                  Thread.sleep(1000); // 每隔一秒钟输出一个 hi
                  System.out.println("主线程:hi ~ " + i);
                  if (i == 5) {
                      System.out.println("主线程让步于子线程,即插队");
                      t.join();
      //                Thread.yield(); // 礼让不成功
      
                      System.out.println("子线程运行完毕");
                  }
              }
          }
      }
      
      class T extends Thread {
          @Override
          public void run() {
              for (int i = 1; i <= 10; i++) {
      
                  try {
                      Thread.sleep(1000); //休眠1秒
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println("子线程:hello ~ " + i);
      
              }
          }
      }
      

用户线程 ( 通知方式 )、守护线程

  • 用户线程:也叫工作线程,当线程的任务执行完毕或以通知的方式结束

    • 通知方式前提:一个线程持有另一个线程的变量
  • 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束

  • 常见经典的守护线程:垃圾回收机制

    • 将一个线程设置成守护线程 ( 若是一个主线程创建一个无限循环的子线程,就算是主线程退出了,子线程还在执行,所以设置成守护线程,使之就算是无限循环,也会在主线程执行完毕后就退出 )

      public class test {
          public static void main(String[] args) throws InterruptedException {
              MyDaemonThread myDaemonThread = new MyDaemonThread();
      //        想在main主线程狗作完了,子线程的铲屎官就结束休息,就将子线程设置为守护线程即可
              myDaemonThread.setDaemon(true);
      //        先设置再启动
              myDaemonThread.start();
              for (int i = 1; i <= 10; i++) {
                  System.out.println("小小狗wu~哇哈哈哈哈~~~ " + i);
                  Thread.sleep(1000);
              }
          }
      }
      
      class MyDaemonThread extends Thread { // Daemon:守护线程
          int count = 0;
          @Override
          public void run() {
              for (; ;) { // 无限循环
                  try {
                      count++;
                      Thread.sleep(1000); //休眠1秒 (毫秒与秒是千位进)
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println("辛勤工作的铲屎官 " + count);
              }
          }
      }
      

线程的生命周期

image-20230520232140990

  • JDK 中用 Thread.State 枚举表示了线程的六种状态 ( 官方 )
    • 实则有的是七种:Runnable 细化了 ( 内核部分编码看不出 )
  • new一个线程,NEW 状态在调用 start0 后,进入可运行状态,即 Runnable 状态 ( 细化为就绪 Ready 状态、运行 Running 状态,真正运行还要取决于进程调度器 — 内核,操作系统决定的 );
    • 运行 Running 状态 — ( 线程挂起或 Thread.yeild — ) —> 就绪 Ready 状态
  • 若是运行 Running 完毕的就进入终止 Teminated 状态
  • 可运行 Runnable 状态时
    • 若是进入同步代码块 ( 获取一把锁 ) 就进入阻塞 Blocked 状态,
    • 调用 wait()、join() ... 就进入等待 Waiting 状态
    • 调用 sleep() ... 进入超时等待 TimedWaiting 状态
  • 随后各自经过一些操作再回到可运行 Runnable 状态 ( 并非一定是 运行状态,也可能是就绪状态 )
public class test1 {
    public static void main(String[] args) throws InterruptedException {
        T t = new T(); // NEW
        System.out.println(t.getName() + " 状态 " + t.getState());
        t.start(); // RUNNABLE

//        只要不是终止就不断执行看处于何状态
        while (Thread.State.TERMINATED != t.getState()) {
            System.out.println(t.getName() + " 主状态 " + t.getState()); // T:sleep 主:sleep
            Thread.sleep(500);
        }

        System.out.println(t.getName() + " 状态 " + t.getState()); // break:TERMINATED
    }
}

class T extends Thread { 
    int count = 0;
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 10; i++) {
                System.out.println("T~hi~" + i);
                try {
                    Thread.sleep(1000); // TIMED_WAITING
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            break; // TERMINATED
        }
    }
}

Synchronized

  • 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性
  • 也可以理解为:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作

方式一

synchronized(对象) { // 得到对象的锁,才能操作同步代码
    // 需要被同步代码;
}
  • 拿到锁才能操作 ( 需要被同步代码 ),然后放回锁,另一个对象再来拿到锁,才能再进行操作

方式二

// synchronized 放方法声明中,表示整个方法 ——> 为同步方法
public synchronized void m (String name) {
    // 需要被同步的代码
}
  • 同一时刻只能有一个对象执行此方法
  • 类似于试衣间,进一人上锁拴上,各操作后,出来开门栓,由下一个进入的人再上锁拴上

售票优化

public class test1 {
    public static void main(String[] args) throws InterruptedException {
//        使用 synchronized 方式就会防止超卖现象
        SellTicket001 sellTicket = new SellTicket001();
//        使用实现接口的方式来售票
        new Thread(sellTicket).start(); // 第一个窗口(简写方式)
        new Thread(sellTicket).start(); // 第二个窗口
        new Thread(sellTicket).start(); // 第三个窗口
    }
}

class SellTicket001 implements Runnable {
    private int ticketNum = 200;
    private boolean loop = true;

    public synchronized void sell() { // 改成同步方法,在同一时刻只能有一个线程来操作执行此方法
            if (ticketNum <= 0) {
                System.out.println("售票结束..."); // 三个线程都要进来一次,所以输出三次售票结束
                loop = false;
                return;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("窗口 " + Thread.currentThread().getName() + " 出售了一张票 "
                    + "剩余票数 = " + (--ticketNum));
    }

    @Override
    public void run() {
//        run是重写的都有的方法,因内带while死循环(票没买完前),进一个线程上锁就不出去了,所以把 synchronized 用在其调用的售票方法里
//        使之可多个进while,但sell()同一时刻只能进一个
        while (loop) { // 卖完就false
            sell();
        }
    }
}
  • 若是只出现一个或两个窗口的售票现象,那就把票数拉高、休眠拉低 ( 就会发现三个窗口都会出现售票情况且最好都会显示售票结束,不会超卖 )

分析同步原理

  • 对象锁靠抢,若是窗口一抢到售票后返回然后还回锁,仍继续是三个窗口再抢

互斥锁

了解

  • Java 语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
  • 每个对象都对应于一个可称为 “ 互斥锁 ” 的标记 ( 在底层某位置 ),这个标记用来保证在任一时刻,只能有一个线程访问该对象
  • 关键字 synchronized 来与对象的互斥锁联系,当某个对象用 synchronized 修饰时,表明该对象在任意时刻只能由一个线程访问
  • 同步的局限性:导致程序的执行效率要降低 ( 类似 ETC 一杆一车,除了获取锁的线程外,其他线程都阻塞 )
  • 同步方法 ( 非静态的 ) 的锁可以是 this 本身 (代码见 — 1. 与 2.1 ),也可以是其他对象 ( 要求是同一个对象 ) ( 代码见 — 2.2 )
  • 同步方法 ( 静态的 ) 的锁为当前类本身 ( 类.class,代码见 — *1. 与 *2. )
public class test1 {
    public static void main(String[] args) throws InterruptedException {
//        使用 synchronized 方式就会防止超卖现象
        SellTicket001 sellTicket = new SellTicket001();
//        使用实现接口的方式来售票
        new Thread(sellTicket).start(); // 第一个窗口(简写方式)
        new Thread(sellTicket).start(); // 第二个窗口
        new Thread(sellTicket).start(); // 第三个窗口
    }
}

class SellTicket001 implements Runnable {
    private int ticketNum = 200;
    private boolean loop = true;
//    2.3.1 因为虽然三个窗口,但都是sellTicket这个对象,所以创建的object实际上也都是一个(实际就相当于把object当作锁了)
    Object object = new Object(); // 2.2.1 可以是this当前(sellTicket)对象, 2.3.2 也可以是此对象里相同的object对象(只创建了一次)

//    1. sell() 加了 synchronized ,为同步方法,这时,锁在 this 当前(new了的)对象
//    2. 也可以在代码块上写synchronized,为同步代码块,互斥锁还是在this当前对象,或者是其他(同一个对象的)对象--(同一个sellTicket对象的其他(相对于sellTicket来说)对象:object)
    public /* synchronized */ void sell() { // 1.1 改成同步方法,在同一时刻只能有一个线程来操作执行此方法

//        synchronized (this) { // 2.2 同步代码块 this(本身为锁)
        synchronized (/* this */ object) { // 2.3 同步代码块 其他对象(从本身提取出来一个object充当锁)

            if (ticketNum <= 0) {
                System.out.println("售票结束..."); // 三个线程都要进来一次,所以输出三次售票结束
                loop = false;
                return;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("窗口 " + Thread.currentThread().getName() + " 出售了一张票 "
                    + "剩余票数 = " + (--ticketNum));
        }
    }



//        若是如下:
    public synchronized static void m1(){}
//        *1. 静态的同步方法(锁是加在 SellTicket001.class 对象里)

//        *2. 想在静态方法里面加(实现)一个同步代码块
    public static void m2(){
        synchronized (SellTicket001.class){ // *2.1 拿类名(而非this)
            System.out.println("m2");
        }
    }

    @Override
    public void run() {
//        run是重写的都有的方法,因内带while死循环(票没买完前),进一个线程上锁就不出去了,所以把 synchronized 用在其调用的售票方法里
//        使之可多个进while,但sell()同一时刻只能进一个
        while (loop) { // 卖完就false
            sell();
        }
    }
}

注意细节

  • 同步方法如果没有使用 static 修饰,默认锁对象:this
  • 如果方法使用 static 修饰,默认锁对象:当前类.class
    • 且锁只在代码块时需要写出来 ( 显式体现出来 )
  • 实现的落地步骤:
    • 需要先分析上锁的代码 ( 如:售票时判断票数的时候 )
    • 选择用同步代码块 ( 推荐,同步范围越小,效率越高 ) 或者是同步方法 ( 一大块方法 )
    • 要求多个线程的锁对象为同一个 ( 如:售票里的 )
      • 如,使用 Thread 时,底层是 new,两个窗口就是两个对象 ( 不同地址 )、两个 this 了 ( 锁也就是两个了,没有争夺的必要了 ),所以必须保证对象是同一个、共享的 ( 这样 this 锁只有一个,才会争夺,才能锁住 )

线程的死锁

  • 多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避免死锁的发生
    • 案例:母亲说小明要先写完作业才能玩手机,小明说要让我先玩手机,才去写作业
public class test1 {
    public static void main(String[] args) {
//        模拟死锁
        DeadLockDemo o1 = new DeadLockDemo(true);
        DeadLockDemo o2 = new DeadLockDemo(false);
        o1.setName("A");
        o2.setName("B");
        o1.start();
        o2.start();
    }
}

class DeadLockDemo extends Thread {
    static Object o1 = new Object();
    static Object o2 = new Object();
    boolean flag;

    public DeadLockDemo(boolean flag) { //构造器
        this.flag = flag;
    }

    @Override
    public void run() {
//        T 的话,线程A就会先得到、持有 o1 对象锁,然后去尝试获取 o2 对象锁
//        拿不到 o2 就会 Blocked
        if (flag) {
            synchronized (o1) { // o1:对象互斥锁,下面的都是同步代码块
                System.out.println(Thread.currentThread().getName() + "进入1");
                synchronized (o2) {
                    System.out.println(Thread.currentThread().getName() + "进入2");
                }
            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + "进入3");
                synchronized (o1) {
                    System.out.println(Thread.currentThread().getName() + "进入4");
                }
            }
        }
    }
}
  • 输出:( 始终卡着也不终止 )

    A进入1
    B进入3

释放锁分析

会释放

  • 当前线程的同步方法、同步代码块执行结束
    • 上厕所,上完出来
  • 当前线程的同步方法、同步代码块中遇到 break、return
    • 没完事,经理喊出来修改 bug,不得已出来
  • 当前线程的同步方法、同步代码块中出现了未处理的 Error 或者 Exception,导致异常结束
    • 没有正常完事,发现忘带纸了,不得已出来
  • 当前线程的同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁
    • 没有正常完事,觉得需要酝酿下,所以出来等会再进去

不会释放

  • 线程执行同步代码块或者同步方法时,程序调用 Thread.sleep ( 进入 TimeWaiting 状态 )、Thread.yield ( 切换为 Ready 状态 ) 方法暂停当前线程的执行,不会释放锁
    • 上厕所太困了,在坑位上睡了一会
  • 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起 ( 切换为 Ready 状态 ),该线程不会释放锁
    • 注意:应尽量避免使用 suspend() 和 resume() 来控制线程,方法不再推荐使用

练习

题一

  • 在 main 方法中启动两个线程,第一个线程循环随机打印 100 以内的整数,直到第二个线程从键盘读取了 " Q " 命令。
public class test {
    public static void main(String[] args) {
        AA a = new AA();
        BB b = new BB(a);
        b.start();
        a.start();
    }
}

// A不会继承别的类,所以 extends 也无妨
class AA extends Thread {
    private boolean loop = true;

    @Override
    public void run() {
        while (loop) {
//            输出0~100
            System.out.println((int)(Math.random() * 100 + 1));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("a线程退出");
    }
//    才能方便被别的线程以通知方式结束线程
    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

// 直到第二个线程从键盘读取了"Q"命令
class BB extends Thread {
    private AA a;
    private Scanner scanner = new Scanner(System.in);
    public BB(AA a) { // 直接在构造器中传入AA类对象
        this.a = a;
    }

    @Override
    public void run() {
        while (true) {
//        接收用户的输入
            System.out.println("请输入你的指令(注意要在输入后及时按下回车)--Q表示退出");
            char key = scanner.next().toUpperCase().charAt(0); // 卡住等待接收(toUpper转大写)
            if (key == 'Q') {
//            以通知的方式结束a线程
                a.setLoop(false);
                System.out.println("b线程退出");
                break;
            }
            System.out.println("输入非可接受的指令");
//            若输入不是 Q,就不会break退出,就会再提示输入
        }
    }
}
  • 例输出:

    请输入你的指令(注意要在输入后及时按下回车)--Q表示退出
    62
    65
    c
    输入非可接受的指令
    请输入你的指令(注意要在输入后及时按下回车)--Q表示退出
    90
    24
    q
    b线程退出
    a线程退出

题二

  • 有两个用户分别从同一个卡上取钱 ( 总额:10000 ),每次都取 1000,当余额不足时,就不能取款了,不能出现超取的现象 ( 即:线程同步问题,在取钱前加一个锁供争抢获取,Blocked 时抢到了锁的才能取钱 )
public class test1 {
    public static void main(String[] args) {
        T t = new T();
        Thread thread1 = new Thread(t); // t放进线程内
        thread1.setName("t1");
        Thread thread2 = new Thread(t);
        thread2.setName("t2");
        thread1.start();
        thread2.start();
    }
}

//  因为这里涉及到多个线程共享资源,所以我们使用实现 Runnable方式
//  每次取 1000
class T implements Runnable {
    private int money = 10000;

    @Override
    public void run() {
        while (true) {
//            解读这里使用 synchronized 实现了线程同步
//            当多个线程执行到这里时,就会去争夺this对象锁(前提是同一个对象t)
//            哪个线程争夺到(获取)this对象锁,哪个就执行 synchronized 代码块,执行完后,会释放this对象锁
//            争夺不到this对象锁的,就blocked,准备继续争夺
//            锁释放了,再一起同起跑线争夺(this:非公平锁,1抢到后可能下一个还是1抢到了)
            synchronized (this) {
//            判断余额是否够
                if (money < 1000) {
                    System.out.println("余额不足");
                    break;
                }
                money -= 1000;
                System.out.println(Thread.currentThread().getName() + "取出了1000 当前余额" + money);
            }
//            休眠1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 输出:

    t1取出了1000 当前余额9000
    t2取出了1000 当前余额8000
    ......
    t2取出了1000 当前余额1000
    t1取出了1000 当前余额0
    余额不足
    余额不足

    • 输出工整、不乱、不错