广南网站建设,怀集住房和城乡建设部网站,宁波找网站建设企业,百度快速收录接口目录 认识线程
线程是什么#xff1a;
线程与进程的区别
Java中的线程和操作系统线程的关系
创建线程
继承Thread类
实现Runnable接口
其他变形
Thread类及其常见方法
Thread的常见构造方法
Thread类的几个常见属性
Thread类常用的方法
启动一个线程-start()
中断…目录 认识线程
线程是什么
线程与进程的区别
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{Overridepublic 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{Overridepublic 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(){Overridepublic void run() {System.out.println(使用匿名内部类创建Thread子类对象);}};//调用start方法 启动线程t.start();
2匿名内部类创建Runnale子类对象
Thread t new Thread(new Runnable() {Overridepublic void run() {System.out.println(使用匿名内部类创建Runnable子类对象);}});//调用start方法 启动线程t.start();
3lambda表达式创建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类的几个常见属性
属性获取方法IDgetId()名称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()老板来电话了通知李四对方是骗子);isQuittrue;}
}运行结果 实例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()老板来电话了通知李四对方是骗子);//isQuittrue;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()老板来电话了通知李四对方是骗子);//isQuittrue;t1.interrupt();}
}运行结果 等待一个线程-join()
有时我们需要等待一个线程才能完成它的工作才能进行自己的工作。例如张三只有等李四转 账成功才决定是否存钱这时我们需要一个方法明确等待线程的结束。
class MyRunnable implements Runnable{Overridepublic 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 count0;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);}
}这段代码我们想的结果肯定是10000t1线程执行5000次之后t2线程执行5000次但是得出的结果是一个接近10000的随机值 此时我们就可以说这个线程是不安全的 修改共享数据
上面不安全的代码中涉及到多个线程针对count变量进行修改此时count是一个多个线程都可以访问到的”共享数据“
原子性
我们可以把一段代码想象成一间房间每一个线程就是进入这个房间的人。如果没有任何机制保证A进入房间之后没有出来B是不可以进入房间的这就是原子性。如果B进入房间了打断了A的隐私这个就是不具备原子性.原子是保证是不可分割的。
比如我们刚刚的操作count其实是由三个步骤组成的。
1、从内存把数据读到CPUload
2、进行数据更新add
3、把数据写回到CPUsave 我们可以看一下这个图,由于多个线程是并发执行的当t1,t2线程把count都读取的时候这时候读取的count都是0 两个线程都对其这个时候t1读取的count1t2读取的也count1当把数据写回CPU的时候是count1所以count并不是等于2而且上述的过程是随机的所以最后的结果不是10000。
所以如果我们如果保证t1执行的过程t2不会插入就可以了就如同A在房间的时候把房间上锁B进不来就可以了。
可见性
可见性指的是一个线程对共享变量值的修改能够及时的被其他线程看到。
就比如上述例子如果t1增加的时候t2知道就不会出现线程不安全的问题了。
顺序性
一段代码可能是这样的
1、去商场吃饭
2、回家写10分钟作业
3、去商场买文具
如果实在单线程情况下JVMCPU指令集会对其优化比如按照1-3-2的方式执行也是没有问题的并且少去了一次商场。这种就叫做指令重排序。 处理器在执行指令时可能会根据各种因素例如处理器架构、缓存等重新排列指令的执行顺序以最大程度地提高吞吐量和性能。这意味着代码中的指令可能不会按照编写的顺序来执行。然而指令重排序在单线程环境下通常不会引发问题因为重排序不会影响单线程程序的结果。 指令重排序是现代处理器为了提高性能而采取的一种优化技术它可以改变代码中指令的执行顺序以更有效地利用处理器的执行单元。尽管指令重排序可以提高程序的执行速度但在多线程编程中可能会引发一些问题特别是与内存可见性和线程安全性有关的问题。
解决线程不安全的问题
对于上述线程不安全的问题我们对代码进行了修改加入了synchronized关键字。 public class Demo5 {public static int count0;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 flag0;public static void main(String[] args) {Thread t1 new Thread(()-{while(flag0){}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 count0;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 flag0;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) {BlockingQueueInteger buffer new ArrayBlockingQueue(20);Thread producerThread new Thread(()-{int value0;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类的核心方法为scheduleschedule包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间执行 public static void main(String[] args) {Timer timer new Timer();timer.schedule(new TimerTask(){Overridepublic void run() {System.out.println(hello);}},3000); 自实现定时器
定时器的构成:
一个带优先级的阻塞队列 为啥要带优先级呢? 因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带 优先级的队列就可以高效的把这个 delay 最小的任务找出来. 队列中的每个元素是一个 Task 对象.Task 中带有一个时间属性, 队首元素就是即将同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
public class Timer {static class Task implements ComparableTask {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();}Overridepublic int compareTo(Task o) {// 谁的时间小谁排前面return (int)(time - o.time);}}// 核心结构private PriorityBlockingQueueTask queue new PriorityBlockingQueue();// 存在的意义是避免 worker 线程出现忙等的情况private Object mailBox new Object();class Worker extends Thread{Overridepublic void run() {while (true) {try {Task task queue.take();long curTime System.currentTimeMillis();if (task.time curTime) {// 时间还没到, 就把任务再塞回去queue.put(task);synchronized (mailBox) {// 指定等待时间 waitmailBox.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() {Overridepublic 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 LinkedBlockingQueueRunnable queue null;public Worker(LinkedBlockingQueueRunnable queue) {super(worker);this.queue queue;}Overridepublic 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 LinkedBlockingQueueRunnable queue new LinkedBlockingQueue();public void submit(Runnable command) {if (workerList.size() maxWorkerCount) {// 当前 worker 数不足, 就继续创建 workerWorker 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() {Overridepublic void run() {System.out.println(吃饭);}});Thread.sleep(1000);}
}