并发与多线程
# 并发与并行
- **并发:同一时刻,多个任务交替执行,而交替的过程非常快,造成一种「貌似同时」**的错觉。简单滴说,单核 cpu 实现的多任务就是并发
- **并行:**同一时刻,多个任务同时执行。多核 cpu 可以实现并行
# 进程与线程
参考自己总结的操作系统学习笔记(操作系统.xml
)
- 进程
- 线程
# 在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();
}
}
...
}
- 继承 Thread,重写 run 方法,在 run 方法中写我们的业务代码
- 实现 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 + "张");
}
}
}
# 线程的终止
线程终止的时刻
- 当线程完成任务后会自动退出
- 当线程在执行任务的过程中,我们可以通过控制变量的方式来决定线程的 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();
}
}
}
}
# 线程的插队
- yield:线程的礼让。让出 cpu 控制权,让其它线程先执行,但是礼让的时间并不确定,所以也不一定礼让成功
- 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();
}
}
}
}
# 两种线程
- 用户线程:也叫工作线程,当线程的任务执行完或以通知的方式结束
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束。常见的守护线程有垃圾回收机制
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 关键字可以应用于以下情况
- 同步代码块
synchronized (对象){// 得到对象的锁,才能操作同步代码
// 此处的对象必须是同一对象或类本身
// 当代码块在静态方法中时,该对象为类本身
// 当代码块在非静态方法中时,该对象为同一个对象
// 需要被同步的代码
}
- 放在方法声明中,以表示整个方法是同步方法
public synchronized void m(String name){
// 需要被同步的代码
}
那这个锁到底加在哪里呢?
- 对静态方法加锁
// 此时的锁是加在当前类本身上
public static synchronized void m(String name){
// 需要被同步的代码
}
- 对非静态方法加锁
// 此时的锁是加在当前类的对象身上
public synchronized void m(String name){
// 需要被同步的代码
}
- 对代码块加锁
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");
}
}
}
}
}
# 释放锁
释放锁的情况
当前线程的同步方法、同步代码块运行结束,会释放锁
案例:上厕所,完事出来
当前线程在同步方法、同步代码块中遇到 break、return
案例:没有正常的完事,经理叫他出来修 bug,不得已出来
当前线程在同步方法、同步代码块中遇到了 Error 或 Exception,导致异常结束
案例:没有正常的完事,发现忘记带纸,不得已出来
当前线程在同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁
案例:没有正常完事,需要酝酿一下,所以先出来等会再进去
不释放锁的情况
线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行,不会释放锁
案例:上厕所,太困了,在坑位上眯一会
线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁
提示:尽量避免使用 suspend() 或 resume() 来控制线程,方法不再推荐使用