My Blog

Multithreading

记录 JavaSE 中多线程的一些基本的概念和方法使用

学习笔记,仅供参考

参考B站Mirco_Frank - java 进阶 | 官网教程 | javatpoint


目录


📖 相关概念

  • 单核 : 当计算机只有一个核时,表面上它好像同时在运行多个程序,其实每个时刻它仅执行一个程序,其他的程序处于挂起(暂停),由于 CPU 的运算速度快,程序之间的转换快,导致看起来好像是同时进行的(一个人干多样活)

  • 多核 : 现在的计算机都是多核的,每个核执行一个程序,不用再来回切换,从而提高了效率(多个人干多样活)

  • 并发 : 单个 CPU 执行多个线程,相互争抢资源,来回切换(没钱,用多线程并发来提高效率)

  • 并行 : 多个 CPU 同时执行不同的线程,互补争抢 CPU 资源(有钱)

  • 进程 : 通常有一套完整、独有的基本运行时资源,而且它们有自己的内存空间

  • 线程 : 创建线程要比创建进程更省资源,线程寄生于进程,一个进程至少有一个线程,这些 线程共享进程的资源 ,从而保证了高效但也存在隐患

  • 主线程(main thread) : 通常指 main 方法,它有能力创建其他的线程

  • 多线程 : 一个进程的多个线程同时对该进程的某个共享变量进行操作

进程线程关系图


🧵 创建线程

创建线程有两种方式:继承 Thread 类 和 实现 Runnable 接口,先看继承 Thread 类

继承 Thread 类

先写继承类, 然后再 Test 类的 main 方法中来实例它,为了展现各线程来回抢占进程(程序)资源,用 while (true) 死循环

/**
 *  继承 Thread 类
 *  重写父类的 run() 方法 
 */
public class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("my thread running...");
        }
    }
}

/*************************分隔线****************************/

/**
 *  测试类
 *  将上面继承 Thread 的类实例化
 */

public calss Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        // 调用 start 方法,启动线程,让其变为 Runnable(可运行状态)
        myThread.start();

        while (true) {
            System.out.println("main thread running...")
        }
    }
}

结果图如下:

继承Thread类

实现 Runnable 接口

/**
 *  实现 Runnable 接口
 *  重写父类的 run() 方法 
 */
public class MyThread implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("my thread running...");
        }
    }
}

/*************************分隔线****************************/

/**
 *  测试类
 *  将上面实现 Runnable 接口的类实例化
 */
public calss Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        // 因为实现 Runnable 接口类的实例对象无法调用 start() 方法,所以要将它交给 Thread 类的实例对象
        Thread thread = new Thread(myThread);
        // 调用 start 方法,启动线程,让其变为 Runnable(可运行状态)
        thread.start();

        while (true) {
            System.out.println("main thread running...")
        }
    }
}

上面代码为了演示所以写的比较多余,因为实例出的对象没啥操作,所以可以进行 简化

// extends Thread class
(new myThread()).start();

// implements Runnable interface
(new Thread(new myThread())).start();

// implements Runnable by use lambda expression
// 用 λ 表达式简化匿名内部类,从而省去中间的实现类
(new Thread( () -> {
    while (true){
        System.out.println("my thread running...")
    }
} )).start();

从上面两种创建方式可知:

相同点 : 两种方式都要重写 run() 方法才行,启动线程时,也都是要调用 start() 方法

不同点 : 继承类的实例对象直接调用 start(),而实现类的实例对象要交给 Thread 对象才能调用 start()

执行图

【问题】线程为什么不直接调用 run() 方法,而是调用的 start() 方法? 【解答】由于多线程的执行是各不影响的,所以每个线程都会有一个独立的栈。而若在 main 中直接调用 run() 方法,那么所进的栈就是主线程当前的栈,这样就会影响到主线程的执行,就不是多线程了。通过调用 start() 则是分配到独立的堆栈,在新的栈中调用 run() 执行程序,即开启了新的线程

当创建线程时,还可以设置线程的名字

// 在 Thread 的有参构造器设置线程名
new Thread(myThread, "mythread");

// 获取当前线程名
String threadName = Thread.currentThread().getName();

👟 多线程抢鞋

在如今互联网发达的时代,多线程的使用就在我们日常生活中。比如:双十一的淘宝抢购、火车站买票等,就连公共厕所的占用也可以用多线程解释。它主要的特点就是多个线程对共享资源的抢占,下面就以抢鞋为例。

/**
 *  实现 Runnable 类
 *  定义鞋的数量以及鞋被抢后打印信息
 */
public class ShoesThread implements Runnale {
    private int shoes = 10;  // 假定有十双鞋

    @Override
    public void run() {
        while (shoes > 0) {
            // 显示谁抢到第几双鞋
            System.out.println(Thread.currentThread().getName() + "抢到第" + (shoes--) + "双鞋!!!");
        }
    }
}

/**************************分隔线***************************/

/**
 *  模拟三个用户抢鞋
 */
public calss Test {
    public static void main(String[] args) {
        // 实例化 ShoesThread
        ShoesThread shoesThread = new ShoesThread();
        // 创建并开启三个用户线程
        new Thread(shoesThread, "Tom").start();
        new Thread(shoesThread, "Jerry").start();
        new Thread(shoesThread, "Frank").start();
    }
}

运行结果图如下:

抢鞋

当然,用继承 Thread 类也能实现这种抢夺公共资源的情形,只需将 implements Runnable 改为 extends Thread 即可

注意与下面的例子区别开来

public static void main(String[] args) {
    // 这样写就是三家鞋店,一人抢一家,各抢十双
    // 这样就不是三个线程抢占共享资源了
    new Thread(new ShoesThread(), "Tom").start();
    new Thread(new ShoesThread(), "Jerry").start();
    new Thread(new ShoesThread(), "Frank").start();
    
}

注意

继承 Thread 类与实现 Runnable 接口在用法上没啥区别,但由于 Java 只支持单继承,所以继承 Thread 类的方式就会有大大局限,不能再继承其他的类;而实现 Runnable 接口的类则还有一个继承其他类的机会,让其更加的灵活。所以一般更多的是使用实现 Runnable 接口的方式来创建线程

守护线程

有前台抢鞋的线程,那就会有后台管理更新数据的线程,即 守护线程

// 1. 实现 Runnable 接口创建一个线程
Thread daemonThread = new Thread( () -> {
    System.out.println("daemon thread is running...");
} );

// 2. 将创建的线程设置为守护线程
daemonThread.setDaemon(true);

// 3. 开启守护线程
daemonThread.start();

// !注意:守护线程要写在其他线程的前面

daemon-thread


🔒 线程同步

为了让抢鞋的情形更加贴近现实,不让鞋一瞬间就被抢完,每被抢一双就延时一段时间。通过 Thread.sleep() 方法来让当前的线程睡眠一段时间,以此达到延时的效果

/**
 *  使用 sleep() 方法延时抢鞋
 */
public class ShoesThread implements Runnale {
    private int shoes = 10;  // 假定有十双鞋

    @Override
    public void run() {
        while (shoes > 0) {
            // 使用 sleep() 时,要捕获 InterruptedException
            // 当该线程运行中被中断时抛出
            try {
                Thread.sleep(500);  // 线程睡眠0.5秒
            } catch (InterruptedException e) {
                return;
            }

            // 显示谁抢到第几双鞋
            System.out.println(Thread.currentThread().getName() + "抢到第" + (shoes--) + "双鞋!!!");
        }
    }
}

但是,加上延时后程序的运行就出现问题

线程不安全

这就表明线程是 非安全、不同步 的,下面就介绍三种方法使得线程同步

锁对象

先创建一个 Object 类的对象 lock,然后交给 synchronized(lock) {...} (花括号叫同步代码块),让同步代码块包住 while 的内容即可

/**
 *  锁对象同步
 *  可能由于 CPU 的运算速度太快,导致只有一个线程在执行
 *  所以将鞋数改为 50,while 改为死循环,用 if 语句做判断
 */
private int shoes = 50;
Object lock = new Object();

@Override
public void run() {
    while (true) {
        synchronized(lock) {
            if (shoes > 0) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + "抢到第" + (shoes--) + "双鞋!!!");
            }
        }
    }
}

经过几次运行,得到了三个线程同步共享变量的情况

线程同步

同步方法

与锁对象类似,只不过不用新建 Object 对象,将同步代码块的内容直接拿出去作为一个方法来直接调用。与锁对象相比,则 更推荐同步方法

private int shoes = 50;

@Override
public void run() {
    while (true) {
        shoesCatch();
    }
}

/**********************分隔线**************************/
/**
 *  同步方法
 */
public synchronized void shoesCatch() {
    if (shoes > 0) {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "抢到第" + (shoes--) + "双鞋!!!");
    }
}

同步锁

除了锁对象和同步方法外,还有使用同步锁的方法来达到线程同步,它需要先提前 new Lock 类的对象,然后使用它的 lock()unlock() 方法包住要同步的内容

/**
 *  同步锁
 */
private int shoes = 50;
Lock reentrantLock = new ReentrantLock();  // 新建同步锁对象


@Override
public void run() {
    while (true) {
        // 上锁
        reentrantLock.lock();
        if (shoes > 0) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "抢到第" + (shoes--) + "双鞋!!!");
        }
        // 解锁
        reentrantLock.unlock();
    }
}

区别

  • synchronized 是关键字;Lock 是接口

  • synchronized 自动获取锁和释放锁;Lock 则需要手动的去获取锁和释放锁

  • synchronized 无法判断锁的状态;Lock 则能判断锁的状态

  • synchronized 同步时,若一个线程阻塞,其他线程就会一直等待;Lock 同步时,一个线程阻塞,其他线程可以中断等待而做其他的事

  • synchronized 适合少量代码的同步;Lock 则适合大量代码的同步

更多的区别参考:

Lock与synchronized的区别? | Java同步锁-synchronized与lock | JUC并发编程—Synchronized锁和 Lock 锁


🎤 Thread 类

下面仅列举学习时所用或所能理解的方法,更详尽的参考API DOC

字段

  • MAX_PRIORITY   // 线程能达到的最大优先级

  • MIN_PRIORITY   // 线程的最小优先级

  • NORM_PRIORITY   // 线程默认的优先级

线程调度 : 每个线程的优先级都是由系统随机分配的,但也可以通过 setPriority() 方法来设置线程的优先级别。线程的优先级范围为 1 - 10,一般主线程 main 的优先级为 5。在 WIN10 上通过代码验证,大体上看优先级高的会先执行,但可能由于 CPU 处理太快或者操作系统对优先级的支持不同,还是不太理想,有交叉执行的情况

构造器

  • Thread()   // 无参构造器,生成一个线程对象

  • Thread(Runnable target)   // 接收 Runnable 的实现类对象

  • Thread(Runnable target, String name)   // 接收 Runnable 的实现类对象,并设置该线程的名字

静态方法

  • currentThread()   // 返回当前执行线程对象

  • interrupted()   // 判断当前线程是否中断,中断返回 true;否则 false

  • sleep​(long millis)   // 让当前的线程睡 millis 毫秒

  • yield()   // 线程让步,向调度程序提示当前线程愿意放弃其当前对 CPU 的使用

实例方法

  • start()   // 开启线程

  • getId() | getName() | getPriority()   // 获取该线程的 id | 线程名 | 优先级别

  • setDaemon(boolean on) | setName(String name) | setPriority(int newPriority)   // 设置该线程为守护线程 | 线程名 | 优先级别

  • interrupt()   // 中断该线程,中断机制参考这里官网教程

  • isAlive()   // 判断该线程是否还存活

  • isDaemon()   // 判断该线程是否为守护线程

  • isInterrupted()   // 判断该线程是否被中断

  • join()   // 线程插队,被插的线程要等该线程执行完才能继续;重载可以设置等待多少毫秒


线程状态(生命周期)

线程通常有六种状态,如下所示。可以查看 API 中的 java.lang.Thread.State

  • New   // 新建状态,使用 new 关键字创建了线程,但还没开启

  • Runnable   // 可运行状态,调用 start() 后,线程开始在 JVM 中执行;线程调度提供固定的时间切片给每个线程去运行(即 Running),当时间切片结束后,放弃 CPU 资源给其他线程,等待下一轮时间切片

  • Block   // 阻塞状态,线程被阻塞,等待监视器锁

  • Waiting   // 等待状态,线程等待另外的线程执行特别的行为

  • Timed Waiting   // 计时等待状态,线程等待另外的线程执行一个动作,直到指定的时间结束为止,防止线程死等

  • Terminated   // 终止状态,线程正常完成所给任务或以意外事件而终止,如 segmentation fault or unhandle exception

ThreadState

可以通过联想斗地主的场景来大致理解这些线程状态,三人进入房间准备就绪(创建了三个线程),都准备好了开始游戏(三个线程开启),抢地主谁抢到谁先出牌(三个线程阻塞,抢同步锁,谁抢到谁执行),地主出牌时👲农民👨‍🌾都在等着不能打牌(抢到锁的开始执行,其他线程等待),要是在规定的出牌时间未出牌就会轮到下家(另外两个线程计时等待,等地主出完牌或时间到),于是三人开始了愉快的打牌(三个线程一直在阻塞抢锁、执行、等待),等某人打完牌结束游戏(三个线程终止)

public class MultithreadingTest {
    // 声明两个线程,声明为 static 方便重写 run() 时好直接用
    public static Thread thread1;
    public static Thread thread2;

    public static void main(String[] args) {
        /**
         *  新建 thread1
         */ 
        thread1 = new Thread(() -> {
            // 先让线程1打印 50 次
            for (int i = 0; i < 50; i++) {
               System.out.println(Thread.currentThread().getName() + " is running...");
               // 在打印第四次时让线程2插队
               if (i == 3) {
                   try {
                       thread2.join();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            }
            
            // 让线程1睡会
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread1");

        /**
         *  新建 thread2
         */
        thread2 = new Thread( () -> {
            // 让线程2打印10,并且每次都看下线程1在被插队后的状态
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " is running...");
                System.out.println("State of thread1 : " + thread1.getState());
            }
        } , "thread2");

        // 查看新建线程1后的状态
        System.out.println("State of thread1 : " + thread1.getState());
        thread1.start();  // 开启两个线程
        thread2.start();
        // 查看开启线程1后的状态
        System.out.println("State of thread1 : " + thread1.getState());

        // 让主线程一直追踪线程1状态,给短暂的间隔防止主线程过多抢占
        while (true) {
            System.out.println("State of thread1 : " + thread1.getState());
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

程序可能还不完善,但好在有时能较好地显示出线程1的六个状态,如下图所示

ThreadStates

🚧 wait 和 notify

wait() 和 notify() 方法是继承自 Object 类,当线程中某对象调用 wait() 时,该线程暂时停止执行,一直等待,直到其他的线程在同一对象上调用 notify() 方法来唤醒它。所以这些线程要有共同的对象,这样才能与其他的线程完成通信。下面以 生产者-消费者 的例子来演示

/**
 *  产品类
 */
public class Condom {
    // true 产品还有,没卖完
    public boolean isStatus = true;
}

/**
 *  生产者类
 */
public class Producer extends Thread {
    // 内部接收产品对象
    private Condom condom;
    public Producer(Condom condom) {
        this.condom = condom;
    }

    while (true) {
        // 将接收到的对象作为锁对象
        synchronized (condom) {
            if (condom.isStatus == true) {
                // 商品还有,生产者等待
                System.out.println("货还有,快点买,我等你");
                try {
                    condom.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 若商品没了,上满货,通知消费者购买
            condom.isStatus = true;
            System.out.println("快点买货,通知你了");
            condom.notify();    
        }
    }
}

/**
 *  消费者类
 */
public class Customer extends Thread {
    // 内部接收产品对象
    private Condom condom;
    public Customer(Condom condom) {
        this.condom = condom;
    }

    while (true) {
        // 将接收到的对象作为锁对象
        synchronized (condom) {
            // 商品没了,消费者等待
            if (condom.isStatus == false) {
                System.out.println("货卖完了,快点生产,我等你");
                try {
                    condom.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 若商品还有,买完货,通知生产者生产
            condom.isStatus = false;
            System.out.println("快点上货,通知你了");
            condom.notify();    
        }
    }
}

/**
 *  测试类
 */
public class Test {
    // 新建一个商品对象
    Condom condom = new Condom();
    // 让两线程共同调用该商品
    new Thread(new Producer(condom), "厂家").start();
    new Thread(new Customer(condom), "买家").start();
}

Producer_Customer