多线程基础
- 1. 进程和线程
- 2. 创建线程的方法
- 3. 并发编程效率分析
- 4. Thread 的常见构造方法
- 5. Thread的几个常见的属性
- 6 中断线程
- 7. 线程等待(join)
- 8. 获取当前线程的实例——Thread.currentThread()
- 9. 休眠当前线程——Thread.sleep()
- 10. 线程的状态
- 11. 线程安全(重难点)
- 12. synchronized关键字【面试重灾区】
- 13. Java标准库中的线程安全类
- 14. volatile 关键字
- 【面试】synchronized和volatile的区别
- 15. wait 和 notify
- 16. 单例模式
- 17. 阻塞式队列
- 【面试】为什么wait和notify要强制放到synchronized中使用?
- 18. 定时器
- 19. 线程池
- 20. 工厂模式
1. 进程和线程
进程:一个运行起来的程序就是一个进程,进程是分配资源的基本单位。
线程:线程是调度的基本单位,线程存在于进程之中,一个进程可以有若干个线程,这些线程共享进程的资源。
引入线程的目的:引入线程就是提高为了“并发编程”的效率。虽然多进程也可以完成“并发编程”,但是由于创建进程/销毁进程/调度进程得开销太大了,导致“并发编程”的效率不是很高。
线程的优点:线程相比进程更加轻量化,在创建、销毁、调度时,都比进程更加高效。因为创建线程和销毁线程时,并不需要申请和释放资源。
面试题:进程和线程的练习和区别
- 进程里面包含线程,一个进程里面可以有多个线程
- 每个进程都有独立的内存空间(虚拟地址空间),同一个进程的多个线程之间,共享这个虚拟地址空间(也就是线程共享进程的资源)
- 进程时资源分配的基本单位,线程是操作系统调度执行的基本单位
2. 创建线程的方法
- 继承Thred,重写 run 方法
- 实现Runnable接口,重写 run 方法
- 采用匿名内部内的方式,继承Thred,重写 run 方法
- 采用匿名内部内的方式,实现Runnable接口,重写 run 方法
- 使用lambda表达式
这里更加推荐使用第 4 中方法,简单,也可以给线程起名字,例如:
public class ThreadDemo {
public static void main(String[] args) {
//创建匿名内部类的实例,将这个实例作为参数传给Thread。
Thread thread = new Thread(new MyRunnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"myThread");//传入一个 name 参数
thread.start();
}
}`在这里插入代码片`
面试题:thread.start() 和 thread.run() 的区别
start:
run:
通过上面的图可以看出:
- start 在运行时会创建一个新的线程
- 而run会在main的线程直接调用
3. 并发编程效率分析
public class ThreadDemo7 {
private static final long count = 10_0000_0000;
//串行针对 a,b进行自增
public static void serial(){
//获取到当前系统的毫秒级时间戳.
long begin = System.currentTimeMillis();
int a=0;
for(long i=0;i<count;i++){
a++;
}
int b=0;
for(long i=0;i<count;i++){
b++;
}
long end = System.currentTimeMillis();
System.out.println("serial time:"+(end-begin));
}
public static void concurrency(){
long begin = System.currentTimeMillis();
Thread t1 = new Thread(){
@Override
public void run() {
int a=0;
for(long i=0;i<count;i++){
a++;
}
}
};
t1.start();
Thread t2 = new Thread(){
@Override
public void run() {
int b=0;
for(long i=0;i<count;i++){
b++;
}
}
};
t2.start();
//需要保证在 t1 和 t2 都执行玩了之后,再结束及时
try {
//join 就是等待对应的线程结束
//当 t1 和 t2 没有执行完之前,join方法就会阻塞等待
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("concurrency time:"+(end-begin));
}
public static void main(String[] args) {
serial();
concurrency();
}
}
运行结果:
根据执行结果可以看出,速度确实提高了。
是正好是一倍吗? 显然不一定,因为线程调度也需要时间。
4. Thread 的常见构造方法
5. Thread的几个常见的属性
public class ThreadDemo9 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//打印线程名字
//通过 Thread.currentThread()方法 过去到线程实例
//那个线程调用这个方法,就能获取到对应的实例.
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"myThread");
thread.start();
//打印以下线程的属性
System.out.println("id:"+thread.getId());
System.out.println("name:"+thread.getName());
System.out.println("state:"+thread.getState());
System.out.println("priority:"+thread.getPriority());
System.out.println("isDaemon:"+thread.isDaemon());
System.out.println("isInterrupted:"+thread.isInterrupted());
System.out.println("isAlive:"+thread.isAlive());
}
}
运行结果:
注意:进程的销毁和代码中对象的销毁是不一样的
进程的销毁时PCB的销毁,代码中对象的销毁依赖的是JDK中的GC回收机制,这两者的生命周期是不一样的。
6 中断线程
中断一个线程就是让一个线程停止工作
中断一个线程的方法:
1. 通过共享的标记来进行沟通
public class ThreadDemo10 {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run() {
while(flag){
System.out.println("线程运行中~~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
//主循环中等待3秒
Thread.sleep(3000);
//三秒之后,将 flag 改成 false
flag = false;
}
}
2. 调用 interrupt() 方法来通知
以 isInterrupted() 为示例:
public class ThreadDemo11 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
//默认状态下,isInterrupted 值为 false
while(!Thread.currentThread().isInterrupted()){
System.out.println("线程运行中~~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//break 可以保证循环结束
break;
}
}
}
};
thread.start();
//在主线程中,通过 thread.isInterrupted() 方法来设置这个标记
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这个操作就是把 Thread.currentThread().isInterrupted()置为 true
thread.interrupt();
}
}
注意:
isInterrupted() 和 interrupted() 的区别
- isInterrupted() 是Thread的示例方法,而interrupted() 是Thread 的静态方法 。
- isInterrupted() 只能反映线程是否被中断,而不能改变线程的状态。即当调用isInterrupted() 时,如果返回 true ,是不会对标记位进行修改的,再次调用返回的仍然是true(相当于开关按下去,不会反弹)
- interrupted()不仅能反映线程是否被中断,还能清除中断线程的标志位。即当调用interrupted() 时,当返回 true ,还会将标记位修改为false,再次调用返回的就是false(相当于开关按下去,会反弹)
7. 线程等待(join)
线程和线程之间,调度顺序是完全不确定的(取决于操作系统调度器自身的实现),为了能够实现对线程执行顺序的控制,就可以采用线程等待的方法来实现。
场景实现
一种常见的场景就是:t1 线程创建 t2 , t3 线程,让 t2 和 t3 这两个线程分别执行一些任务,然后 t1 线程进行汇总。为了满足这样的场景,就得使 t1 结束的时机都比 t2 , t3 迟。
代码实现:
public class ThreadDemo7 {
private static final long count = 10_0000_0000;
public static void concurrency(){
long begin = System.currentTimeMillis();
Thread t1 = new Thread(){
@Override
public void run() {
int a=0;
for(long i=0;i<count;i++){
a++;
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
int b=0;
for(long i=0;i<count;i++){
b++;
}
}
};
t1.start();
t2.start();
try {
//当 t1 和 t2 没有执行完之前,join方法就会阻塞等待
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("concurrency time:"+(end-begin));
}
public static void main(String[] args) {
concurrency();
}
}
上述代码中,为了能够实现 计时的功能 ,主线程的long end = System.currentTimeMillis(); 必须得等 t1 和 t2 线程执行完之后再执行。
而t1.join(); t2.join();
即可以让当前执行的主线程暂停下来,等待 t1 和 t2 执行完之后再执行主线程。
8. 获取当前线程的实例——Thread.currentThread()
public class ThreadDemo13 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getId());
System.out.println(this.getId());
}
};
t.start();
}
}
执行结果:
在这种情况下,用图示的两种方法获取当前线程的示例,好像都没啥问题。但是要注意的是:必须使用 继承Thread ,重写 run 的方式创建线程,才会没区别。如果通过 Runnable 或者 lambda 的方式就不行了,因为此时的 this 并不代表当前线程。
9. 休眠当前线程——Thread.sleep()
sleep这个方法,本质上就是将线程PCB从就绪队列移动到了阻塞队列。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
运行结果:
两个数字大概差 3000
10. 线程的状态
- NEW: 对象创建出来了,但是内核的PCB没有创建出来
- RUNNABLE: PCB已经创建出来,同时PCB处于随时待命状态(就绪),这个线程可能在CPU上运行,也可能在就绪队列中排队
- TERMINATED: 工作完成了,PCB已经结束了,但是创建的对象还在。
- BLOCKED: 活等
- WAITING: 死等
- TIMED_WAITING: 加锁等待,等待锁被其他线程释放之后,就会重新被激活。
public class ThreadDemo14 {
public static void main(String[] args) throws InterruptedException {
Thread t =new Thread(){
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
};
System.out.println(t.getId()+":"+t.getState());
t.start();
System.out.println(t.getId()+":"+t.getState());
Thread.sleep(1000);
System.out.println(t.getId()+":"+t.getState());
Thread.sleep(3000);
t.interrupt();
Thread.sleep(1000);
System.out.println(t.getId()+":"+t.getState());
}
}
运行结果:
11. 线程安全(重难点)
11.1 观察线程不安全
代码1:
public class ThreadDemo15 {
public static int count=0;
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
for(int i=0;i<100000;i++){
count++;
}
}
};
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
执行结果:
代码2:
public class ThreadDemo15 {
public static int count=0;
public static void main(String[] args) {
Thread t1 = new Thread(){
@Override
public void run() {
for(int i=0;i<50000;i++){
count++;
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
for(int i=0;i<50000;i++){
count++;
}
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
执行结果:
两个代码的结果分析:
当用一个线程进行100000次的 count++ 时,得到了的结果就是100000,但是当分成两个线程分别进行 50000 次的 count++ 时,就会产生一些奇怪的结果,而且每次执行的结果还都不确定。
这就是因为线程不安全引起的,代码而的线程在执行过程中可能的过程有:
情况分析(以情况3为例,假设两个线程在两个cpu上执行):
可以看出,虽然执行了两次count++,但是最终的结果只增加了1,这就是线程不安全引起的。
同理,所以上面6种情况,最终内存上的结果分别是:2,2,1,1,1,1.
11.2 线程不安全的原因
还有一点就是:指令重排序——CPU的优化引起
12. synchronized关键字【面试重灾区】
0. synchronized 锁的是什么?
两个线程竞争同一把锁, 才会产生阻塞等待,两个线程分别尝试获取两把不同的锁, 不会产生竞争
1. synchronized的特性1——互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象synchronized 就会阻塞等待。
**加锁**——进入synchronized修饰的代码块
**解锁**——退出synchronized修饰的代码块
synchronized用的锁是存在Java对象头里的。可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态:
2. synchronized的特性2——刷新内存
从这可以看出,当程序中用了synchronized之后,程序的执行速度肯定是变慢了,即用了synchronized之后就与”高性能“无缘了。
3. synchronized的特性3——可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
可重入 与 不可重入:
Java里面的锁是可重入锁,在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
- 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
4. synchronized的本质(关键点)
synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.
5. synchronized 使用示例
- 直接修饰普通方法:锁的是SynchronizedDemo 对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
- 修饰静态方法:锁的是SynchronizedDemo 类对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
- 修饰代码块:明确指定锁那个对象
锁当前对象:
public class SynchronizedDemo {
public void method() {
synchronized (this) {
//锁当前对象
}
}
}
锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
13. Java标准库中的线程安全类
-
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施,例如:
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder -
但是还有一些是线程安全的. 使用了一些锁机制来控制。例如:
Vector (不推荐使用)
HashTable (不推荐使用)
ConcurrentHashMap
StringBuffer -
还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的
String
14. volatile 关键字
14.1. volatile的作用
volatile可以保证内存的可见性
14.2. volatile如何保证内存的可见性
因为编译器为了提高工作效率,对于代码对应的指令并不会完全按照指令进行,它可能会将指令中的一部分省略,以达到优化的目的,这就使得内存不能及时地刷新。volatile就通过限制优化,以保证内存地可见性。
14.3. 代码演示
代码:
public class ThreadDemo22 {
public static int flag = 1;
//public static volatile int flag = 1;
public static void main(String[] args) {
Thread t1 =new Thread(){
@Override
public void run() {
while(flag!=0){
}
System.out.println("循环结束!");
}
};
Thread t2 = new Thread(){
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
flag = scanner.nextInt();
}
};
t1.start();
t2.start();
}
}
运行结果分析:
【面试】synchronized和volatile的区别
- synchronized用来修饰一段代码和方法,而volatile只是用来修饰变量。
- synchronized不仅能够保证内存的可见性,还可以实现程序的原子性操作。而volatile只能够保证内存的可见性,不能保证原子性。
- synchronized可以造成线程的阻塞,而volatile不会。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
15. wait 和 notify
wait ——等待
notify——通知
用来协同多个线程之间的执行顺序
wait的作用
- 让当前线程阻塞等待(将这个线程的PCB从就绪队列拿到等待队列中)并准备接收通知(来自notify)。
- 释放当前锁。想要使用wait/notify,必须搭配synchronized,需要先获取锁,才能谈得上释放锁。
- 满足一定条件时,重新尝试获取这个锁。
注意:1,2 是要原子完成的。
notify的使用
- notify一次只能唤醒一个线程,如果有多个线程在等待中,调用notify就只能随机唤醒其中一个。notifyAll一次可以唤醒多个线程。
- notify的作用唤醒线程,但是调用notify本身的线程并不会立即释放锁,而要等到synchronized代码块执行完之后才能释放锁。
wait 和 notify使用过程中的要保持对象一致性
比如:
如果在线程1中调用对象1的wait方法。 那么在线程2中,也调用对象1的notify才能唤醒线程1
代码演示
public class ThreadDemo18 {
static class WaitTask implements Runnable{
private Object locker=null;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker){
System.out.println("wait 开始");
try {
//进行 wait 的线程
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait 结束");
}
}
}
static class NotifyTask implements Runnable{
private Object locker=null;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker){
//进行 notify 的线程
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(3000);
t2.start();
}
}
运行结果分析:
16. 单例模式
16.0 什么是单例模式
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例,单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.
饿汉:类加载的同时, 创建实例.
懒汉:类加载的时候不创建实例. 第一次使用的时候才创建实例.
16.1 饿汉模式
class Singleton{
//把构造方法设置为 private ,防止在类外面调用构造方法,
//也禁止了调用者在其他类再创建其他实例的机会
private Singleton(){
}
//利用static,在类加载阶段,在这里直接就创建实例
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
public class ThreadDemo21{
public static void main(String[] args) {
Singleton instance =Singleton.getInstance();
}
}
16.2 懒汉模式
class SingleTon{
private SingleTon() {
}
private static SingleTon instance = null;
public static SingleTon getInstance(){
if (instance==null){
instance = new SingleTon();
}
return instance;
}
}
public class ThreadDemo20 {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
}
}
16.3 两个模式的线程安全性分析
饿汉模式:
懒汉模式:
16.4 懒汉模式中线程不安全的改进方案
- 分析:懒汉模式中,引起线程不安全的原因是:在进行如下代码时,是非原子性的:
if (instance==null){
instance = new SingleTon();
}
解决方法很简单,就是给这个操作加锁,如下所示:
synchronized (SingleTon.class){
if (instance==null){
instance = new SingleTon();
}
}
- 分析:instance的定义方式,也可能会导致内存可见性的问题
应该将:
private static SingleTon instance = null;
改成:
private static volatile SingleTon instance = null;
16.4 线程安全时,懒汉模式的效率问题
分析:在懒汉模式中,只要在第一批的并发线程中,才会出现线程安全问题,而后续的操作中,并不会有线程安全问题,在16.3的加锁操作之后,就会在每次调用SingleTon时,都会进行加锁操作,导致程序效率不高。
解决方法:加上判断语句,只在第一批的并发线程中进行加锁操作。
代码如下:
if(instance==null){
synchronized (SingleTon.class){
if (instance==null){
instance = new SingleTon();
}
}
}
【面试】这里的双重 if 有什么作用?
17. 阻塞式队列
17.1 什么是阻塞队列
阻塞队列是一种特殊的队列,它也遵守“先进先出”的规律。
它的特殊之处:
- 当队列满的时候,继续入队就会进行阻塞,直到有其他线程的元素被取走
- 当队列为空时,继续出队就会进行阻塞,直到其他线程有元素入队之后
17.2 生产者消费者模型
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。即通过一个容器来解决生产者和消费者的强耦合问题。
- 阻塞队列相当于一个缓冲区,平衡了生产者与消费者之间的处理能力
- 阻塞队列也能使生产者和消费者之间 解耦
举个例子
这里得入口服务器相当于生产者,专用服务器相当于消费者
模拟实现阻塞队列
- 通过 “循环队列” 的方式来实现.
- 使用 synchronized 进行加锁控制.
- put 插入元素的时候, 判定如果队列满了, 就进行wait. (注意, 要在循环中进行 wait. 被唤醒时不一 定队列就不满了, 因为同时可能是唤醒了多个线程).
- take取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
public class MyBlockingQueue {
int head=0;
int tail=0;
int size=0;
int [] arr;
Object locker = new Object();
public MyBlockingQueue(){
arr = new int[1000];
}
public void put(int elem) throws InterruptedException {
synchronized (locker){
while(size==arr.length){
//阻塞
locker.wait();
}
if(tail>=arr.length){
tail=0;
}
arr[tail]=elem;
size++;
tail++;
locker.notify();
}
}
public int take() throws InterruptedException {
int ret;
synchronized (locker){
while(size==0){
//阻塞
locker.wait();
}
ret = arr[head];
head++;
if(head>= arr.length){
head=0;
}
size--;
locker.notify();
}
return ret;
}
}
代码分析(关于while)
关于阻塞分析:
测试代码:
public class Test_MyBlockingQueue {
public static void main(String[] args) {
MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
Thread producer = new Thread(){
@Override
public void run() {
for(int i =0;i<100;i++){
System.out.println("生产元素:"+i);
try {
myBlockingQueue.put(i);
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//消费者
Thread customer = new Thread(){
@Override
public void run() {
while(true){
try {
int e = myBlockingQueue.take();
System.out.println("消费元素:"+e);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
producer.start();
customer.start();
try {
producer.join();
customer.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
【面试】为什么wait和notify要强制放到synchronized中使用?
class MyBlockingQueue {
// 用来保存数据的集合
Queue<String> queue = new LinkedList<>();
public void put(String data) {
// 队列加入数据
queue.add(data);
// 唤醒线程继续执行(这里的线程指的是执行 take 方法的线程)
notify(); // ③
}
public String take() throws InterruptedException {
// 使用 while 判断是否有数据(这里使用 while 而非 if 是为了防止虚假唤醒)
while (queue.isEmpty()) { // ①
// 没有任务,先阻塞等待
wait(); // ②
}
return queue.remove(); // 返回数据
}
}
class MyBlockingQueue {
// 用来保存任务的集合
Queue<String> queue = new LinkedList<>();
public void put(String data) {
synchronized (MyBlockingQueue.class) {
// 队列加入数据
queue.add(data);
// 为了防止 take 方法阻塞休眠,这里需要调用唤醒方法 notify
notify(); // ③
}
}
public String take() throws InterruptedException {
synchronized (MyBlockingQueue.class) {
// 使用 while 判断是否有数据(这里使用 while 而非 if 是为了防止虚假唤醒)
while (queue.isEmpty()) { // ①
// 没有任务,先阻塞等待
wait(); // ②
}
}
return queue.remove(); // 返回数据
}
}
我的理解:在一定程度上保证原子性,当满足wait或者notify条件时,在去调用wait或者notify的过程中,条件不会发送变化。
18. 定时器
18.0 什么是定时器
定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.
18.1 模拟实现定时器
public class ThreadDemo26{
//使用这个类来描述这个任务
static class Task implements Comparable<Task>{
//command表示这个任务
private Runnable commmand;
//time表示这个任务啥时候结束
//这里的time使用ms级的时间戳表示
private long time;
//约定time是一个时间差
//希望this.time保存一个绝对的时间(ms级的时间戳)
public Task(Runnable commmand,long time){
this.commmand=commmand;
this.time=System.currentTimeMillis()+time;
}
public void run(){
commmand.run();
}
@Override
public int compareTo(Task o) {
return (int)(this.time-o.time);
}
}
static class Timer{
//使用这个带优先级版本的阻塞队列来组织这些任务
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
//使用locker对象来解决忙等问题
Object locker = new Object();
public void schedule (Runnable command,long delay){
Task task = new Task(command,delay);
//queue.put(task);
//每次插入新的任务都唤醒一下扫描线程,让扫描线程能够重新计算 wait 的时间,保证新的任务也不会错过
queue.put(task);
synchronized (locker){
locker.notify();
}
}
public Timer(){//当创建Timer对象时,就会创建线程 t
//创建扫描线程,这个扫描线程就来判定当前的任务,看看是不是已经到时间能执行了
Thread t = new Thread(){
@Override
public void run() {
while(true){
//取出队列首元素,判断时间是不是到了
try {
synchronized (locker){
Task task = queue.take();
long curTime = System.currentTimeMillis();
if(task.time>curTime){
//时间还没到,重新插入回去
//queue.put(task);
//根据时间差进行一个等待
queue.put(task);
locker.wait(task.time-curTime);
}else {
//时间到了
task.run();
System.out.println();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
break;//如果出现interrupt方法就能退出线程
}
}
}
};
t.start();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("程序启动");
Timer timer = new Timer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
},1000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
},5000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
},10000);
}
}
19. 线程池
19.0 什么是线程池
线程池就相当于一个存放线程的池子。当我们使用线程的时候,我们可以直接去线程池中拿到线程,这就省去了创建线程的消耗;当我们不需要这个线程的时候,我们并不一定要将这个线程销毁,可以将这个线程放入线程池,方便下一次的使用。
之前,因为进程在并发编程时频繁的创建、销毁进程的开销是比较大的,所以我们引入了更轻量化的线程。但是,为了追求更高的并发编程的效率,我们就引入线程池。
线程池提高并发编程的底层原理:因为线程的创建以及销毁都是需要用户态和内核态的切换来完成的,这就是导致效率不高的原因;当引入线程池之后,线程的使用就不需要创建和销毁,因为也就不需要进行用户态和内核态的切换操作,从而提高了并发效率。
【面试】ThreadPoolExecutor的构造方法的参数都是啥意思?
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:除过核心线程的其他线程,在不被使用的多场时间里面保持存在,超过时间后,就得挪出线程池。
- unit:时间单位——ms,s,minute
- workQueue:一个阻塞队列,描述了线程池需要执行的任务
- ThreadFactory:线程的创建方式
- handler:拒绝策略。当任务满了的时候,又来了新任务时的解决策略,比如:丢弃最新的任务,丢弃最老的任务,阻塞队列,抛出异常
19.1 线程池的使用
由上可以知道,ThreadPoolExecutor使用起来是比较复杂的,于是标准库提供了Executors这个类,相当于对ThreadPoolExecutor又进行了一层封装。
- 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
- 返回值类型为 ExecutorService
- 通过 ExecutorService.submit 可以注册一个任务到线程池中.
public class ThreadDemo27 {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
}
}
- Excutors 创建线程池的几种方式:
- newFixedThreadPool: 创建固定线程数的线程池
- newCachedThreadPool: 创建线程数目动态增长的线程池.
- newSingleThreadExecutor: 创建只包含单个线程的线程池.
- newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor 类的封装
19.2 线程池的模拟实现
实现逻辑:
- 线程池对外提供的功能就是,外部传入任务就行
- 在插入任务的同时,创建线程,保证线程总数不超过约定的最大线程数。
- 对于每个线程而言,他要不断地进行queue.take(),即不断地获取任务去执行。
线程池核心代码:
class Worker extends Thread{
private BlockingQueue<Runnable> queue;
public Worker(BlockingQueue<Runnable> queue){
this.queue=queue;
}
@Override
public void run() {
while(true){
try {
Runnable task=queue.take();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class MyThreadPool {
//最大线程数
int maxThreadNum;
public MyThreadPool(int maxThreadNum){
this.maxThreadNum =maxThreadNum;
}
//存储任务
private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();
//存储线程
private List<Thread> workerList = new ArrayList<>();
public void submit(Runnable task) throws InterruptedException {
if(workerList.size()<maxThreadNum){
//如果线程数没有到上线,就继续创建线程
Worker worker = new Worker(queue);
worker.start();
workerList.add(worker);
}
queue.put(task);
}
}
测试代码:
public class MyThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(2);
for(int i=0;i<20;i++){
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
});
}
}
}
【面试】描述一下线程池的执行流程和拒绝策略有哪些?
20. 工厂模式
工厂模式也是一种设计模式,和单例模式是并列的关系。
工厂模式存在的意义就是给构造方法填坑
比如:
对于上面的例子,想直接用构造方法是不行的,这时就得用到其他的方法来构造实例了,这样用来构造实例的方法,称为“工厂方法”。