一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码
操作系统支持多任务,程序猿就需要并发(并行+并发)编程,通过多进程,可以实现并发编程,但是如果需要频繁创建和销毁进程,就会占用很大的资源,对资源的申请和释放是一个低效的操作
解决这个问题的方法:
就是一个类似于字符串常量池,数据库连接池一类的东西。当我们释放一个进程时,并不是真的把他的资源释放,而是把它放到池里面,下次再要用的时候,就直接从里面拿出来就行了,这样就跳过了申请和释放的过程。但是进程不用的时候它也消耗资源,进程很多的时候,吃的资源就多,那么其它正在使用进程调度的进程所能调用的资源就减少了。
线程比进程更轻量,创建线程比创建进程更快,销毁线程比销毁进程更快,调度线程比调度进程更快。
进程的重量在资源申请释放,线程是包含在进程中的,每个进程至少有一个线程存在,即主线程,一个进程可以有很多个线程,每个线程共用一份系统资源(1.内存空间 2.文件描述符表),只有在进程启动创建第一个线程的时候,需要花成本去申请系统资源,一旦进程(第一个线程)创建完毕,此后,后续再创建的线程,就不需要再申请资源了,创建/销毁的效率就提高了不少。进程是系统分配资源的最小单位,线程是系统调度的最小单位。
注意: 线程不是越多越好,CPU的核心数是固定的,线程多了也用不上,只能在那等着,此时程序的效率没有进一步的提升,反而可能会下降,因为调度本身也会有开销。此时就是进行调度,调度上了一个线程,势必会挤下一个线程,总并发程度仍然是固定不变的。真正有效果的,是再搞一个CPU(再搞一个主机),即分布式系统。
- 进程包含线程。
- 进程有自己独立的内存空间和文件描述符表,同一个进程中的多个线程之间,共享同一份地址空间和文件描述符表。
- 进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位。
- 进程之间具有独立性,一个进程挂了,不会影响到别的进程;同一进程里多个线程之间,一个线程挂了,可能会把整个进程带走,影响到其它线程的。
- 进程的优先级
- 进程的状态 -> 1. 就绪状态 2. 阻塞状态
- 进程的记账信息
- 进程的上下文
在 Java 标准库中,就提供了一个 Thread 类,来 表示 / 操作 线程。
Thread 类也可以视为是 Java 标准库提供的 API(API:Thread 类提供的方法 和 类)。
Tread能够表示一个线程。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread t");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();// 先创建MyThread实例,t的引用实际上是指向子类的实例
t.start();// 启动线程,在进程中搞了另外一个流水线,新的流水线开始并发的执行另一个逻辑了,也就是run方法里面的代码
}
}
接下来看一个更加直接表示线程之间是并发执行的例子
class MyThread extends Thread {
@Override
public void run() {
while(true) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();// 先创建MyThread实例,t的引用实际上是指向子类的实例
t.start();// 启动线程,在进程中搞了另外一个流水线,新的流水线开始并发的执行另一个逻辑了,也就是run方法里面的代码
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
sleep方法会抛异常:
运行效果:
t就是我们创建的线程,在一个进程中,至少会有一个线程,在一个java进程中,就会有一个调用main方法的线程,这个线程是系统自己创建的,也可以称为是主线程,所以 t 线程和main线程就是属于并发执行的关系,从宏观上讲是并行+并发的(取决于内部的调度),我们是区分不了到底是哪一个的,多个线程在CPU上调度执行顺序是随机的,而内部的调度虽然有优先级,但是这个优先级对于系统来说,只是建议,至于要怎么调度,还是得看系统的(阳奉阴违)。
这种上面写的就是。
class MyRunnable implements Runnable {
@Override
public void run() {
while(true) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t = new Thread(myRunnable);
t.start();
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
while(true) {
System.out.println("hello t");
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();
}
}
}
}
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("hello t");
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();
}
}
}
}
其实,创建线程最推荐的写法,最简单最直观的写法就是:
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread( () -> {
while(true) {
System.out.println("hello t");
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();
}
}
}
}
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo31 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 这只是创建个任务
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
// 还需要找个人来完成这个任务(线程)
// Thread 不能直接传 callable,需要再包装一层
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
System.out.println(futureTask.get());
}
}
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
System.out.println("hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "我的线程");
t.start();
}
}
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello t");
});
t.start();
try {
// 上述 t 线程没有进行任何循环和 sleep, 意味着里面的代码会迅速执行完毕.
// main 线程如果 sleep 结束, 此时 t 基本上就是已经执行完了的状态. 此时 t 对象还在
// 但是在 系统中 对应的线程已经结束了.
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.isAlive());
}
}
public class ThreadDemo8 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
}
});
// 默认是前台线程, 也就是设为 false
// 此时这个线程会阻止进程结束
// 改成 true 变成后台线程. 不影响进程的结束.
t.setDaemon(true);
t.start();
}
}
作用功能不同:
run方法的作用是描述线程具体要执行的任务;
start方法的作用是真正的去申请系统线程
运行结果不同:
run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。
中断这里就是字面意思,就是让一个线程停下来,线程的终止,本质上来说,让一个线程终止,办法就一种,让线程的入口方法执行完毕,(return ,抛出异常 …)
public class ThreadDemo10 {
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();
}
}
System.out.println("t 线程终止");
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
isQuit = true;
}
}
此处因为多个线程共用一个虚拟地址空间!因此,main 线程 修改的 isQuit 和 t 线程判定 的 isQuit 是同一个值。但是,如果是在进程的那种情况下,在不同的虚拟地址的情况下,这种写法就会失效
不能,因为lambda表达式的变量捕获规则,捕获的变量必须是final或者“实际final”的,这里后面isQuit会被修改,所以只能作为外面的成员变量。
public class ThreadDemo11 {
public static void main(String[] args) {
Thread t = new Thread( () -> {
// currentThread 是获取到当前线程实例.
// 此处 currentThread 得到的对象就是 t
// isInterrupted 就是 t 对象里自带的一个标志位.
while(!Thread.currentThread().isInterrupted()) {
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 内部的标志位给设置成 true
t.interrupt();
}
}
Thread currentThread().isInterrupted() 【这是一个实例方法,其中 currentThread 能够获取当前线程的实例】
运行后此时发现:3s后,调用t.interrupt()方法的时候,线程没有正在的就结束了,而是打印了异常信息,又继续执行了,也就是说, t.interrupt() 不仅仅是针对 while循环的条件(标记位) 进行操作,它还触发一个异常
所以如果需要结束循环,就得在catch中搞个break
特殊情况:
目的就是为了让线程自身能够对于线程何时结束,有一个更明确的控制。
当前,interrupt方法效果,不是让线程立即结束,而是告诉她你该结束了,至于他是否要结束,立即结束还是等会结束,都是代码来灵活控制的,interrupt只是通知而不是命令。
public class ThreadDemo12 {
public static void main(String[] args) {
Thread t = new Thread( () -> {
System.out.println("hello t");
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello main");
}
}
运行结果就一定是先打印hello t,再打印hello main
在main中调用t.join(),就是执行到join时让main线程等待 t 线程结束后再执行下去,在t.join执行的时候,如果 t 线程还没结束,mian线程就会阻塞等待(Blocking),但是其它线程不受影响,代码走到这一行就停下来,当前这个线程不参与cpu的调度执行了。但是如果mian线程调用t.join时,t 已经结束了,此时join不会阻塞,会继续执行下去
public class ThreadDemo13 {
public static void main(String[] args) {
Thread t = new Thread( () -> {
while(true) {
}
});
System.out.println(t.getState());
t.start();
}
}
把 Thread 对象创建好了,但是没有调用start方法。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread( () -> {
});
t.start();
Thread.sleep(2000);
System.out.println(t.getState());
}
操作系统中的线程已经执行完毕,销毁了。但是 Thread 对象还在,此时获取的状态就是 TERMINATED
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread( () -> {
while(true) {
}
});
t.start();
System.out.println(t.getState());
}
正在执行或者在就绪状态的,随时可以别调度到CPU上的
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread( () -> {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
代码中调用了 sleep、join(超时时间),就会进入到 TIMED_WAITING。它的意思就是当前的线程在一定的时间内,是阻塞状态。
当前线程在等待 锁,导致到了阻塞(阻塞状态之一),一般是在我们使用 synchronized 来去加锁的时候,可能会触发这种状态。
当前线程在等待 唤醒,导致到了阻塞(阻塞状态之一),一般是在我们使用 wait 来等待唤醒的时候,可能会触发这种状态。
某个代码,在多线程环境下执行,会出bug,就是线程不安全,本质上是因为线程之间的调度顺序是不确定的
class Counter {
private int count = 0;
public void add() {
count++;
}
public int get() {
return count;
}
}
public class ThreadDemo14 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.get());
}
}
每次运行的结果都是随机的,因为
既然count++不是原子的,那么有没有办法让他变为原子的,有的:
"锁"就能保证"原子性"的效果,一旦某个线程加上锁之后,其它线程也想加锁,就不能直接加上,需要阻塞等待,一直等到拿到锁的线程释放了锁为止。线程调度,是抢占式执行,导致了进程的调度是随机的。
class Counter2 {
private int count = 0;
public void add() {
synchronized(this) {
count++;
}
}
public int getCount() {
return count;
}
}
public class ThreadDemo15 {
public static void main(String[] args) throws InterruptedException {
Counter2 counter2 = new Counter2();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter2.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter2.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter2.getCount());
}
}
加锁还能怎么写:
synchronized public void add() {
count++;
}
private Object locker = new Object();
public void add() {
synchronized (locker ) {
count++;
}
}
public class ThreadDemo16 {
public static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flag == 0) {
}
System.out.println("线程 t1 结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
这里线程并没有结束,这就是内存可见性的锅:
编译器优化导致了这个问题,如果是在单线程中,编译器的优化是不会有问题的,但是在多线程中,就不一定了,出现了误判导致了bug。
所谓内存可见性,就是在多线程环境下,编译器对代码的优化,产生了误判,从而引起了bug。
所以我们的处理方法就是让编译器对这个场景暂停优化
被volatile修饰的变量,此时编译器就会禁止上述优化,能够保证每次都是从内存从新读取数据
- volatile不保证原子性
- volatile适用的场景,是一个线程读,一个线程写的情况
- synchronized则是多个线程写
volatile这个效果,称为"保证内存可见性"
指令重排序,也是编译器优化的策略,调整了代码执行的顺序,让程序更高效,前提也是保证整体逻辑不变。还是对于单线程容易保证,多线程不好说。
这里加锁就是其中一个在执行的时候,另外一个不执行,就不会出现先执行1和3,在执行t2的情况,只能有一块执行,而使用volatile就是让new操作严格按照1,2,3的顺序执行,也不会出错。
volatile 不能保证原子性
wait和notify都是属于Object类的方法,只要你是个类对象,不是内置类型(基本数据类型),就能使用这两个方法,调用wait方法就会阻塞等待,等到notify的通知才继续
举个例子:一群人去ATM取钱,第一个老哥进去后,发现ATM中的钱被取空了,于是这个老哥只能在那里干等,其它人也进不去,这叫线程饿死。如果使用wait和notify,这个老哥就会出来,等到有人把钱存进去之后,notify通知他后,他在进去取钱。
notify也是需要放到锁中使用的,必须要先执行wait然后notify,此时才有效果,如果还没有wait就notify,就是空打一炮,此时wait无法唤醒,代码不会出现其它异常,但是notify就没有了作用,代码功能就不能正常执行了。
public class ThreadDemo18 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(() -> {
try {
System.out.println("wait 开始");
synchronized (object) {
object.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait 结束");
});
Thread t2 = new Thread(() -> {
System.out.println("notify 开始");
synchronized (object) {
object.notify();
}
System.out.println("notify 结束");
});
t1.start();
Thread.sleep(1000);
t2.start();
}
}
wait可以带参数,就不会是死等的状态了
唤醒操作,还有一个叫notifyAll,会把所有的线程唤醒同时去竞争锁
可以有多个线程等待同一个对象,如果调用notify,就只会随机唤醒一个
他们最大的区别就是初心不同,就是设计者东西是来解决什么问题的,wait解决的是线程之间的顺序控制,而sleep单纯就是让当前线程休眠一会
共同点:
都是使线程暂停一段时间的方法。
不同点:
wait是Object类中的一个方法,sleep是Thread类中的一个方法;
wait必须在synchronized修饰的代码块或方法中使用,sleep方法可以在任何位置使用;
wait被调用后当前线程进入BLOCK状态并释放锁,并可以通过notify和notifyAll方法进行唤醒;sleep被调用后当前线程进入TIMED_WAIT状态,不涉及锁相关的操作;
synchronized 的工作过程
1、 获得互斥锁
2、 从主内存拷贝变量的最新副本到工作的内存
3、 执行代码
4、 将更改后的共享变量的值刷新到主内存
5、 释放互斥锁
这也是为什么前面 synchronized 可以解决 内存可见性的线程问题。
直观来说:同一个线程针对同一个锁,连续加锁两次。
如果出现了死锁,就是不可重入,如果不会死锁,就是可重入。
一个程序中,某个类,只创建出一个实例(一个对象),不能创建多个对象。
比如:JDBC编程中的DataSource,就是单例的。大部分跟数据有关的东西,服务器里面只存一份,那么就都可以使用单例模式来进行表示。
比如:你吃完饭用了4个碗,对于饿汉模式:就会把这4个碗都给洗了;而懒汉模式,就会等到下次要吃饭的时候,要用到2个碗就只洗2个。对于打开一个有10G的文件来说,饿汉模式会把一整个文件都给加载了,而懒汉模式,就会在你看到哪里再加载到哪里,就会很快,所以这种模式下,懒汉模式的效率更快。
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() { }
}
public class ThreadDemo19 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
}
}
饿汉模式是比较着急的,在类加载的时候,就会把instance对象创建出来
懒汉模式来实现单例:非必要,不创建
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {}
}
public class ThreadDemo20 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
}
}
但是加锁,又是一个比较低效的操作,加锁可能设计到阻塞等待,所以非必要,不加锁。
单例模式线程安全问题
饿汉模式,天然就是安全的,只是读操作
懒汉模式,不安全的,有读还有写
- 加锁,把if和new变成原子操作
- 双重if,减少不必要的加锁操作
- 使用volatile禁止指令重排序,保证后续线程肯定拿到的是完整对象
class SingletonLazy {
volatile private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {}
}
public class ThreadDemo20 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
}
}
阻塞队列和普通的队列有着相同的特性:先进先出
但是,对比于普通队列,阻塞队列有着其它的功能:
阻塞队列,带有阻塞特性,是线程安全的
- 如果队列为空,尝试出队列,就会阻塞等待,等待队列不为空为止
- 如果队列满, 尝试入队列,也会阻塞等待,等待到队列不满为止
这个阻塞队列,尤其是写多线程代码时,多个线程之间进行数据交互,可以使用阻塞队列简化代码编写。Java标准库,就提供了阻塞队列的使用。其中入队方法为put,出队方法为take,和队列的入队出队不大一样。
下面我们写一个"生产者消费者模型"多线程使用的阻塞队列
什么是生产者消费者模型:包饺子,如果4个人同时需要和面,擀面皮,包饺子的话,一般擀面杖只有一根,然后他们会竞争这根擀面杖,效率就会很低,即使有4根擀面杖,来回切换也会使得效率低下,更高效的做法是1个人负责擀面皮,另外3个负责包饺子,而中间那个存放饺子皮的盖帘,这个交易场所,就是阻塞队列。
能解决的问题有很多,最主要的两个方面就是:
- 可以让上下游模块之间,更好的“解耦合”
- 削峰填谷
耦合:简单来说就是牵一发而动全身,写代码要追求低耦合。
内聚:相关联的代码,分门别类的规划起来。
假设有两个服务器:A作为入口服务器直接接收用户的网络请求,B作为应用服务器,来给A提供一些数据。
此时如果A和B直接通信,此时就是耦合比较高的情况,如果B挂了,就会导致A也挂了,另外如果再加个C,此时对于A有一个较大的调整,这样就是高耦合。
此时如果引入生产者消费者模型,耦合就降低了
A作为入口服务器来说,起到的效果就是调用一下和其它服务器把结果汇总
B是有具体业务的,工作压力承担大,单个请求吃的资源就多,容易挂
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadDemo22 {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
Thread t1 = new Thread(() -> {
while (true) {
try {
int value = queue.take();
System.out.println("消费元素: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread t2 = new Thread(() -> {
int value = 0;
while (true) {
System.out.println("生成元素: " + value);
try {
queue.put(value);
value++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
}
}
实现阻塞队列
- 先实现一个普通队列
- 加上线程安全
- 加上阻塞功能
class MyBlockingQueue {
private int[] elems = new int[1000];
volatile private int head = 0;
volatile private int tail = 0;
volatile private int size = 0;
synchronized public void put(int elem) throws InterruptedException {
if(size == elems.length) {
this.wait();
}
elems[tail] = elem;
tail++;
if(tail == elems.length) {
tail = 0;
}
size++;
this.notify();
}
synchronized public Integer take() throws InterruptedException {
if(size == 0) {
this.wait();
}
int elem = elems[head];
head++;
if(head == elems.length) {
head = 0;
}
size--;
this.notify();
return elem;
}
}
public class ThreadDemo23 {
public static void main(String[] args) {
MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
}
}
class MyBlockingQueue {
private int[] elems = new int[1000];
volatile private int head = 0;
volatile private int tail = 0;
volatile private int size = 0;
synchronized public void put(int elem) throws InterruptedException {
while (size == elems.length) {
this.wait();
}
elems[tail] = elem;
tail++;
if(tail == elems.length) {
tail = 0;
}
size++;
this.notify();
}
synchronized public Integer take() throws InterruptedException {
while (size == 0) {
this.wait();
}
int elem = elems[head];
head++;
if(head == elems.length) {
head = 0;
}
size--;
this.notify();
return elem;
}
}
public class ThreadDemo23 {
public static void main(String[] args) {
MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
Thread t1 = new Thread(() -> {
while(true) {
try {
int value = myBlockingQueue.take();
System.out.println("消费 : " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
int value = 0;
while(true) {
try {
myBlockingQueue.put(value);
System.out.println("生产 :" + value);
value++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
定时器就是闹钟,设定一个时间,当时间到,就可以执行一个指定的代码
java标准库中,提供了定时器:Timer,是java.util包下的
Timer它的核心方法就只有一个:schedule
public class ThreadDemo26 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello1");
}
}, 2000);
System.out.println("hello0");
}
}
先打印了hello0,过了2s,再打印出的hello1,但是程序并没有结束
这是因为Timer里头内置了线程(还是前台线程,会阻止线程结束),来负责执行注册任务的,当我们代码走到任务结束了,Timer的内部这个线程还在等,等待其它任务加进来,就继续执行。
定时器,内部管理的不仅仅是一个任务,可以管理很多任务的
我们从MyTimer类的内部需要什么入手
- 管理很多的任务
- 执行时间到了的任务
任务又可以分为:
- 描述任务(创建一个类来表示一个定时器的任务)
- 组织任务(使用一定的数据结构进行数据组织)
- 执行时间到了的任务
组织的任务需要的数据结构:核心的数据结构就堆(PriorityQueue),因为需要线程安全,所以有个更加适合的就是PriorityBlockingQueue,带有阻塞功能的优先级队列
import java.util.concurrent.PriorityBlockingQueue;
class MyTimer {
private PriorityBlockingQueue<MyTimerTask> priorityBlockingQueue = new PriorityBlockingQueue<>();
private Object block = new Object();
public void schedule(Runnable runnable, long delay) {
MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
priorityBlockingQueue.put(myTimerTask);
}
public MyTimer() {
Thread t = new Thread(() -> {
while(true) {
try {
MyTimerTask myTimerTask = priorityBlockingQueue.take();
long curTime = System.currentTimeMillis();
if(myTimerTask.delay <= curTime) {
myTimerTask.run();
}else {
priorityBlockingQueue.put(myTimerTask);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
class MyTimerTask {
public Runnable runnable;
public long delay;
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.delay = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
}
写到这里,我们遗漏了两个重要的问题:
import java.util.concurrent.PriorityBlockingQueue;
class MyTimer {
private PriorityBlockingQueue<MyTimerTask> priorityBlockingQueue = new PriorityBlockingQueue<>();
private Object block = new Object();
public void schedule(Runnable runnable, long delay) {
MyTimerTask myTimerTask = new MyTimerTask(runnable, delay);
priorityBlockingQueue.put(myTimerTask);
synchronized (block) {
block.notify();
}
}
public MyTimer() {
Thread t = new Thread(() -> {
while(true) {
try {
synchronized (block) {
MyTimerTask myTimerTask = priorityBlockingQueue.take();
long curTime = System.currentTimeMillis();
if(myTimerTask.delay <= curTime) {
myTimerTask.run();
}else {
priorityBlockingQueue.put(myTimerTask);
block.wait(myTimerTask.delay - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
class MyTimerTask implements Comparable<MyTimerTask> {
public Runnable runnable;
public long delay;
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.delay = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.delay - o.delay);
}
}
public class ThreadDemo27 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello5");
}
}, 5000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello4");
}
}, 4000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello3");
}
}, 3000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello2");
}
}, 2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello1");
}
}, 1000);
System.out.println("hello0");
}
}
线程池的执行流程:
1.当新加入一个任务时,先判断当前线程数是否大于核心线程数,如果结果为 false,则新建线程并执行任务;
2.如果结果为 true,则判断任务队列是否已满,如果结果为 false,则把任务添加到任务队列中等待线程执行
3.如果结果为 true,则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务
4.如果结果为 true,执行拒绝策略
提前把线程准备好,创建线程不是从系统申请,而是从池子里拿,线程不用了,也是还给池子
池的目的:就是为了提高效率,线程的创建,虽然比进程轻量,但是在频繁创建的情况下,开销也是不可忽略的。所以希望还能够进一步提高效率:1. 协程(轻量级线程),不过java标准库还不支持
2. 线程池
为啥从池子里拿线程比从系统创建线程要更加高效?
从池子里拿线程,纯粹的用户态操作
从系统创建线程,涉及到用户态和内核态之间的切换
真正的创建是要在内核态完成的
线程池的优点:
降低资源消耗:减少线程的创建和销毁带来的性能开销。
提高响应速度:当任务来时可以直接使用,不用等待线程创建
可管理性: 进行统一的分配,监控,避免大量的线程间因互相抢占系统资源导致的阻塞现象。
举个例子:你去银行办理银行卡,大厅就是用户态,柜台里就相当于内核态,有个老哥去办银行卡,工作人员要他的身份证复印件,给他两个选择,一个是在大厅的复印机自己去复印,另一个是工作人员帮他复印,很显然,如果他自己去复印,他肯定直接就去复印完然后回来,可是工作人员帮的话,可能中途还去个厕所啥的。
所以,纯用户态的操作,时间是可控的,涉及到内核操作,时间就不太可控了
在java标准库中,线程池对应的类,叫ThreadPoolExecutor,是原装的线程池对象,上述工厂方法是对这个对象的进一步封装,我们进入java的官方文档来看一下这个类
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool {
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
while(true){
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
}
public class ThreadDemo29 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int number = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello" + number);
}
});
}
}
}
当前的代码中,搞了10个线程,那么在实际开发中,一个线程池线程的数量,设置为多少合适?