并发与多线程

4/8/2022 JavaSE

# 并发与并行

  1. **并发:同一时刻,多个任务交替执行,而交替的过程非常快,造成一种「貌似同时」**的错觉。简单滴说,单核 cpu 实现的多任务就是并发
  2. **并行:**同一时刻,多个任务同时执行。多核 cpu 可以实现并行

# 进程与线程

参考自己总结的操作系统学习笔记(操作系统.xml

  1. 进程
  2. 线程

# 在Java中创建线程的两种方式

在使用 Java 创建线程之前我们先来认识下两个非常重要的类型:**「Runnable 接口」**和 「Thread 类」

// Runnable 接口源码。可以知道 Runnable 接口中只有一个 run 抽象方法,超级简单
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

// Thread 类源码。可以看到 Thread 类中的 run 方法就是实现了 Runnable 接口中的 run 方法
class Thread implements Runnable{
    ...
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    ...
}
  1. 继承 Thread,重写 run 方法,在 run 方法中写我们的业务代码
  2. 实现 Runnable 接口,重写 run 方法,在 run 方法中写我们的业务代码

# 多线程案例

# 通过继承Thread类创建线程

package com.lkj.thread;

public class MultiThread {
    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat(); // 在 main 线程中创建一个 Cat 线程
        cat.start();// 启动 Cat 线程
        // Cat 线程)的执行并不会阻塞 main 线程(主线程)的继续执行
        for (int i = 0; i < 60; i++) {
            System.out.println("main 线程:" + i + ",main 线程的名字:" + Thread.currentThread().getName());
            Thread.sleep(1000);
        }
    }
}

class Cat extends Thread {
    int times = 0;// 计数器
    @Override
    public void run() {
        // 写我们的业务代码
        while (true) {
            System.out.println("我是一只小猫咪,我会喵喵喵" + (++times) + ",Cat 线程的名字:" + Thread.currentThread().getName());// 线程的名字为 Thread-0
            if (times == 80) break;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

思考一个问题:为什么不直接调用 run 方法,而是调用 start 方法?

当你直接调用 run 方法,相当于你在 main 线程中直接调用 run 方法,run 方法会作为 main 线程中的一个普通方法执行,此时你并没有启动 Cat 线程(虽然你创建了 Cat 线程),所以,程序会阻塞 run 方法后续代码的执行,直到 run 方法执行完毕,因为此时你只启动了一个线程,即 main 线程(主线程)

package com.lkj.thread;

public class MultiThread {
    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat(); // 在 main 线程中创建一个 Cat 线程
      	// 直接调用 run 方法,此时程序会等到 run 方法执行完毕后,才会继续往后执行
        cat.run(); 
        // Cat 线程)的执行并不会阻塞 main 线程(主线程)的继续执行
        for (int i = 0; i < 60; i++) {
            System.out.println("main 线程:" + i + ",main 线程的名字:" + Thread.currentThread().getName());
            Thread.sleep(1000);
        }
    }
}

class Cat extends Thread {
    int times = 0;// 计数器
    @Override
    public void run() {
        // 写我们的业务代码
        while (true) {
            System.out.println("我是一只小猫咪,我会喵喵喵" + (++times) + ",Cat 线程的名字:" + Thread.currentThread().getName());// 此时线程的名字就是 main 
            if (times == 80) break;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

而当你调用 start 方法时,我们来分析下它的源码,就明白为什么会启动一个线程了

// start 源码。源码中调用了 start0 方法
public synchronized void start() {
  ...
	start0(); // 启动线程的核心方法
  ...
}

// start0 是一个 native 方法,该方法会由 JVM 调用,底层由 c/c++ 实现,它可以直接与操作系统进行交互,告诉操作系统我想要启动一个线程,请给我分配相关的资源吧
// 也就是说实现多线程效果的是其实 start0,而不是 run 方法,在实现了多线程后,run 方法只是在某个子线程中执行的普通方法,不再是主线程的普通方法了
private native void start0();

要想完全理解 java 中的多线程编程,你必须对操作系统有一定的了解

# 通过实现Runnable接口创建线程

package com.lkj.thread;

public class MultiRunnable {
    public static void main(String[] args) {
        Dog dog = new Dog();// 创建 Dog 线程
        Thread thread = new Thread(dog);
        thread.start();
    }
}

class Dog implements Runnable {
    int times = 0;
    @Override
    public void run() {
        // 先把业务代码搞定
        while (true) {
            System.out.println("我是一只小狗,我会汪汪叫 " + (++times) + " 线程名字:" + Thread.currentThread().getName());
            if(times == 10) break;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

思考一个问题:为什么我创建的 Dog 实例传给 Thread 构造函数,然后再调用 start 呢?

其实上述的 Thread thread = new Thread(dog); Thread 源码内部应用了代理模式中的静态代理,将接收到的 dog 对象先保存到 Thread 类中的一个字段变量 target 中。当我们调用 Thread 的 start 方法时,start 方法内部会首先调用 start0 本地方法来启动一个线程,然后再通过 target 变量(dog 实例)调用我们写好的 run 方法。现在我们来简单地模拟一下该静态代理的具体实现

代理模式分为两种:「动态代理」「静态代理」

package com.lkj.thread;

// 此时的 StaticProxy 类似于 Thread 类
public class StaticProxy implements Runnable{

    private Runnable target;// 代理对象

    public StaticProxy(Runnable target) {
        this.target = target;
    }

    @Override
    public void run() {
        if(target != null){
            target.run();
        }
    }

    public void start(){
//        start01(); // 真正实现多线程的一个方法,是一个本地方法,由 JVM 调用
        run();
    }
}

// 此时的 Tiger 类类似于上面的自定义 Dog 类
class Tiger implements Runnable{
    @Override
    public void run() {
        // 开始写我们的业务代码
        System.out.println("我是一只老虎,我会凶人哦ð");
    }
}

class Test1{
    public static void main(String[] args) {
        Tiger tiger = new Tiger();
        StaticProxy staticProxy = new StaticProxy(tiger);
        staticProxy.start();
    }
}

# 总结

Java 中能够实现多线程的最根本的原因在于 Thread 类中有一个 start0本地方法,该本地方法会被 JVM 调用,JVM 底层会创建线程,而我们重写的 run 方法与普通方法并没什么不同,唯一有区别的就是该方法是在主线程中调用还是在子线程中调用

# 在main线程下创建两个子线程并启动它们

package com.lkj.thread;

public class MultiThreadCase {
    public static void main(String[] args) {
        T1 t1 = new T1();
        T2 t2 = new T2();
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);
        thread1.start();
        thread2.start();
        System.out.println("主线程执行完毕");
    }
}

// 线程 T1
class T1 implements Runnable {
    int times = 0;
    @Override
    public void run() {
        while(true) {
            System.out.println("我是一个子线程,我会说 hi " + (++times) + ",我叫" + Thread.currentThread().getName());
            if(times == 6) break;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 线程 T2
class T2 implements Runnable {
    int times = 0;
    @Override
    public void run() {
        while (true) {
            System.out.println("我是一个另一个子线程,我会说 hello " + (++times) + ",我叫" + Thread.currentThread().getName());
            if(times == 10) break;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

# 多线程售票案例

**题目:**现在有三个窗口正在同时售 100 张票,请根据你学的 java 线程的知识来实现这个售票系统

package com.lkj.ticket;

public class TicketCase {
    public static void main(String[] args) {
//        SellTicket sellTicket1 = new SellTicket();
//        SellTicket sellTicket2 = new SellTicket();
//        SellTicket sellTicket3 = new SellTicket();
//        sellTicket1.start();
//        sellTicket2.start();
//        sellTicket3.start();
        SellTicket01 sellTicket01 = new SellTicket01();
        Thread thread1 = new Thread(sellTicket01);
        Thread thread2 = new Thread(sellTicket01);
        Thread thread3 = new Thread(sellTicket01);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

/**
 * 通过继承 Thread 类的方式
 */
class SellTicket extends Thread{
    // 静态变量 ticketNum,代表当前票的总数,多个线程共享该数据
    private static int ticketNum = 100;
    @Override
    public void run() {
        while (true){
            if(ticketNum <= 0) {
                System.out.println("当前已无可售的票,剩下" + ticketNum + "张");
                break;
            }
            System.out.println("我是窗口" + Thread.currentThread().getName() + ",正在售票当中");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            --ticketNum;
            System.out.println("还剩下总票数:" + ticketNum + "张");
        }
    }
}

/**
 * 通过实现 Runnable 接口的方式
 */
class SellTicket01 implements Runnable{
    // 静态变量 ticketNum,代表当前票的总数,多个线程共享该数据
    private static int ticketNum = 100;
    @Override
    public void run() {
        while (true){
            if(ticketNum <= 0) {
                System.out.println("当前已无可售的票,剩下" + ticketNum + "张");
                break;
            }
            System.out.println("我是窗口" + Thread.currentThread().getName() + ",正在售票当中");
            try {
              	// 模拟售票的过程,每卖出一张票需要花费 50 毫秒
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            --ticketNum;
            System.out.println("还剩下总票数:" + ticketNum + "张");
        }
    }
}

# 线程的终止

线程终止的时刻

  1. 当线程完成任务后会自动退出
  2. 当线程在执行任务的过程中,我们可以通过控制变量的方式来决定线程的 run 方法是否继续执行进而来控制线程的终止
package com.lkj.thread;

public class ThreadExit {
    public static void main(String[] args) throws InterruptedException {
        ThreadExitCase threadExitCase = new ThreadExitCase();
        Thread thread = new Thread(threadExitCase);
        thread.start(); // 启动子线程
        Thread.sleep(10 * 1000);
        // 主线程休眠 10 秒后通过设置子线程中的 flag 变量来终止子线程
        // 其本质还是终止 run 方法的执行
        threadExitCase.setFlag(false);
    }
}

class ThreadExitCase implements Runnable {
    int num = 0;
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            System.out.println("我是一个线程,我叫" + Thread.currentThread().getName() + (++num));
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

# 线程的中断

注意⚠️:线程中断的意思并不是将线程杀死,当中断被捕获后会线程会继续运行

package com.lkj.thread;

public class ThreadInterrupt {
    public static void main(String[] args) throws InterruptedException {
        ThreadInterruptCase threadInterruptCase = new ThreadInterruptCase();
        threadInterruptCase.start();// 启动子线程
        threadInterruptCase.setName("lkj");// 设置子线程的名字
        Thread.sleep(5 * 1000);
        // 休眠 5 秒后中断子线程
        threadInterruptCase.interrupt();
    }
}

class ThreadInterruptCase extends Thread {
    int num = 0;

    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 10; i++) {
                System.out.println("我叫" + Thread.currentThread().getName() + ",我在吃包子,正在吃第" + (++num) + "个");
            }
            try {
                Thread.sleep(10 * 1000);
            } catch (InterruptedException e) {
                // 捕获到线程中断了,
                System.out.println(Thread.currentThread().getName() + "线程被中断了,中断之后继续吃包子");
//                e.printStackTrace();
            }
        }
    }
}

# 线程的插队

  1. yield:线程的礼让。让出 cpu 控制权,让其它线程先执行,但是礼让的时间并不确定,所以也不一定礼让成功
  2. join:线程的插队。插队的线程一旦插队成功,则一定先执行完插入的线程,然后再继续执行当前线程

通过一个案例来学习上述两个概念吧

package com.lkj.thread;

/**
 * 主线程和子线程分别每隔1秒吃一个包子,分别总共吃20个包子。
 * 但是此时有一个条件,就是当主线程吃完第五个包子时,让子线程吃完剩下的包子再主线程继续吃剩下的包子
 */
public class ThreadInsertion {
    public static void main(String[] args) throws InterruptedException {
        ThreadInsertionCase threadInsertionCase = new ThreadInsertionCase();
        Thread thread = new Thread(threadInsertionCase);
        thread.start();// 启动子线程
        for (int i = 1; i <= 20; i++) {
            System.out.println("主线程开始吃包子,正在吃第" + i + "个");
            if(i == 5) {
                System.out.println("主线程吃的有点饱,需要休息下,先让子线程把包子吃完我再吃剩下的包子吧");
                // 子线程开始插队,直到子线程将所有任务执行完毕后,主线程再继续执行剩下的任务
                thread.join(); 
                // 在主线程中调用 yield 方法,表示主线程开始礼让,让子线程开始执行,具体是否礼让成功不可知
                // Thread.yield();
            };
            Thread.sleep(1000);
        }
    }
}

class ThreadInsertionCase implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            System.out.println("子线程开始吃包子,正在吃第" + i + "个");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

# 两种线程

  1. 用户线程:也叫工作线程,当线程的任务执行完或以通知的方式结束
  2. 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束。常见的守护线程有垃圾回收机制
package com.lkj.thread;

public class ThreadDaemon {
    public static void main(String[] args) throws InterruptedException {
        ThreadDaemonCase threadDaemonCase = new ThreadDaemonCase();
        // 设置子线程为守护线程,此时主线程(main 线程)为用户线程
        // 当用户线程结束时,守护线程会自动结束
        threadDaemonCase.setDaemon(true);
        threadDaemonCase.start();// 启动子线程
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程在工作~~" + Thread.currentThread().getName());
            Thread.sleep(1000);
        }
        System.out.println("主线程结束了!!!");
    }
}

class ThreadDaemonCase extends Thread{
    @Override
    public void run() {
        while (true){
            System.out.println("子线程在工作~~~");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

# 线程的七种状态

注意⚠️:These states are virtual machine states which do not reflect any operating system thread states

翻译过来就是:这些状态是虚拟机状态,不反映任何操作系统线程状态。也就是上图所画的七种线程状态只是 Java 虚拟机所定义的线程状态,与操作系统中所定义的线程状态是不一样的。

package com.lkj.thread;

public class ThreadState {
    public static void main(String[] args) throws InterruptedException {
        ThreadStateCase threadStateCase = new ThreadStateCase();
        System.out.println("子线程的状态:" + threadStateCase.getState());
        threadStateCase.start();// 启动子线程
        System.out.println("子线程的状态:" + threadStateCase.getState());
        while (Thread.State.TERMINATED != threadStateCase.getState()) {
            System.out.println("子线程的状态:" + threadStateCase.getState());
            Thread.sleep(1000);
        }
        System.out.println("子线程的状态:" + threadStateCase.getState());
    }
}

class ThreadStateCase extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程在执行 " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

# 线程同步机制

# 同步机制的理解

在某一个时刻,多个线程访问同一个数据时,只能有一个线程进行访问,以保证数据被单一来源进行修改,这么做的目的是为了后续线程对资源的准确访问

# synchronized

在 Java 中进行同步的方式是使用 synchronized 关键字。synchronized 关键字可以应用于以下情况

  1. 同步代码块
synchronized (对象){// 得到对象的锁,才能操作同步代码
  // 此处的对象必须是同一对象或类本身
  // 当代码块在静态方法中时,该对象为类本身
  // 当代码块在非静态方法中时,该对象为同一个对象
  // 需要被同步的代码
}
  1. 放在方法声明中,以表示整个方法是同步方法
public synchronized void m(String name){
  // 需要被同步的代码
}

那这个锁到底加在哪里呢?

  1. 对静态方法加锁
// 此时的锁是加在当前类本身上
public static synchronized void m(String name){
  // 需要被同步的代码
}
  1. 对非静态方法加锁
// 此时的锁是加在当前类的对象身上
public synchronized void m(String name){
  // 需要被同步的代码
}
  1. 对代码块加锁
synchronized (对象){// 得到对象的锁,才能操作同步代码
  // 此处的对象必须是同一对象或类本身
  // 当代码块在静态方法中时,该对象为类本身
  // 当代码块在非静态方法中时,该对象为同一个对象
  
  // 需要被同步的代码
}

利用 synchronized 解决多线程售票问题

# 线程的死锁

由于两人都想获取对方的锁而又不愿先释放自己持有的锁,于是就会造成死锁的现象

package com.lkj.thread;

public class DeadLock {
    public static void main(String[] args) {
        Thread A = new DeadLockCase(true);
        Thread B = new DeadLockCase(false);
        A.start();
        B.start();
    }
}

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

    public DeadLockCase(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if(flag){
            synchronized (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");
                }
            }
        }
    }
}

# 释放锁

释放锁的情况

  1. 当前线程的同步方法、同步代码块运行结束,会释放锁

    案例:上厕所,完事出来

  2. 当前线程在同步方法、同步代码块中遇到 break、return

    案例:没有正常的完事,经理叫他出来修 bug,不得已出来

  3. 当前线程在同步方法、同步代码块中遇到了 Error 或 Exception,导致异常结束

    案例:没有正常的完事,发现忘记带纸,不得已出来

  4. 当前线程在同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁

    案例:没有正常完事,需要酝酿一下,所以先出来等会再进去

不释放锁的情况

  1. 线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行,不会释放锁

    案例:上厕所,太困了,在坑位上眯一会

  2. 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁

    提示:尽量避免使用 suspend() 或 resume() 来控制线程,方法不再推荐使用

Last Updated: 4/8/2022, 7:03:16 PM