多线程——初阶
创始人
2025-06-01 02:00:39
0
  1. 认识线程

1.1概念

1.1.1线程是什么

一个线程就是一个“执行流”。每个线程之间都可以按照顺序执行自己得代码。多个线程之间“同时”执行着多分代码。

【通俗的例子】一家公司去银行办理业务,既要进行财务转账,又要进行福利的发放,还要进行社保的缴纳。但是本公司只有一个会计张三,他每回只能排一个窗口,办理一个业务。为了更快的办理业务,张三可以叫上两个同事(新增两个线程),一起办理不同的业务,同时排3个窗口。此时,我们就把这种情况称为多线程。将一个大任务,分解成不同的小任务,交给不同的执行流就分别排队执行。其他另外两个同事是张三叫来的。所以张三一般被称为主线程。

1.1.2为什么要有线程

首先,“并发编程”是“刚需”

  • 在单核CPU的发展遇到了瓶颈,想要提高算力,就需要多核CPU,而并发编程能更充分利用多核CPU资源。

  • 有些任务场景需要“等待IO”,为了让等待IO的时间能够去做一些其他的任务,也需要并发编程。

其次,虽然多进程也能实现并发编程,但是线程比进程更加轻量化。

最后,线程虽然比进程轻量,但是人们还不满足,于是又有了“线程池”(ThreadPool)和“协程”(Coroutine)(以后讲解)

1.1.3进程和线程的区别

  • 线程是包含线程的。每个进程至少有一个线程存在,即主线程。

  • 进程和进程之间不共享内存空间。同一进程的线程之间共享同一内存空间。

【通俗的例子】比如上述的例子中,每个公司可以看成进程,每个公司的票据是不共享的。但是每个公司中的每个会计他们是共享票据的。
  • 进程是资源分配的最小单位,线程是系统调度的最小单位。

1.1.4Java的线程和操作系统的线程

线程是操作系统中的基本概念。操作系统内核实现了线程这样的机制,并且对用户提供了一些API供用户使用。

Java标准库中Thread类可以视为对操作系统提供的API进行进一步的抽象和封装。

1.2创建线程

1.2.1继承Thread类

继承Thread来创建一个线程类。

【方法一】直接继承Thread类

class MyThread extends Thread {@Overridepublic void run() {while(true) {System.out.println("hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class Test {public static void main(String[] args) {Thread t = new MyThread();//不会创建一个新的线程,而是在main线程中运行//t.run();//会创建一个新的线程t.start();while(true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

【方法二】使用匿名内部类继承Thread

public static void main(String[] args) {Thread t = new Thread() {@Overridepublic void run() {while(true) {System.out.println("hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};t.start();while(true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}

【其他技巧】我们如果要观察线程,我们可以打开C:\Program Files\Java\jdk1.8.0_192\bin下面的jconsoles中查看(我们必须启动main线程,必要时以管理员身份运行)

1.2.2实现Runnable接口

【方法一】直接实现Runnable接口

class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("hello Runnable");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}
}
public class TestDome2 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start();while(true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

【方法二】使用匿名内部类实现Runnable接口

public static void main(String[] args) {Thread t = new Thread(new MyRunnable() {@Overridepublic void run() {while(true) {System.out.println("hello Runnable");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}});t.start();}

【方法三】使用lambda表达式(重点)(最常用)

使用lambda表达式的条件:

  • 必须是接口;

  • 接口中只有一种抽象方法。

public static void main(String[] args) {Thread t = new Thread(() -> {while(true) {System.out.println("hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();}

2.Thread类及常用的方法

2.1Thread的常见的构造方法

方法

说明

Thread()

创建线程对象

Thread(Runnable target)

使用Runnable对象创建线程对象

Thread(String name)

创建线程对象,并命名

Thread(Runnable target, String name)

使用Runnable对象创建线程对象,并命名

我们已经讲解了如何创建一个新的线程,现在可以自己试一下给线程命名。

2.2Thread的几个常见的属性

属性

获取方法

ID

getId()

名称

getName()

状态

getState()

优先级

getPriority()

是否后台线程

isDaemon()

是否存活

isAlive()

是否被中断

isInterrupted()

  • ID是线程的唯一标识,不同的线程不会重复

  • 名称是各种调试工具用到的

  • 状态标识线程当前所处的一个情况。

  • 优先级高的线程理论上更容易被调用,但是作用不大

  • JVM会在一个进程的所有非后台线程结束后,才会结束

  • 是否存活,可以简单理解为,为run()方法是否执行结束

  • 线程中断问题

public class Test {public static void main(String[] args) {Thread thread = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "新线程活着");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("新线程即将死去");}, "新线程");System.out.println(Thread.currentThread().getName() + "ID: " + Thread.currentThread().getId());System.out.println(Thread.currentThread().getName() + "状态: " + Thread.currentThread().getState());System.out.println(Thread.currentThread().getName() + "优先级: " + Thread.currentThread().getPriority());System.out.println(Thread.currentThread().getName() + "后台线程: " + Thread.currentThread().isDaemon());System.out.println(Thread.currentThread().getName() + "活着: " + Thread.currentThread().isAlive());System.out.println(Thread.currentThread().getName() + "被中断: " + Thread.currentThread().isInterrupted());thread.start();while(thread.isAlive()) {}System.out.println(Thread.currentThread().getName() + "状态: " + thread.getState());}
}

【注意】currentThread()可以获取本线程的对象。

2.3启动一个线程——start()

我们看到如何通过覆写run()方法创建一个线程对象,但线程对象被创建并不一意味着线程就开始与运行。

  • 覆写run()方法是提供给线程要做的事情的指令清单;

  • 线程对象可以简单理解为了更快解决很多的业务,多叫了几个人来做不同的事情。

  • 调用start()方法,表示启动这个新的线程。

【注意】调用start()方法,才真正的在操作系统的底层创建出来一个线程

2.4中断一个线程

会计张三的同事李四(新的线程)一旦进入工作状态,他就会按照一定的步骤去完成相应的工作,不完成是不会结束的。但是有时候我们要增加一些机制,例如老板来电话说对方是一个骗子,我们要停止转账,该如何通知李四呢?

【方式】

  • 通过共享的标记进行沟通;

  • 调用interrupt()方法来通知。

【示例一】使用自定义的变量作为标志位。

public class TestDome2 {public static boolean isQuit = false;//示例一public static void main(String[] args) {Thread t = new Thread(() -> {while(!isQuit) {System.out.println("hello t");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}isQuit = true;}
}

【易错点】

  1. isQuit变量必须是静态成员变量,在main方法中定义isQuit是错误的。lambda表达式在捕获外面的局部变量的时候,要遵守变量捕获规则,捕捉的变量必须是final或“实际final"(变量没有被final修饰,但是代码中并没有做出修改。但是静态成员变量不受影响。

  1. 新的线程每次打印需要1s,main线程在启动t线程后,基本运行5s,但是着并不意味着t能打印5次。因为线程也是并行关系,这就导致调用每个线程是随机的,这可能导致main线程休眠了5s,但是t线程只运行了3,4次(概率很低,不代表没有可能)

【示例二】使用Thread.interrupted()或者Thread.currentThread().interrupted()代替自定义标志位。

Thread内部包含了一个boolean类型的变量作为线程是否被中断的标志

方法

说明

public void interrupt()

中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,并设置标志位。

public static boolean interrupted()

判断当前的中断标志位是否被设置,调用后清除标志位

public boolean isInterrupted()

判断对象关联的线程的标志位是否设置,调用后不清楚标志位

  • 使用thread对象的interrupted()方法来通知线程结束

public class TestDome3 {public static void main(String[] args) {Thread t = new Thread(() -> {while(!Thread.interrupted()) {System.out.println("hello t");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();break;}}});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}t.interrupt();}
}

thread收到通知的方式有两种:

  1. 如果线程因为wait/join/sleep()等方法而阻塞挂起,会引发interruptedException异常,同时又会清空标志位。

【解释】interrupt()方法会设置标志位为true,并唤醒wait()/jion()/sleep(),此时会抛出interrupted异常,这回清空标志位,改为true。如果catch中不写bread;这会导致抛出异常后,继续自行打印程序
  1. interrupt()是一个中断请求,只是设置标志位,如果休眠唤醒线程。并不是强行终止线程。thread可以通过

  • Thread.interrupted()判断当前线程的中断标志,使用后会清除中断标志位。

  • Thread.currentThread().isInterrupted()判断指定线程的中断标志,不会清除中断标志

【示例三】观察标志位是否被清除

  • Thread.interrupted()判断当前线程的中断状态,如果是true,将会在返回相关值后,自动变为false;

  • Thread.currentThread().isInterrupted()判断当前的中断状态,如果标志位被设置,在以后的返回结果中都为true;

public static void main(String[] args) {Thread t = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println(Thread.interrupted());}});t.start();t.interrupt();}

如果中断标志位为true,在返回这个boolean值后,将会自动清空标志位

public static void main(String[] args) {Thread t = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().isInterrupted());}});t.start();t.interrupt();}

并不会清空标志位,如果修改为true将不会被自动清空。

【注意】如果线程因为wait/join/sleep()等方法而阻塞挂起,会引发interruptedException异常,同时又会清空标志位。(无论是interrupted(),还是isInterrupted(),都会清空标志位)

2.5等待一个线程

有时,我们需要等待一个线程完成他的工作后,才能进行自己的下一步工作。这时我们就需要一个方法明确等待某线程的结束。

【提示】只有在我们真正需要某一线程结果时,我们选择等待某一线程的结束。不能无理由的使用,多线程的目的是,多个线程同时运行,提高运行效率。如果相互等待,就会违背初心,降低了效率。
public class TestDome5 {public static void main(String[] args) {Thread t = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println("hello t");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();try {t.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("hello main");}
}

【解析】在某线程中使用t.join()会导致某线程等待t线程结束后,此线程才继续运行。

方法

说明

public void join()

等待线程,直到线程结束;

public void jion(long millis)

等待线程结束,最多等待millis毫秒;

public void join(long millis, int nanos)

同理,但是可以更到精度。

2.6获取当前线程引用

获取当前线程的引用(上面已经用过)

public static Thread currentThread();

2.7休眠当前线程

因为线程的调度是随机的,这些方法只能保证实际休眠时间是大于等于参数设置的休眠时间

方法

说明

public static void sleep(long millis) throws InterruptedException

休眠当前线程millis毫秒

public static void sleep(long millis, int nanos) throws InterruptedException

可以使精度更高

3.线程的状态

3.1观察线程的所有状态

线程的状态是一个枚举类型Thread.State

  • new:创建了新的线程,但还没有调用start()方法;

  • runnable:线程准备就行可以随时运行和运行中统称为runnable;

  • blocked:线程阻塞于锁;

  • waiting:进入该状态的线程需要等待其他线程做出一些特定操作;

  • timed_wating:与waiting不同的是,超出指定的时间后将继续执行。

  • terminated:线程已经结束

public class TestDome6 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while(!Thread.interrupted()) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();break;}}});//newSystem.out.println(t.getState());t.start();Thread.sleep(10);//timed_waitingSystem.out.println(t.getState());t.interrupt();Thread.sleep(100);//terminatedSystem.out.println(t.getState());}
}

4.线程安全(重点)

4.1观察线程不安全

public class TestDome7 {public int count = 0;public void add() {count++;}public static void main(String[] args) throws InterruptedException {TestDome7 test = new TestDome7();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {test.add();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {test.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(test.count);}
}

我们预测的结果是100000,但是实际的结果却不是100000。并且每次运行的结果并不一样。

4.2线程安全的概念

线程安全的确切定义是复杂的,但我们可以这样理解:

如果多线程环境下运行的结果是符合我们的预期,即在单线程的环境下相同的结果,则说这个线程是安全的

4.3线程不安全的原因

4.3.1修改共享数据

上面的线程不安全的代码中,涉及到多个线程针对count变量的修改,此时这个count是一个多线程都能访问到的“共享数据"

【解析】

count++;此代码其实可以分为3步:

第一步load,即从内存中读取数据;

第二步add,即count++;

第三步save,将数据进行存储。

但是两个线程针对统一数据进行修改,这3个步骤有很多种的排列方式

第一种情况和第二种情况都是没问题,但是第三种情况却出现了问题

我们发现有一次相加是无效操作。这样的情况有很多,比如:

所以结果也不一定是50000~100000。

【解决方法】请查看5.synchronized关键字—监视器锁monitor lock

原子性

什么是原子性?可以简单理解为不可分割的整体。上述出现线程不安全的问题是count++看似是一条语句,但实际上不是一个指令,不同的指令之间相会穿插,导致没有原子性。我们要解决这样的问题,也是使用synchronized

4.3.2可见性

可见性指,一个线程对共享变量值的修改,能够及时提供给其他线程看见。

【代码示例】

public class TestDome8 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while(count == 0) {}});t.start();Thread.sleep(10);count = 1;System.out.println("此时线程结束");}
}

代码理应结束的,但是一直不结束。

【解析】

Java内存模型(JMM):Java虚拟机规范中定义了Java内存模型。目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。

  • 线程之间的共享变量存储在 主内存;

  • 每个线程都有自己的 “工作内存”;

  • 当线程要读取一个共享变量的时候,会把变量从内存拷贝到工作内存,再从工作内存读取数据;

  • 当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,再同步会主内存。

由于每个线程都有自己的工作内存,这些工作内存中的内容相当于同一个共享变量“副本”。此时修改线程1的工作内存中的值,线程2的工作内存不一定会及时改变。

1)初始情况下, 两个线程的工作内容一致。

2)一旦线程2修改了count的值,有时“工作内存”不一定能及时同步(我们这里是循环空语句(线程1),导致程序执行很快,为了优化,只有第一次读取的是主内存的值,其他情况下,我们读取的都是寄存器上的值,主内存上的count值被修改,导致线程1还是无法停止下来)

此时引入了两个问题:

  • 为什么有这么多不同的内存?

  • 为什么要这么麻烦的拷贝来,拷贝去?

  1. 为什么整这么多内存?

实际上没有这么多“内存”。这只是Java规范中的一个术语,属于“抽象”的叫法。

所谓“主内存”是真正硬件角度的“内存”。而所谓的“工作内存”,则是指CPU的寄存器和高速缓存。

  1. 为什么这么麻烦的拷来拷去?

因为CPU访问自身寄存器的速度以及高速缓冲区的速度,远远超过了访问内存的速度(快了3-4个数量级)

【解决方法】请看6.volatile

代码顺序性:一个代码是这样的(示例不好列举)(简单说明):

线程1:Student s = new Student();
线程2:while(s != null) {
s.learn();
}

线程1看似是一条指令,但是可以大致分为3部分:

  • 申请内存空间;

  • 调用构造方法(初始化内存空间)

  • 把对象的引用赋值给s(内存地址的赋值)

如果进行指令重排序,在进行了线程1的第一步和第二部,然后执行线程2会导致循环体命令不执行。

【解决方法】请看6.volatile

5.synchronized关键字—监视器锁monitor lock

5.1synchronized的特性

synchronized会起到互斥的效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一对象synchronized就会阻塞等待

  • 进入synchronized修饰的代码块,相当于加锁;

  • 退出synchronized修饰的代码块,相当于解锁

synchronized用的锁是存在java对象头里面的。(这就意味着()内只能是object对象,不能是内置类型,转化为对应的类也不行)

可以简单的理解为,每个对象在内存中储存的时候,都有一块内存表示当前的“锁定”状态(类似于厕所的“有人/无人”
如果当前是“无人”状态,那么就可以使用,使用时需要设置为“有人”状态;
如果当前是“有人”转台,那么只有等里面的人解锁后才能使用。
理解“阻塞等待”:
针对每一把锁,操作系统内部都能维护了一个等待队列,当这个锁被某个线程占有的时候,其他线程尝试加锁,就加不上了,就会阻塞等待,一直等到之前的线程之后,由操作系统唤醒一个新的线程,再来获取到这个锁。
【注意】
1. 上一个线程解锁后,下一个线程不是立即获得锁,而是需要系统唤醒;
2. 假设有ABC三个线程,A先获得线程,B尝试获取锁,然后C尝试获取锁,但是BC都没有成功,在阻塞队列中排队等待。A释放锁后,B虽然比C先请求获取锁,但是系统调度的时候,并没有先后顺序。

5.2synchronized的使用

synchronized本质上要修改对象的“对象头”。从使用角度来看synchronized也势必要搭配一个具体的对象来使用。

【方法一】直接修饰普通方法

public class class_name {public synchronized void method(){}
}

【方法二】修饰静态方法

public class class_name {public synchronized static void method(){}
}

【方法三】修饰代码块

锁定当前的对象

public class class_name {public void method() {synchronized(this) {}}
}

锁定类对象

public class class_name {public void method() {synchronized(class_name.class) {}}
}

【重点】两个线程或多个线程之间,如果是不安全的(如上述的例子)。synchronized(){}里()中的对象相同即可,这样就可以达到使用同一把锁,这样一个线程使用了这把锁,其他不安全的线程就会因为这把锁造成阻塞,而排队等待,这样就可以达到线程安全的目的。如上面的例子,我们把这两个不安全的线程,使用相同的锁即可。()里的内容有点类似于锁的名字,一次只供一个线程使用。

6.volatile关键字

6.1volatile保证内存可见性

代码在写入volatile修饰的变量的时候,

  • 改变线程内存中volatile变量副本的值;

  • 将改变后的副本的值从寄存器刷新到主内存中。

代码在读取volatile修饰的变量时候(对应上面4.3.2的情况)

  • 从主内存中读取volatile变量的最新值到线程的寄存器中;

  • 从工作内存中读取volatile变量的副本。

前面我们讨论内存的可见性说了,直接访问工作内存是非常快的,但是可能出现数据不一致的情况。
加上volatile,强制读写内存,速度变慢,但是数据更加准确了。

【解决方法】

public class TestDome9 {volatile public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while(count == 0) {}});t.start();Thread.sleep(100);count = 1;System.out.println("此时线程结束");}
}

同时volatile也可以保证代码顺序。

7. wait和notify

由于线程之间是抢占性执行的,因此线程之间的执行顺序难以预知。

但实际开发中我们希望合理的协调多个线程之间的执行先后顺序。

完成这个协调工作,主要涉及三个方法

  • wait()/wait(long timeout):让当前线程进入等待;

  • notify()/notifyAll():唤醒当前对象上等待的线程。

【注意】wait(),notify(),notifyAll()都是Object类的方法。
【通俗的例子】有两个篮球高手1和2(在同一对),篮球高手1开始在a点。传球给篮球高手2,在c点。篮球高手2在原地方等待一会,等到高手1跑到b点,再将球传给1。

7.1wait()方法

wait做的事情:

  • 使当前执行代码的线程进行等待;(把线程放到等待队列中)

  • 释放当前的锁;

  • 满足一定的条件时被唤醒,重新尝试获取这个锁。

wait()要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常。

wait结束等待条件:

  • 其他线程调用该对象的notify()方法。

  • wait()等待时间超时(wait(long timeout)方法等待timeout毫秒后,将继续执行)

  • 其他线程调用该等待线程的interupted()方法,会导致wait()抛出interruptedException异常

【代码示例】使用wait()方法

/*** Describe:wait()方法使用。(稍后配合notify使用)* User:lenovo* Date:2023-03-19* Time:19:31*/
public class ThreadDome7 {public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object) {System.out.println("等待中");object.wait();System.out.println("结束");}}
}

这样在执行到object.wait()之后就一直等待下去,这时候我们需要一个notify()来唤醒。

7.2 notify()方法

notify()方法是唤醒等待的线程。

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知notify(),并使它们获取该对象的对象锁。

  • 如果多个线程等待,则有线程调度器随机选择一个呈wait状态的线程;

  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

【代码示例】使用notify()方法唤醒线程

  • 创建WaitTask类,对应一个线程,run()内部循环调用wait()

  • 创建NotifyTask类,对应另一个线程,在run()内部调用一次notify

  • 注意,WaitTask和NotifyTask内部持有同一个Object locker, WaitTask和NotifyTask想要配合就需要搭配同一个Object.

/*** Describe:notify()方法* User:lenovo* Date:2023-03-19* Time:19:56*/
public class ThreadDemo8 {static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized(locker) {try {System.out.println("wait 开始");locker.wait();System.out.println("wait 结束");} catch (InterruptedException e) {e.printStackTrace();}}}}static class NotifyTask implements Runnable{private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 开始");locker.notify();System.out.println("notify 结束");}}}public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();Thread.sleep(1000);t2.start();}
}

7.3 notifyAll()方法

notify()方法只能唤醒同一个线程,使用notifyAll方法可以一次唤醒所有的等待进程

【代码示例】

/*** Describe:notifyAll()方法* User:lenovo* Date:2023-03-19* Time:20:20*/
public class ThreadDemo9 {static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized(locker) {try {System.out.println("wait 开始");locker.wait();System.out.println("wait 结束");} catch (InterruptedException e) {e.printStackTrace();}}}}static class NotifyTask implements Runnable{private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 开始");locker.notifyAll();System.out.println("notify 结束");}}}public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new WaitTask(locker));Thread t3 = new Thread(new WaitTask(locker));Thread t4 = new Thread(new NotifyTask(locker));t1.start();t2.start();t3.start();Thread.sleep(1000);t4.start();}
}

7.4 wait和sleep的对比(面试题)

其实理论上wait和sleep完全是没有可比性的。因为一个用于线程之间的通信,一个是让线程阻塞一段时间。唯一的相同点都是可以让线程停下来。

  • wait需要搭配synchronized使用。sleep不需要

  • wait是Object的方法;sleep是Thread的静态方法。

8.多线程模式

8.1单例模式

单例模式是校招中最常考的设计模式

啥是设计模式?
设计模式好比象棋中“棋谱”。比如:象棋中红方当头炮,黑方马来跳。针对红方的一些走法,黑方应对的时候有一些固定的套路。按照套路走,局势不会吃亏。

软件开发中也有很多常见的问题场景。针对这些问题场景,大佬总结了一些固定的套路,按照固定的套路来实现代码,不会吃亏。

单例模式能保证某个类在程序中只存在唯一的一份实例对象,而不会创建多个对象。

这一点在很多场景上都需要。比如JDBC中的DataSource实例就只需要一个。

单例模式具体实现方法,分为两种“饿汉模式”和“懒汉模式”

【模式一】饿汉模式

在类加载的时候创建唯一的实例对象

/*** Describe:单例模式,饿汉模式* User:lenovo* Date:2023-03-20* Time:10:48*/
class Singleton {//在创建类的时候直接实例化对象private static Singleton instance = new Singleton();//构造方法设置成private这样就不能创建新的对象private Singleton() {}public static Singleton getInstance() {return instance;}
}

【模式二】懒汉模式

类加载的时候不创建实例。第一次使用的时候才创建实例化对象

/*** Describe:单例模式,懒汉模式* User:lenovo* Date:2023-03-20* Time:10:57*/
class Singleton {private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if(instance == null) {instance = new Singleton();}return instance;}
}

【模式三】懒汉模式——多线程

上面的懒汉模式在单线程的情况下是安全的,但是在多线程下是不安全的。

【线程安全问题之处】

如果线程t1在执行完if判断条件以后,还没执行if内的语句,此时线程2也开始进行if条件判定,同时也会判断成功,就会导致创建了两个不同的对象。

【解决方法】加上synchronized

/*** Describe:懒汉模式,多线程问题* User:lenovo* Date:2023-03-20* Time:11:58*/
class Singleton {public static Singleton instence = null;private Singleton(){}public synchronized static Singleton getSingleton() {if(instence == null) {instence = new Singleton();}return instence;}
}

【解决方法改进版】双if语句

我们发现只有在instance = null时,才会导致线程安全问题,所以我们使用synchronized修饰if语句

但是我们发现这种方法,每次使用我们都需要加锁和解锁,这降低了大大降低了效率。并且我们只需要第一次调用才需要判断是否创建新的对象 ,所以我们再次改进,形成双if语句

//最终版本public static Singleton getInstance() {if(instence == null) {synchronized (Singleton.class) {if(instence == null) {instence = new Singleton();}}}return instence;}

【解析】第一个if()用于判断是否需要加锁。第二个if()用于判断是否重复实例化对象。

这样做的好处,不需要每次调用这个方法都需要加锁和解锁(避免了重复调用此方法产生额外的资源)。同时保证了多线程的安全性。

【懒汉模式和饿汉模式的优缺点】
饿汉模式:使用类的时候都会实例化对象,没有多线程安全问题。但是造成了资源的浪费。如果 这个对象太大,会造成很大的资源浪费。
懒汉没事:需要做到线程安全。不会造成资源的浪费。

8.2阻塞队列

阻塞队列是什么?

阻塞队列是一种特殊的队列,也遵守着“先进先出”的原则。

阻塞队列是一种线程安全的数据结构,并且具有以下特性:

  • 当队列满的时候,继续入队列就会阻塞,直到其他线程从队列中取走元素;

  • 队列空的时候,继续出队列也会出现阻塞,直到其他线程从队列中存放元素。

阻塞队列的一个典型应用场景就是“生产者消费模型”。这是一种非常经典的开发模型。

生产消费模型

生产者消费模型就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费之之间不直接产生通讯,而通过阻塞队列进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接人给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。

1)阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

比如在“秒杀”场景下,服务器同一时刻可能收到大量的支付请求。如果直接处理这些请求,处理这些业务的服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程)。这个时候就可以把这些请求都放到同一阻塞队列中(另一服务器中,不负责处理这些信息,但可以储存,抗压能力很强),然后再由消费者线程慢慢的来处理每个支付请求。

这样做内有效进行“削峰”,防止服务器被突然到来的一波请求直接冲垮。

2)阻塞队列也能使生产者和消费者之间耦合

比如过年一家人包饺子。一般都有明确的分工,比如一个人擀饺子皮(生产者),其他人负责包(消费者)。
擀饺子皮的人不关心包饺子的是谁,包饺子的人也不关心擀饺子皮的人是谁。擀饺子皮的人如果块,可以把饺子皮放到案盘上(阻塞队列),共包饺子的人使用。如果擀得慢,包饺子的人等着在案盘上拿。这样两者会不影响。

标准库中的阻塞队列

在Java标准库中内置了阻塞队列,如果我们需要一些程序中使用阻塞队列,直接使用标准库中的即可

  • BlockingQueue是一个接口。真正实现的类是LinkedBlockingQueue

  • put方法用于阻塞使的入队列,take用于阻塞式的出队列。

  • BlockingQueue也有offer,poll,peek等方法,但是这些方法不会带有阻塞特性。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;/*** Describe:* User:lenovo* Date:2023-03-20* Time:15:29*/
public class TestDemo13 {public static void main(String[] args) throws InterruptedException {BlockingQueue blockingQueue = new LinkedBlockingDeque<>();Thread customer = new Thread(() -> {while(true) {try {System.out.println("开始消费");int value = blockingQueue.take();System.out.println(value);System.out.println("结束消费");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}, "消费者");customer.start();Thread producer = new Thread(() -> {int count = 0;while(true) {try {System.out.println("开始生产");blockingQueue.put(count);System.out.println(count);count++;System.out.println("结束生产");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}, "生产者");producer.start();customer.join();producer.join();}
}

阻塞队列的实现

  • 通过“循环队列”的方式来实现;

  • 使用synchronized进行控制加锁;

  • put插入元素的时候,如果队列已满,就进行wait等待,使用take()中的notify()进行唤醒;

  • take取出元素的时候,如果队列为空就进行wait等待,使用put()中的notify()进行唤醒。

/*** Describe:阻塞队列的模拟实现* User:lenovo* Date:2023-03-20* Time:15:53*/
public class MyBlockingQueue {private int[] items = new int[1000];private  volatile int size = 0;private int head = 0;private int tail = 0;public synchronized void put(int value) throws InterruptedException {if(size == items.length) {this.wait();}items[tail] = value;tail++;size++;if(tail == items.length) {tail = 0;}this.notify();}public synchronized Integer take() throws InterruptedException {if(size == 0) {this.wait();}int value = items[head];head++;size--;if(head == items.length) {head = 0;}this.notify();return value;} 
}

【解析】put中wait(),不会被自身的notify()唤醒,因为wait在前,notify()在后,不会同时执行。

take()同理。

这就会产生如果队列已满,take()会唤醒put()继续;

队列为空,put()会唤醒take()继续。

同时为了解决可见性和重排序的问题,我们可以给各变量加上volatile。(这里我认为不会产生这样的问题)

【阻塞队列形象的例子】阻塞队列就像大坝,用户的请求就像上流的雨水,处理请求的服务器就像下游。在某一时间段(比如:秒杀活动),上流发生大规模的暴雨(用户请求蜂拥而至)),我们可以使用大坝平缓的放水(从阻塞队列按照一定的规律取元素)。这样不会让下流发生洪涝灾害。

相关内容

热门资讯

【MySQL】锁 锁 文章目录锁全局锁表级锁表锁元数据锁(MDL)意向锁AUTO-INC锁...
【内网安全】 隧道搭建穿透上线... 文章目录内网穿透-Ngrok-入门-上线1、服务端配置:2、客户端连接服务端ÿ...
GCN的几种模型复现笔记 引言 本篇笔记紧接上文,主要是上一篇看写了快2w字,再去接入代码感觉有点...
数据分页展示逻辑 import java.util.Arrays;import java.util.List;impo...
Redis为什么选择单线程?R... 目录专栏导读一、Redis版本迭代二、Redis4.0之前为什么一直采用单线程?三、R...
【已解决】ERROR: Cou... 正确指令: pip install pyyaml
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
Lock 接口解读 前置知识点Synchronized synchronized 是 Java 中的关键字,...
Win7 专业版安装中文包、汉... 参考资料:http://www.metsky.com/archives/350.htm...
3 ROS1通讯编程提高(1) 3 ROS1通讯编程提高3.1 使用VS Code编译ROS13.1.1 VS Code的安装和配置...
大模型未来趋势 大模型是人工智能领域的重要发展趋势之一,未来有着广阔的应用前景和发展空间。以下是大模型未来的趋势和展...
python实战应用讲解-【n... 目录 如何在Python中计算残余的平方和 方法1:使用其Base公式 方法2:使用statsmod...
学习u-boot 需要了解的m... 一、常用函数 1. origin 函数 origin 函数的返回值就是变量来源。使用格式如下...
常用python爬虫库介绍与简... 通用 urllib -网络库(stdlib)。 requests -网络库。 grab – 网络库&...
药品批准文号查询|药融云-中国... 药品批文是国家食品药品监督管理局(NMPA)对药品的审评和批准的证明文件...
【2023-03-22】SRS... 【2023-03-22】SRS推流搭配FFmpeg实现目标检测 说明: 外侧测试使用SRS播放器测...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
初级算法-哈希表 主要记录算法和数据结构学习笔记,新的一年更上一层楼! 初级算法-哈希表...
进程间通信【Linux】 1. 进程间通信 1.1 什么是进程间通信 在 Linux 系统中,进程间通信...
【Docker】P3 Dock... Docker数据卷、宿主机与挂载数据卷的概念及作用挂载宿主机配置数据卷挂载操作示例一个容器挂载多个目...