Multithreading
记录 JavaSE 中多线程的一些基本的概念和方法使用
学习笔记,仅供参考
目录
📖 相关概念
单核 : 当计算机只有一个核时,表面上它好像同时在运行多个程序,其实每个时刻它仅执行一个程序,其他的程序处于挂起(暂停),由于 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...")
}
}
}
结果图如下:
实现 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();
// !注意:守护线程要写在其他线程的前面
🔒 线程同步
为了让抢鞋的情形更加贴近现实,不让鞋一瞬间就被抢完,每被抢一双就延时一段时间。通过 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;否则 falsesleep(long millis)
// 让当前的线程睡 millis 毫秒yield()
// 线程让步,向调度程序提示当前线程愿意放弃其当前对 CPU 的使用
实例方法
start()
// 开启线程getId() | getName() | getPriority()
// 获取该线程的 id | 线程名 | 优先级别setDaemon(boolean on) | setName(String name) | setPriority(int newPriority)
// 设置该线程为守护线程 | 线程名 | 优先级别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
可以通过联想斗地主的场景来大致理解这些线程状态,三人进入房间准备就绪(创建了三个线程),都准备好了开始游戏(三个线程开启),抢地主谁抢到谁先出牌(三个线程阻塞,抢同步锁,谁抢到谁执行),地主出牌时👲农民👨🌾都在等着不能打牌(抢到锁的开始执行,其他线程等待),要是在规定的出牌时间未出牌就会轮到下家(另外两个线程计时等待,等地主出完牌或时间到),于是三人开始了愉快的打牌(三个线程一直在阻塞抢锁、执行、等待),等某人打完牌结束游戏(三个线程终止)
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的六个状态,如下图所示
🚧 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();
}