目录
认识线程
线程是什么:
线程与进程的区别
Java中的线程和操作系统线程的关系
创建线程
继承Thread类
实现Runnable接口
其他变形
Thread类及其常见方法
Thread的常见构造方法
Thread类的几个常见属性
Thread类常用的方法
启动一个线程-start()
中断一个线程-interrupt()
等待一个线程-join()
线程的状态
观察线程的所有状态
观察线程状态和转移
线程的安全(重点)
线程安全的概念
线程不安全的原因
修改共享数据
原子性
可见性
顺序性
解决线程不安全的问题
synchronized关键字
synchronized使用示例
volatile关键字
volatile和synchronized区别
wait和notify关键字
wait和sleep的区别(面试)
多线程案例
单例模式
饿汉模式
懒汉模式
阻塞式队列
生产者消费者模式
定时器
线程池
认识线程
线程是什么:
1)首先一个线程就是一个“执行流”,每一个线程直间都可以按照顺序执行自己的代码,多个线程之间“同时”执行多份代码(这里可能会有疑问,为什么同时要有一个引号呢,后面我们来揭晓)
线程与进程的区别
1)进程是包含线程的,每一个进程至少有一个线程称作为主线程。
2)进程与进程之间不共享空间,但是同一个进程中的线程共享同一个内存空间
3)进程是系统分配的最小单元,线程是系统调度的最小单元
4)线程比进程更加轻量化,创建销毁调度都比进称更快
最后,线程虽然比进程更加轻量化,但是还不能满足我们的需求,于是就引进了线程池和协程。
Java中的线程和操作系统线程的关系
线程是操作系统的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户来使用(如Linux中的pthread库)
Java标准库中Thread类可以视为是对操作系统提供的API进行进一步的抽象和封装
创建线程
继承Thread类
class Thread1 extends Thread{
@Override
public void run() {
System.out.println("这里是线程运行代码!");
}
}
public class Demo1 {
public static void main(String[] args) {
//创建Thread1类的实例
Thread1 t = new Thread1();
//调用start方法 启动线程
t.start();
}
}
实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("这里是线程运行代码");
}
}
public class Demo1 {
public static void main(String[] args) {
//创建Thread1类的实例
//Thread1 t = new Thread1();
//创建Thread类实例,调用Thread的构造方法时,将Runnable对象作为target参数
Thread t = new Thread(new MyRunnable());
//调用start方法 启动线程
t.start();
}
}
其他变形
1)匿名内部类创建子类对象
Thread t = new Thread(){
@Override
public void run() {
System.out.println("使用匿名内部类创建Thread子类对象");
}
};
//调用start方法 启动线程
t.start();
2)匿名内部类创建Runnale子类对象
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名内部类创建Runnable子类对象");
}
});
//调用start方法 启动线程
t.start();
3)lambda表达式创建Runnable子类对象
Thread t = new Thread(()->{
System.out.println("使用lambda表达式创建Runnable子类对象");
});
//调用start方法 启动线程
t.start();
Thread类及其常见方法
Thread类是JVM用来管理线程的一个类,换句话来说,每一个线程都有唯一的Thread对象与之关联用我们上面的例子来看,每一个执行流,都需要一个对象来描述,而Thread对象就是用来描述一个线程执行流的,JVM会将这些Thread对象组织起来,用于线程调度、管理。
Thread的常见构造方法
方法 | |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并且命名 |
Thread(Runnable target,String name) | 使用Runnable创建线程对象,并命名 |
Thread(TreadGroup group,Runnable target)(了解) | 线程可以用来分组管理,分好的组即为线程组,这个目前我们了解即可 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
Thread类的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
ID是线程的唯一标识,不同线程不会重复
名称是各种调试工具用到
状态表示线程但前所处于的一个情况,下面我们会进一步的说明
优先级高的线程理论上是更容易被调度到
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台进程结束后,才会结束运行。
是否存活,即简单的理解为,run方法是否结束运行
线程中断问题,之后我们进一步的说明
我们可以将下面的代码运行一下理解,上面的属性。
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 我还活着");
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我即将死去");
});
System.out.println(Thread.currentThread().getName()
+ ": ID: " + thread.getId());
System.out.println(Thread.currentThread().getName()
+ ": 名称: " + thread.getName());
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
System.out.println(Thread.currentThread().getName()
+ ": 优先级: " + thread.getPriority());
System.out.println(Thread.currentThread().getName()
+ ": 后台线程: " + thread.isDaemon());
System.out.println(Thread.currentThread().getName()
+ ": 活着: " + thread.isAlive());
System.out.println(Thread.currentThread().getName()
+ ": 被中断: " + thread.isInterrupted());
thread.start();
while (thread.isAlive()) {}
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
}
Thread类常用的方法
启动一个线程-start()
之前我们已经看到了如何覆写run方法创建一个对象,但是线程对象被创建出来并不意味着线程开始运行,而调用start方法,才真正的在操作系统底层创建出了一个线程。
中断一个线程-interrupt()
例如:李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们 需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如 何通知李四停止呢?这就涉及到我们的停止线程的方式了。
目前常见的中断线程常见下面两种方式:
1、通过共享一个标记(创建一个变量)
2、调用interrupt()方法
实例1:使用自定义变量来作为标志位
需要给标志位加上volatile 关键字(这个关键字的功能我们后面会介绍)
public class Demo2 {
public static volatile boolean isQuit = false;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(!isQuit){
System.out.println(Thread.currentThread().getName()
+"别管我,我在转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()
+"幸亏没全转过去!");
},"李四");
System.out.println(Thread.currentThread().getName()+":开始转账!");
t1.start();
try {
Thread.sleep(2000); // 主线程休眠,给 t1 线程一些时间来执行
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()
+"老板来电话了,通知李四对方是骗子");
isQuit=true;
}
}
运行结果:
实例2:使用Thread,interrupted()或者Therad.currentThread().isInterrupted()代替自定义标志位
Thread内部包含了一个boolean类型的变量作为线程中断的标记
方法 | 说明 |
public void interrupt() | 中断对象关联的线程,如果线程正在堵塞,则以异常通知否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清楚标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清楚标志位 |
总结来说就是:interrupt()
用于设置中断标志位,isInterrupted()
用于检查中断标志位的状态,而 interrupted()
则是检查并清除当前线程的中断状态。
public class Demo2 {
//public static volatile boolean isQuit = false;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
//while(!Thread.interrupted())//两种都可以
while(!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()
+"别管我,我在转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()
+"幸亏没全转过去!");
},"李四");
System.out.println(Thread.currentThread().getName()+":开始转账!");
t1.start();
try {
Thread.sleep(2000); // 主线程休眠,给 t1 线程一些时间来执行
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()
+":老板来电话了,通知李四对方是骗子");
//isQuit=true;
t1.interrupt();
}
}
如果我们按照上述代码直接进行修改会报错:
原因是 当你在 t1
线程的循环内使用 Thread.sleep(1000)
时,线程可能会在 sleep
过程中被 t1.interrupt()
中断,从而导致 InterruptedException
被抛出。然而,你在 catch
块中抛出了 RuntimeException
,可能导致编译错误。
所以我们对上述代码进行修改:
public class Demo2 {
//public static volatile boolean isQuit = false;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
//while(!Thread.interrupted())//两种都可以
while(!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()
+"别管我,我在转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()
+ "被中断了,幸亏没转过去!");
// 恢复中断状态,以便后续代码可以检查中断状态
Thread.currentThread().interrupt();
}
}
System.out.println(Thread.currentThread().getName()
+"幸亏没全转过去!");
},"李四");
System.out.println(Thread.currentThread().getName()+":开始转账!");
t1.start();
try {
Thread.sleep(2000); // 主线程休眠,给 t1 线程一些时间来执行
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()
+":老板来电话了,通知李四对方是骗子");
//isQuit=true;
t1.interrupt();
}
}
运行结果:
等待一个线程-join()
有时我们需要等待一个线程才能完成它的工作,才能进行自己的工作。例如:张三只有等李四转 账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()
+":我正在工作!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName()
+":我的工作结束了!");
}
}
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread t1 = new Thread(target,"李四");
Thread t2 = new Thread(target,"张三");
System.out.println("先让李四工作!");
t1.start();
t1.join();
System.out.println("李四的工作结束了,让张三来工作!");
t2.start();
t2.join();
System.out.println("张三的工作结束了!");
}
}
运行结果:
可能这么看的话,不是特别明白join的作用,如果我们把join注释掉:
作用就是显而易见了!
线程的状态
观察线程的所有状态
线程的状态是一个枚举类型 Thread.State
public class Demo4 {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
运行结果:
状态 | 说明 |
NEW | 安排了工作,但是没有运行 |
RUNNABLE | 可工作的,又可正在工作和即将工作 |
BLOCKED | 排队等着其他事情 |
WAITING | 排队等着其他事情 |
TIMED_WAITING | 排队等着其他事情 |
TERMINATED | 工作完成 |
关于new和Runnable状态区别:
"New" 状态是线程对象被创建但尚未启动执行的阶段,而 "Runnable" 状态是线程已经准备好执行但还没有被操作系统选中执行的阶段。使用 start()
方法可以将线程从 "New" 状态切换到 "Runnable" 状态,然后操作系统负责将其切换到 "Running" 状态并执行线程代码。
状态理解图
观察线程状态和转移
观察NEW、RUNNABLE、TERMINATED状态的转换
使用isAlive方法判定线程存活状态
isAlive()
方法用于判断线程是否已经启动并且尚未终止。该方法返回一个布尔值,如果线程处于活动状态,即正在运行或者已经启动但还未终止,返回true
;如果线程已经终止,返回false
。
public class Demo4 {
public static void main(String[] args) {
Thread t = new Thread(()->{
for (int i = 0; i < 100; i++) {
}
},"李四");
System.out.println(t.getName()+": "+t.getState());
t.start();
while(t.isAlive()){
System.out.println(t.getName()+": "+t.getState());
}
System.out.println(t.getName()+": "+t.getState());
}
}
运行结果:
当线程处于不同的状态时,可以通过示例更好地理解它们。以下是针对"BLOCKED"、"WAITING" 和 "TIMED_WAITING" 三种状态的示例:
BLOCKED(阻塞状态):
Object lock = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock) {
// 执行一些同步操作
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
// 这里的线程会进入 BLOCKED 状态,因为 lock 被 thread1 持有
}
});
thread1.start();
thread2.start();
WAITING(等待状态):
Object monitor = new Object();
Thread thread1 = new Thread(() -> {
synchronized (monitor) {
try {
monitor.wait(); // 进入 WAITING 状态,等待被唤醒
} catch (InterruptedException e) {
// 处理中断异常
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (monitor) {
monitor.notify(); // 唤醒等待中的线程
}
});
thread1.start();
thread2.start();
TIMED_WAITING(定时等待状态):
Thread thread = new Thread(() -> {
try {
Thread.sleep(5000); // 进入 TIMED_WAITING 状态,等待 5 秒后自动恢复
} catch (InterruptedException e) {
// 处理中断异常
}
});
thread.start();
在上述示例中:
- "BLOCKED" 状态:在第一个示例中,
thread2
试图获得与thread1
共享的锁时,会进入 "BLOCKED" 状态,因为锁被thread1
持有。 - "WAITING" 状态:在第二个示例中,
thread1
调用了monitor.wait()
,进入 "WAITING" 状态,等待被thread2
唤醒。 - "TIMED_WAITING" 状态:在第三个示例中,
thread
调用了Thread.sleep(5000)
,进入 "TIMED_WAITING" 状态,等待 5 秒后自动恢复到 "RUNNABLE" 状态。
上诉会有一些关于锁的用法,后面会说到。
yield()大公无私,让出CPU
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(true){
System.out.println("张三");
//Thread.yield();
}
},"t1");
Thread t2 = new Thread(()->{
while(true){
System.out.println("李四");
}
},"t2");
t1.start();
t2.start();
}
这段代码,如果我把yield()注释掉,张三和李四会几乎均等的打印,但是如果我不注释掉李四打印次数就会远远大于张三。
通过调用 yield
方法,线程可以主动放弃执行,让其他线程获得运行的机会,从而实现更合理的调度。
线程的安全(重点)
线程安全的概念
如果多线程环境下代码运行结果是符合我们的预期的,即在单线程的环境下应该的结果,则说这个程序是线程安全的。
线程不安全的原因
首先我们看以下的代码:
public class Demo5 {
public static int count=0;
public static void increase(){
count++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
increase();
}
});
t1.start();
t2.start();
t1.join(); // 等待 t1 线程完成
t2.join(); // 等待 t2 线程完成
System.out.println(count);
}
}
这段代码我们想的结果肯定是10000,t1线程执行5000次,之后t2线程执行5000次,但是得出的结果是一个接近10000的随机值
此时我们就可以说这个线程是不安全的
修改共享数据
上面不安全的代码中涉及到多个线程针对count变量进行修改,此时count是一个多个线程都可以访问到的”共享数据“
原子性
我们可以把一段代码想象成一间房间,每一个线程就是进入这个房间的人。如果没有任何机制保证,A进入房间之后,没有出来,B是不可以进入房间的,这就是原子性。如果B进入房间了,打断了A的隐私,这个就是不具备原子性.原子是保证是不可分割的。
比如我们刚刚的操作:count++,其实是由三个步骤组成的。
1、从内存把数据读到CPU(load)
2、进行数据更新(add)
3、把数据写回到CPU(save)
我们可以看一下这个图,由于多个线程是并发执行的,当t1,t2线程把count都读取的时候这时候读取的count都是0 ,两个线程都对其++,这个时候t1读取的count=1,t2读取的也count=1,当把数据写回CPU的时候是count=1,所以count并不是等于2,而且上述的过程是随机的,所以最后的结果不是10000。
所以如果我们如果保证t1执行的过程t2不会插入就可以了,就如同A在房间的时候把房间上锁,B进不来,就可以了。
可见性
可见性指的是:一个线程对共享变量值的修改,能够及时的被其他线程看到。
就比如上述例子,如果t1增加的时候t2知道,就不会出现线程不安全的问题了。
顺序性
一段代码可能是这样的:
1、去商场吃饭
2、回家写10分钟作业
3、去商场买文具
如果实在单线程情况下,JVM,CPU指令集会对其优化,比如,按照1->3->2的方式执行,也是没有问题的,并且少去了一次商场。这种就叫做指令重排序。
处理器在执行指令时,可能会根据各种因素(例如处理器架构、缓存等)重新排列指令的执行顺序,以最大程度地提高吞吐量和性能。这意味着代码中的指令可能不会按照编写的顺序来执行。然而,指令重排序在单线程环境下通常不会引发问题,因为重排序不会影响单线程程序的结果。
指令重排序是现代处理器为了提高性能而采取的一种优化技术,它可以改变代码中指令的执行顺序,以更有效地利用处理器的执行单元。尽管指令重排序可以提高程序的执行速度,但在多线程编程中可能会引发一些问题,特别是与内存可见性和线程安全性有关的问题。
解决线程不安全的问题
对于上述线程不安全的问题,我们对代码进行了修改加入了synchronized关键字。
public class Demo5 {
public static int count=0;
public static synchronized void increase(){
count++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
increase();
}
});
t1.start();
t2.start();
t1.join(); // 等待 t1 线程完成
t2.join(); // 等待 t2 线程完成
System.out.println(count);
}
}
运行结果:
此时的运行结果就是和我们预期的相同了,下面我们就来说一下synchronized关键字。
synchronized关键字
1)互斥
synchronized 会引起互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象时,synochronized就会堵塞等待.
- 进入synchronized修饰的代码块,就相当于加锁.
- 退出synchronized修饰的代码块,就相当于解锁.
理解 "阻塞等待". 针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝 试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的 线程, 再来获取到这个锁.
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这 也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能 获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
synchronized底层是使用操作系统mutex lock实现的.
2)刷新内存
synochronized的工作过程;
1、获得互斥锁
2、从主内存拷贝变量的最新副本到工作的内存
3、执行代码
4、将更改后的共享变量的值刷新到主内存
5、释放互斥锁
所以synchronized也能保证内存可见性.
3)可重入
可重入(Reentrancy),也被称为递归性,是指一个线程在持有某个锁的情况下,能够再次获得该锁而不会被阻塞。这种情况下,锁会记录线程持有的次数,每次成功获取锁时,计数器会增加,释放锁时,计数器会递减。只有当计数器为零时,锁才会被完全释放,其他线程才能获得锁。
可重入性使得编写递归代码和多层嵌套调用时更加方便,因为你不必担心线程会因为多次获得同一个锁而陷入死锁状态。同时,它也为一些高级同步机制(如可重入锁、读写锁等)的实现提供了基础。
synchronized使用示例
synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配一个具 体的对象来使用.
示例1:同步实例方法
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
在这个示例中,increment
和 getCount
方法都被使用 synchronized
修饰,这意味着同一时刻只有一个线程可以访问这些方法。这样可以确保在多线程环境中对 count
变量的操作是安全的。
示例 2:同步代码块
public class SynchronizedExample {
private int count = 0;
private Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
在这个示例中,我们使用了同步代码块来控制对 count
变量的访问。通过指定一个对象作为锁,我们确保在同一时刻只有一个线程可以进入同步块,从而实现了线程安全。
示例 3:静态同步方法
public class SynchronizedExample {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
这个示例中,我们使用 synchronized
关键字修饰了静态方法。静态同步方法锁定的是类的 Class
对象,确保在同一时刻只有一个线程可以访问这些静态方法。
区别总结:
- 示例1中的锁是实例对象的监视器锁,用于同步实例方法。
- 示例2中的锁是自定义的
lock
对象,用于同步代码块。它可以实现更细粒度的同步控制,也可以用于不同实例之间的同步。 - 示例3中的锁是类的
Class
对象,用于同步静态方法。
在选择使用哪种同步方式时,你需要根据实际需求来考虑同步的粒度、范围以及性能等因素。
volatile关键字
volatile能保证内存可见性
volatile
关键字在Java中用于确保变量的可见性。它的主要作用是告诉Java虚拟机,这个变量可能被多个线程同时访问,因此需要确保线程之间对这个变量的修改对其他线程是可见的。具体来说,volatile
变量具有以下特性:
-
禁止重排序:
volatile
变量会禁止编译器和运行时环境对其赋值和读取操作进行重排序。这确保了写操作不会被提前到读操作之前,从而避免了可能的可见性问题。 -
强制刷新主内存: 当一个线程对
volatile
变量进行写操作时,会强制将该变量的值刷新到主内存中,而不仅仅是线程的本地缓存。当其他线程需要读取这个变量时,它们会从主内存中读取最新的值,而不是从自己的本地缓存。
通过禁止重排序和强制刷新主内存,volatile
变量确保了对这个变量的写操作对其他线程是可见的,从而保证了内存可见性。
代码示例:
import java.util.Scanner;
public class Demo6 {
public static int flag=0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(flag==0){
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(()->{
Scanner scan = new Scanner(System.in);
System.out.println("输入一个整数:");
flag = scan.nextInt();
});
t1.start();
t2.start();
}
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)
读的是自己工作内存中的内容. 当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.
这种情况就是典型的可见性问题,即一个线程对共享变量的修改对其他线程不可见。使用 volatile
关键字可以解决这个问题,因为它会强制线程在读取和写入 flag
变量时都从主内存中读取和写入,从而确保线程之间的可见性。
public static volatile int flag = 0;
// 执行效果
// 当用户输入非0值时, t1 线程循环能够立即结束.
volatile和synchronized区别
volatile不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.
public class Demo5 {
public static volatile int count=0;
public static void increase(){
count++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
increase();
}
});
t1.start();
t2.start();
t1.join(); // 等待 t1 线程完成
t2.join(); // 等待 t2 线程完成
System.out.println(count);
}
}
//运行结果:7602
此时可以看到,最终count无法保证是10000
synchronized 也能保证内存可见性
synchronized 既能保证原子性, 也能保证内存可见性.
public volatile static int flag=0;
public static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(true){
synchronized (lock){
if(flag!=0){
break;
}
}
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(()->{
Scanner scan = new Scanner(System.in);
System.out.println("输入一个整数:");
flag = scan.nextInt();
});
t1.start();
t2.start();
}
wait和notify关键字
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。
完成这个协调工作, 主要涉及到三个方法
wait() / wait(long timeout): 让当前线程进入等待状态.
notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.
wait()方法
wait作用:
- 使当前执行代码的线程进行等待(把线程放入到等待队列中)
- 释放当前锁
- 满足一定条件时被唤醒,重新尝试获取这个锁
wati要搭配synchronized来使用,脱离synchronized使用wait会抛出异常
wait结束的等待条件:
- 其他线程调用该对象的notify方法
- wait等待的时间超过(wait方法提供一个带有timeout参数的版本,来指定时间)
- 其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptedException异常
示例:
public static void main(String[] args) {
Object lock = new Object();
Thread t = new Thread(()->{
synchronized (lock){
System.out.println("正在执行");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("执行结束");
}
});
t.start();
}
这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就 需要使用到了另外一个方法唤醒的方法notify()
notify()方法
notify方法是唤醒等待线程
示例:
public class Demo7 {
private static final Object lock = new Object();
private static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!flag) {
try {
lock.wait(); // 等待条件满足
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread t1: Condition met!");
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread t2: Condition is about to be met.");
flag = true;
lock.notify(); // 通知等待的线程条件已满足
}
});
t1.start();
t2.start();
}
}
方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行 完,也就是退出同步代码块之后才会释放对象锁。
notifyAll()方法
notifyAll
方法是 Java 中用于线程间通信的一个方法,它类似于 notify
方法,但有一些不同之处。
notifyAll
方法用于唤醒在当前对象上调用了 wait
方法而进入等待状态的所有线程。与 notify
方法不同,它会通知所有等待的线程,而不仅仅是其中一个。这在某些情况下非常有用,特别是当有多个线程在同一个对象上等待某个条件满足时。
示例:
public class Demo7 {
private static final Object lock = new Object();
private static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!flag) {
try {
lock.wait(); // 等待条件满足
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread t1: Condition met!");
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("Thread t2: Condition is about to be met.");
flag = true;
lock.notifyAll(); // 通知所有等待的线程条件已满足
}
});
Thread t3 = new Thread(() -> {
synchronized (lock) {
while (!flag) {
try {
lock.wait(); // 等待条件满足
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread t3: Condition met!");
}
});
t1.start();
t2.start();
t3.start();
}
}
//运行结果:
//Thread t2: Condition is about to be met.
//Thread t1: Condition met!
//Thread t3: Condition met!
在这个示例中,除了使用 notifyAll
方法通知等待的 t1
线程外,还有一个额外的 t3
线程等待条件满足。当 t2
线程满足条件并调用 notifyAll
后,所有等待在 lock
上的线程都会被唤醒。
需要注意的是,使用 notifyAll
可以确保所有等待的线程都能够被唤醒,但在某些情况下可能会导致过多的线程被唤醒,从而影响性能。因此,在选择使用 notify
还是 notifyAll
时,需要根据具体的应用场景来考虑。
wait和sleep的区别(面试)
总结来说,wait
和 sleep
的主要区别在于:
wait
是Object
类的方法,用于线程之间的协作和通信;sleep
是Thread
类的方法,用于线程休眠。wait
需要在同步代码块内部使用,而sleep
可以在任何地方使用。- 调用
wait
会释放锁,调用sleep
不会释放锁。 wait
通常和条件判断一起使用,用于等待某个条件满足;sleep
用于实现线程的延迟或定时操作。
多线程案例
单例模式
提到单例模式dehua,我们就需要了解一下什么是设计模式:
设计模式好比象棋中的 "棋谱". 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有 一些固定的套路. 按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 "问题场景". 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照 这个套路来实现代码, 也不会吃亏.
单例模式就是一种设计模式。
单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例。
就比如JDBC中的DataSource实例就是只需要一个。
而单例模式具体实现又分为”饿汉模式“和”懒汉模式“两种。
饿汉模式
它在类加载时就创建了单例实例,无论是否会被使用。在这种模式下,实例在类加载的时候就被创建,因此称为“饿汉”模式。
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
// 私有构造方法
}
public static Singleton getInstance() {
return instance;
}
}
作用是:在第一次使用单例实例时不需要等待,但可能会导致资源浪费,因为实例在类加载时就被创建,即使在后续的运行中可能并未使用到它。
而其中将构造方法设置为私有是为了防止其他类实例化该类。这是单例模式的关键特性之一,确保只有一个实例能够被创建。
懒汉模式
单线程版本
类在加载的过程中不创建实例,第一次使用的时候创建实例
public class Singleton {
private static Singleton instance;
private Singleton() {
// 私有构造方法
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // 输出 true,表示是同一个实例
}
}
多线程版本
懒汉模式-多线程版本可能是不安全的:
在懒汉模式的多线程版本中,如果不加入额外的线程安全机制,可能会导致在多线程环境下创建多个实例,从而违背了单例模式的要求。这是因为多个线程可能会同时通过
if (instance == null)
的检查,然后都进入到实例化的逻辑,最终导致创建多个实例
加入synchronized可以改善线程安全问题:
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
多线程改良版本
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
// 私有构造方法
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
// 在多线程环境下测试
Runnable task = () -> {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
};
// 启动多个线程
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
//运行结果
//2122349303
//2122349303
在这个版本的代码中,我们使用了双重检查锁机制,首先检查 instance
是否为 null
,如果是,才进入同步块进行实例化。同时,使用 volatile
关键字修饰 instance
变量,以确保在多线程环境下对变量的可见性。
二者的区别
这两种方式的主要区别在于性能和锁的粒度:
-
synchronized 方法:
- 每次调用
getInstance
方法都会对整个方法进行加锁,这会造成性能上的一些开销,特别是在高并发情况下。 - 只有一个线程可以进入方法,其他线程必须等待该线程执行完毕,才能继续执行。
- 这种方式简单,不需要双重检查,但可能会对性能有一定的影响。
- 每次调用
-
双重检查锁:
- 通过两次检查
instance
变量,第一次在无锁的情况下进行,只有在instance
为null
时才会尝试加锁创建实例。 - 如果
instance
不为null
,就不需要进入同步块,避免了每次都加锁的性能开销。 - 这种方式在第一次创建实例时才会加锁,之后获取实例时无需加锁,可以减小锁的粒度,提高了性能。
- 通过两次检查
综合来看,如果您对性能要求较高且在多线程环境下使用单例模式,双重检查锁可能是更好的选择。如果您的应用程序不太关注性能,而且代码简单易懂,使用 synchronized 方法也可以保证线程安全。
阻塞式队列
阻塞式队列是一种特殊的队列,也遵守“先进先出”原则
阻塞队列是一种线程安全的数据结构,具有以下特征:
- 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素
- 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素
阻塞队列的一个典型应用场景就是“生产者消费者模型”,这是一种非常典型的开发模型。
生产者消费者模式
生产者-消费者模式是一种多线程协作的模式,用于解决一个线程生产数据,另一个线程消费数据的问题。这种模式可以有效地实现数据的异步传递和处理,同时也能够控制资源的利用和线程的协调。
在生产者-消费者模式中,通常有以下角色:
-
生产者(Producer): 负责生成数据并将数据放入共享的缓冲区中,以供消费者使用。
-
消费者(Consumer): 从共享的缓冲区中获取数据并进行处理。消费者在缓冲区为空时会等待,直到有新数据可用。
-
缓冲区(Buffer): 用于存储生产者生成的数据,以及供消费者从中获取数据。缓冲区应该是线程安全的数据结构。
生产者-消费者模式的目标是实现生产者和消费者之间的解耦,使它们能够独立运行,并在合适的时候协调工作。通过合理地控制生产者和消费者的速度,可以避免资源的浪费和线程的竞争问题。
示例:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Demo9 {
public static void main(String[] args) {
BlockingQueue<Integer> buffer = new ArrayBlockingQueue<>(20);
Thread producerThread = new Thread(()->{
int value=0;
while(true){
try {
buffer.put(value);
System.out.println(Thread.currentThread().getName()+" "+value);
value++;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"生产者");
Thread consumerThread = new Thread(()->{
while (true){
try {
int value = buffer.take();
System.out.println(Thread.currentThread().getName()+" "+value);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"消费者");
producerThread.start();
consumerThread.start();
}
}
//运行结果:
//生产者 0
//消费者 0
//生产者 1
//消费者 1
//生产者 2
//消费者 2
//消费者 3
//生产者 3
//生产者 4
//消费者 4
//...
生产者线程生成数据并放入队列,消费者线程从队列中获取数据。通过堵塞队列,我们可以避免手动实现等待和通知机制,从而实现了线程间的协调。
堵塞队列则提供了以下几种常用的操作:
-
put(E element)
:将元素放入队列中,如果队列已满,线程会被阻塞,直到队列有空位。 -
take()
:从队列中取出元素,如果队列为空,线程会被阻塞,直到队列有元素。 -
offer(E element, long timeout, TimeUnit unit)
:将元素放入队列中,如果队列已满,在指定的时间内等待,如果仍然无法放入则返回特定值。 -
poll(long timeout, TimeUnit unit)
:从队列中取出元素,如果队列为空,在指定的时间内等待,如果仍然无法取出则返回特定值。
定时器
定时器是软件开发中的一个重要的组件,类似一个闹钟,达到某个时间之后,就执行某个指定的代码。
定时器式一种实际开发中非常常用的组件.
比如网络通信中,如果对方500ms内没有返回数据,则断开连接尝试重新连接.
比如一个Map,希望里面某个Key在3s之后自动删除.
类似这样的场景就需要用到定时器.
标准库中的定时器
- 标准库中提供了一个Timer类,Timer类的核心方法为schedule
- schedule包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间执行
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println("hello");
}
},3000);
自实现定时器
定时器的构成:
- 一个带优先级的阻塞队列
为啥要带优先级呢? 因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带 优先级的队列就可以高效的把这个 delay 最小的任务找出来.
- 队列中的每个元素是一个 Task 对象.
- Task 中带有一个时间属性, 队首元素就是即将
- 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
public class Timer {
static class Task implements Comparable<Task> {
private Runnable command;
private long time;
public Task(Runnable command, long time) {
this.command = command;
// time 中存的是绝对时间, 超过这个时间的任务就应该被执行
this.time = System.currentTimeMillis() + time;
}
public void run() {
command.run();
}
@Override
public int compareTo(Task o) {
// 谁的时间小谁排前面
return (int)(time - o.time);
}
}
// 核心结构
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();
// 存在的意义是避免 worker 线程出现忙等的情况
private Object mailBox = new Object();
class Worker extends Thread{
@Override
public void run() {
while (true) {
try {
Task task = queue.take();
long curTime = System.currentTimeMillis();
if (task.time > curTime) {
// 时间还没到, 就把任务再塞回去
queue.put(task);
synchronized (mailBox) {
// 指定等待时间 wait
mailBox.wait(task.time - curTime);
}
} else {
// 时间到了, 可以执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
public Timer() {
// 启动 worker 线程
Worker worker = new Worker();
worker.start();
}
// schedule 原意为 "安排"
public void schedule(Runnable command, long after) {
Task task = new Task(command, after);
queue.offer(task);
synchronized (mailBox) {
mailBox.notify();
}
}
public static void main(String[] args) {
Timer timer = new Timer();
Runnable command = new Runnable() {
@Override
public void run() {System.out.println("我来了");
timer.schedule(this, 3000);
}
};
timer.schedule(command, 3000);
}
}
线程池
线程池是一种用于管理和复用线程的机制,它能够有效地管理线程的创建、销毁以及复用,从而提高系统的性能和资源利用率。使用线程池可以避免频繁地创建和销毁线程,降低了线程创建的开销,并且能够更好地控制线程的数量,防止过多的线程造成系统资源的耗尽。
想象一下您是一个餐馆的经理,而餐厅的服务员就像是线程,而顾客的订单就是任务。每当一个顾客进来,您需要为他们提供服务,即执行任务。现在,您有两种处理方式:
没有线程池的情况:
- 顾客进来,您临时雇佣一个服务员(创建一个新线程)。
- 顾客的订单被处理后,您解雇服务员(销毁线程)。
这种方式会导致一些问题:
- 每次都需要花时间和资源雇佣/解雇服务员,浪费了开销。
- 如果顾客进来太多,可能导致服务员不够,从而出现长时间等待的情况。
有线程池的情况:
- 您预先雇佣一批服务员(线程池中的线程)。
- 顾客进来,您指派一个空闲的服务员(从线程池中选取一个线程)来处理订单。
- 订单处理完毕后,服务员继续待命,等待下一个顾客。
这种方式的优势在于:
- 您不需要频繁地雇佣/解雇服务员,节省了开销。
- 线程池管理着一定数量的服务员,确保总是有服务员可用,避免等待时间。
总结来说,线程池就像是一个预先雇佣好的服务员团队,可以高效地处理顾客的订单,避免了频繁创建和销毁线程带来的性能开销,同时提供了更好的资源利用率和任务管理。这在处理多任务并发的情况下非常有用,例如网络请求、后台处理等场景。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo11 {
public static void main(String[] args) {
// 提交任务给线程池执行
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务给线程池执行
for (int i = 1; i <= 5; i++) {
int taskNumber = i;
executor.execute(()->{
System.out.println("Task " + taskNumber
+ " 正在由线程池 "
+ Thread.currentThread().getName()
+"执行");
});
}
// 关闭线程池
executor.shutdown();
}
}
//运行结果:
//Task 1 正在由线程池 pool-1-thread-1执行
//Task 3 正在由线程池 pool-1-thread-3执行
//Task 2 正在由线程池 pool-1-thread-2执行
//Task 5 正在由线程池 pool-1-thread-3执行
//Task 4 正在由线程池 pool-1-thread-1执行
在这个例子中,我们首先创建了一个固定大小为3的线程池。然后,我们提交了5个任务给线程池执行。每个任务输出自己的编号和执行线程的名称。最后,我们关闭了线程池。
Executors创建线程池的几种方式:
- newFixedThreadPool:创建固定的线程数的线程池.
- newCachedThreadPool:创建线程数动态增长的线程池.
- newSingleThreadExecutor:创建只包含单个线程的线程池.
- newScheduledThreadPool:设定延迟时间后执行命令或定期执行命令(可以理解成进阶版的Timer).
Executors本质就是ThreadPoolExecutor类的封装.
实现线程池
- 核心操作为 submit, 将任务加入线程池中
- 使用 Worker 类描述一个工作线程.
- 使用 Runnable 描述一个任务.
- 使用一个 BlockingQueue 组织所有的任务
- 每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
- 指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增线程了.
class Worker extends Thread {
private LinkedBlockingQueue<Runnable> queue = null;
public Worker(LinkedBlockingQueue<Runnable> queue) {
super("worker");
this.queue = queue;
}
@Override
public void run() {
// try 必须放在 while 外头, 或者 while 里头应该影响不大
try {
while (!Thread.interrupted()) {
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
}
}
}
public class MyThreadPool {
private int maxWorkerCount = 10;
private LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue();
public void submit(Runnable command) {
if (workerList.size() < maxWorkerCount) {
// 当前 worker 数不足, 就继续创建 worker
Worker worker = new Worker(queue);
worker.start();
}
// 将任务添加到任务队列中
queue.put(command);
}
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool();
myThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("吃饭");
}
});
Thread.sleep(1000);
}
}