操作系统中引入进程概念,是为了能够实现多个任务并发执行的效果,但是进程有一个重大的问题:频繁创建和销毁进程的成本较高,占用资源过多。
不用怕,下面就给大家引入线程的概念:
一、认识线程
1.1 概念
1.1.1
一个线程就是一个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
1.1.2 进程和线程的区别(important)
- 进程包含线程
- 进程有自己独立的内存空间和文件描述符表,同一个进程的多个线程之间共享同一份地址空间和文件描述符表
- 进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位
- 进程之间具有独立性,一个进程挂了不会影响其他进程。同一进程的多个线程之间,一个线程挂了可能会将整个进程送走,影响其他线程
下面的图片比较形象的表示出了进程和线程的关系~~
1.2 创建线程
1.2.1 方法一:继承 Thread 类
class MyThread extends Thread { //Java标准库提供了一个Thread类能够表示一个线程
@Override
public void run() {
System.out.println("Hello MyThread");
}
}
public class Thread_Test1 {
public static void main(String[] args) {
Thread t = new MyThread(); //创建了MyThread实例,t引用指向子类实例
t.start(); //启动线程(在进程中搞了另一个流水线,新的流水线开始并发的执行)
}
}
上述代码涉及两个线程:1:main方法对应的线程(一个进程里至少要有一个线程),也可以称为主线程;2:通过t.start创建的新线程
下面我将调整代码,让我们更直观的体会“每个线程是一个独立的执行流”:
class MyThread2 extends Thread {
@Override
public void run() {
while(true) {
System.out.println("hello t");
try {
Thread.sleep(1000); //线程休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();//打印出当前位置的调用栈
}
}
}
}
public class Thread_Test2 {
public static void main(String[] args) {
Thread t = new MyThread2();
t.start();
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000); //线程休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();//打印出当前位置的调用栈
}
}
}
}
此时控制台:
注意:此处的交替执行并不是严格意义的,一秒后打印main还是t是不确定的!!
即 多个线程在CPU上的执行顺序是随机的
1.2.2 方法二:实现 Runnable 接口
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 Tread_Test3 {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable); //创建Tread实例时将runnable对象作为其构造函数的参数
t.start();
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();//打印出当前位置的调用栈
}
}
}
}
此时控制台:
1.2.3 其他方法
(1)继承Thread类,使用匿名内部类
public class Test_Thread4 {
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();//打印出当前位置的调用栈
}
}
}
}
(2)实现Runnable接口,使用匿名内部类
public class Test_Tread5 {
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();//打印出当前位置的调用栈
}
}
}
}
1.2.4 最推荐方法!!==> lambda表达式
简单介绍一下,lambda表达式本质上是一个匿名函数。在Java中函数一般无法脱离类,而lambda是一个例外
lambda的写法:
() -> {
}
lambda表达式创建线程:
public class Thread_Test6 {
public static void main(String[] args) {
Thread t = new Thread(() -> { //lambda表达式
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();//打印出当前位置的调用栈
}
}
}
}
二、Thread 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
2.1 Thread的常见构造方法
(后两个方法的name只是给线程起了个名字,不影响程序执行,只是为了调试的时候快速找到咱们关心的线程)
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.2 Thread 的几个常见属性
注意:
isDaemon()的结果如果是false,则是前台线程,而前台线程会阻止java进程结束,必须等所有的前台线程都执行完,Java进程才能结束。
下面简单演示一下:
public class Thread_Test7 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println("存活ing");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();//打印出当前位置的调用栈
}
}
System.out.println("即将死亡");
});
t.start();
//Thread.currentThread()表示当前正在运行的线程,getName()可以获取当前线程的名字
System.out.println(Thread.currentThread().getName()+ ": ID: " + t.getId());
System.out.println(Thread.currentThread().getName() + ": 名称: " + t.getName());
System.out.println(Thread.currentThread().getName() + ": 状态: " + t.getState());
System.out.println(Thread.currentThread().getName() + ": 优先级: " + t.getPriority());
System.out.println(Thread.currentThread().getName() + ": 后台线程: " + t.isDaemon());
System.out.println(Thread.currentThread().getName() + ": 存活: " + t.isAlive());
System.out.println(Thread.currentThread().getName() + ": 被中断: " + t.isInterrupted());
}
}
运行结果:
2.3 启动一个线程
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。而调用 start() 方法,线程才真正独立去执行了。(调用 start 方法, 才真的在操作系统的底层创建出一个线程.)
2.4 中断一个线程
本质上来说让一个线程终止就一个办法:让他的入口方法执行完毕(return,抛出异常…)!
怎么做到呢?
目前常见的有以下两种方式:
2.4.1 给线程中设置一个标志位
public class Thread_Test8 {
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();
//在主线程中修改isQuit
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();//打印出当前位置的调用栈
}
isQuit = true;
}
}
通过变量isQuit达到中断的目的
2.4.2 Thread.interrupted()
使用 thread 对象的 interrupted() 方法通知线程结束
public class Thread_Test9 {
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(); //打印出当前位置的调用栈
}
}
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把 t 内部的标志位给设置成 true
t.interrupt();
}
}
运行结果:
我们发现一个问题:3s过去了,线程并没有结束,而是抛了一个异常后继续执行了!!
为什么???
这就不得不说interrupt()的作用了:
1:设置标志位为true
2:如果该线程正在阻塞中(如正在执行sleep),就会把阻塞状态唤醒,通过抛出异常的方式让sleep立即结束
注意!!!!!!!!!!!
当sleep被唤醒,会自动把interrupt的标志位清空(true->false),这就导致下次循环仍然可以执行!!!!
那我们想要结束循环怎么办呢?
应该在catch后加个break
public class Thread_Test9 {
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) {
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
break;
}
}
});
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把 t 内部的标志位给设置成 true
t.interrupt();
}
}
注意:interrupt()方法不能让线程立即结束,而只是通知线程应该结束了,至于什么时候结束就由代码灵活控制了。
2.5 等待一个线程-join()
线程之间是并发执行的,操作系统对于线程的调度是无序的,无法判断谁先结束~
但是有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转
账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
可以使用线程等待来实现~~即join()方法:
public class Thread_Test10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello t");
});
t.start();
t.join();
System.out.println("hello main");
}
}
可以看到我们让t先执行,代码中在t.join执行的时候如果t进程还未结束,那么main线程就会发生阻塞等待,此时main线程暂时不参与cpu调度执行。(在哪个线程里调join()方法,那个方法就阻塞等待)
除此之外还可以设定最多等待多少毫秒~~
2.6 获取当前线程引用
public class Thread_Test11 {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println(t.getName());
}
}
三、线程的状态
3.1 线程的所有状态
- NEW 系统中的线程还没创建出来,只是有一个Thread对象
- TERMINATED 系统中的线程已经执行完了,Thread对象还在
- RUNNABLE 就绪状态(准备好随时可以在cpu上运行/正在cpu上运行)
- TIMED_WAITING 指定时间等待,sleep方法~~
- BLOCKED 表示等待锁出现的状态
- WAITING 使用wait方法出现的状态
获取状态要使用前面介绍的getState()方法~~:
public class Thread_Test12 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hello t");
});
t.getState(); //线程启动前获取状态~
t.start();
}
}
3.2 线程状态和状态转移的意义
四、线程安全(important)
4.1 观察线程不安全
class Counter {
private int count = 100000;
public void add() {
count++;
}
public int getCount() {
return count;
}
}
public class Thread_Test13 {
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.getCount());
}
}
这个代码本意是两个线程对同一变量各自自增5w次,我们预期结果应该是10w,但运行发现…
为什么是这样的结果呢?
这和线程调度的随机性密切相关~
count++操作本质上是三个cpu指令构成:
1:load,把内存中的数据读取到cpu寄存器中
2:add,就是把寄存器中的值进行+1运算
3:save,把寄存器中的值写回到内存中
~~而由于多线程的调度顺序是不确定的,所以实际执行过程中这两个线程操作的指令的排列组合可能有很多~
举个栗子:(左边箭头为时间轴)
4.2 线程不安全的原因
1:抢占式执行
2:多个线程修改同一变量
3:修改操作不是原子的(如果某个操作对应单个cpu指令,就是原子的)
4:内存的可见性
5:指令重排序
(后两个原因后面介绍)
五、synchronized 关键字
5.1 锁
我们想让上面例子的count++变成原子的,就要通过加锁的操作来实现~
“锁”能够起到保证“原子性”的效果~
锁的核心操作有两个:加锁和解锁
一旦某个线程加了锁,其他线程便需要阻塞等待,直到拿到锁的线程释放了锁为止~
5.2 加锁
如何加锁?
—通过synchronized关键字~实现加锁效果
当进入synchronized修饰的代码块时就会出发加锁
当出了synchronized修饰的代码块就会触发解锁
public void add() {
synchronized (this) { //使用this,谁调用的add就是对谁加锁
count++;
}
}
上述代码块中synchronized ()
的括号中的是“锁对象”即在针对那个对象加锁
()中的锁对象可以写作任何一个Object对象(内置类型不行)
如果两个线程针对同一个对象加锁,此时就会出现锁竞争(一个拿到锁,一个阻塞等待):
class Counter {
private int count = 100000;
public void add() {
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
}
public class Thread_Test13 {
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.getCount());
}
}
给上面的例子加上锁后,t1和t2这两个线程就是在竞争同一个锁对象,此时就会产生“锁竞争”(t1拿到锁,t2就得阻塞),此时就可以保证++操作是原子的,不受影响!!!
5.3 synchronized 使用示例
5.3.1 直接修饰普通方法: 以this为锁对象
synchronized public void add() {
count++;
}
5.3.2 修饰静态方法: 锁 counter类的对象(给类对象加锁)
synchronized public static void add() {
}
5.3.3 修修饰代码块: 明确指定锁哪个对象
锁当前对象:
public void add() {
synchronized (this) {
count++;
}
}
锁类对象
public static void test2() {
synchronized (Counter.class) {
}
}
六、volatile 关键字
6.1 内存可见性引起的线程不安全
先看下面代码:
import java.util.Scanner;
public class Thread_Test15 {
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();
}
}
预期的效果:通过flag== 0的条件控制t1线程中的循环,一开始t1进入循环。t2通过控制台输入一个整数,一旦输入了非零值,此时t1的循环立即结束,从而使t1线程退出
实际的效果:输入非零值后t1线程没有结束,通过jconsole仍可以看到t1线程正在执行,处于RUNNABLE状态
输入了整数1:
t1线程仍处于RUNNABLE状态
为什么会出现这个bug?------->内存可见性的锅!!!
while (flag == 0)
上面这个语句执行的过程是:
(1)load,从内存读取数据到cpu寄存器
(2)cmp,比较寄存器里的值是否为0
注意:这里load操作的时间开销远远高于cmp!!!(读寄存器比读内存快几千倍!!!,这里一秒就要执行上亿次!!!)
编译器就发现:
(1)load的开销很大
(2)每次load的结果都一样~
此时编译器就做了一个大胆的决定!:把load就给优化掉了!!(去掉了),只有第一次执行load才真正执行了,后续循环都只cmp不load(相当于复用之前寄存器里load过的值)
这样的编译器优化如果是在单线程里其判定是非常准确的!!但是多线程就不一定了!!可能导致虽然效率变高了但是结果变了(出现bug)
通过上面的例子理解:
所谓的内存可见性就是在多线程环境下,编译器对于代码优化,产生了误判从而引起了bug,进一步导致代码bug
补充:
如果代码是这样的:
while (flag == 0) {
sleep(1000);
}
那么就会按照咱们预期的进行—>因为加上sleep后运行速度变的非常慢,当循环次数下降了,这里load就不再是负担了,编译器就没必要优化了~~
6.2 volatile能保证内存可见性
volatile public static int flag = 0;
如果加上volatile关键字,就能保证每次都是重新从内存读取flag变量的值,此时就可以正常退出!!!
6.3 volatile 不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.
volatile适用的场景是一个线程读,一个线程写
synchronized适用的场景是多个线程写
6.4 volatile禁止指令重排序
指令重排序也是编译器优化的策略,让程序更高效,但前提是保持整体逻辑不变。
举一个对单线程进行指令重排序的例子:
这个优化是成功的,调整后结果不变~~
单线程下这样的优化容易保证,如果是多线程就不好说了:
下面简单演示一下:
Student s
t1:
s = new Student();
t2:
if(s != null)
s.learn();
t1中s = new Student();
大体分为三部:
(1)申请内存空间
(2)调用构造方法(初始化内存的数据)
(3)把对象的引用赋值给s(内存地址的赋值)
如果是单线程环境此处就可以进行指令重排序:(1)肯定先执行,(2)和(3)谁先谁后都可以
但是多线程环境下:假设t1按照(1)(3)(2)的顺序执行,当t1执行完(1)(3)之后,即将执行(2)时,t2开始执行,但是由于t1的(3)执行过了,这个引用已经非空了!,此时由于t1还没有经过(2)初始化,此时的learn会成啥样就不知道了,很可能产生bug!!!
可以使用volatile关键字修饰s,创建就会禁止指令重排序
Student s
t1:
volatile s = new Student();
t2:
if(s != null)
s.learn();
七、wait 和 notify
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.~
举个例子:
篮球场上的每个运动员都是独立的 “执行流” , 可以认为是一个 “线程”. 而完成一个具体的进攻得分动作, 则需要多个运动员相互配合, 按照一的顺序执行一定的动作, 线程1 先 “传球” , 线程2 才能 “扣篮”.
而使用wait和notify就可以控制线程合理配合~
7.1 wait方法
接下来观察wait方法:
public class Thread_Test16 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait之前");
object.wait();
System.out.println("wait之后");
}
}
运行:
可以看到出现了非法的锁状态异常
出现这个异常说明什么呢?—>这把锁没有获取到就尝试解锁
可能这样说不能很好理解,我们看看wait()方法的具体功能:
(1)解锁
(2)阻塞等待
(3)当收到通知的时候就唤醒,同时尝试重新获取锁
可以看到wait()方法的第一件事就是解锁,但是此时并没有获取锁,因此wait()必须写到synchronized代码块中
注意!!!:加锁的对象和wait的对象必须是同一个!!!
public class Thread_Test16 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait之前");
synchronized (object) { //加锁
object.wait();
}
System.out.println("wait之后");
}
}
这里可以看到执行到wait()时线程就进入了阻塞等待状态,想要唤醒就需要用到notify()方法~
7.2 notify()方法
观察notify()的用法:
public class Thread_Test17 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
try {
System.out.println("wait开始");
synchronized (locker) {
locker.wait();
}
System.out.println("wait结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
synchronized (locker) { //notify()也要放在synchronized中使用
System.out.println("notify开始");
locker.notify();
System.out.println("notify结束");
}
});
t2.start();
}
}
为什么会有 Thread.sleep(1000);
?—>因为必须要先执行wait(),然后notify()才有效果!!若果还没有wait()就notify()就相当于一炮打空了,此时wait无法唤醒~
在上述代码中,虽然t1是先执行的,但可以通过wait-notify控制,先让t2先执行一些逻辑。t2执行完了之后,notify唤醒t1(传球),t1再往下执行(扣篮)
7.3 notifyAll()方法
当多个线程等待同一对象时(比如t1 t2 t3 都调用object.wait();
),此时在main中调用object.notify();
会随即唤醒上面的一个线程~
如果调用的是object.notifyAll();
,此时就会把上述三个线程都唤醒
7.4 wait和sleep区别
初心不同:wait解决线程之间的顺序控制,sleep单纯是让当前线程休眠一会~
使用不同:wait要搭配锁使用,sleep则不需要~
八、多线程案例
8.1 单例模式
单例模式是设计模式的一种,而是设计模式就相当于围棋的棋谱~是前人大佬们总结出来的针对某些场景下的固定套路
有些开发场景中要求某个概念是单例(一个程序中某个类只创建一个实例/对象)的,因此就有大佬写出了单例模式~
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
单例模式具体的实现方式, 分成 “饿汉模式”(着急) 和 “懒汉模式”(从容) 两种.
8.1.1 饿汉模式
类加载的同时, 创建实例.
class A {
//唯一实例
private static A instance = new A(); //被static修饰,该属性是类的属性,jvm中,每个类的类对象只有唯一一份,类对象里的这个成员自然也是唯一一份
//获取实例的方法
public static A getInstance() {
return instance; //单纯的读操作
}
//禁止new实例(将构造方法设为private)
private A(){}
}
public class Thread_Test18 {
public static void main(String[] args) {
A instance1 = A.getInstance();
A instance2 = A.getInstance();
此时 s1 和 s2 是同一个对象!!
}
}
8.1.2 懒汉模式-单线程版
类加载的时候不创建实例. 第一次使用的时候才创建实例
class B {
private static B instance = null;
public static B getInstance() {
if (instance == null) {
instance = new B();
}
return instance;
}
private B() {}
}
public class Thread_Test19 {
public static void main(String[] args) {
B instance1 = B.getInstance();
B instance2 = B.getInstance();
System.out.println(instance1 == instance2);
}
}
8.1.3 懒汉模式-多线程版(加锁)
上面的单线程代码乍一看没有什么问题,但其实存在线程不安全的问题!!
线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance() 方法, 就可能导致创建出多个实例!!!
eg:
此时就new了多个对象!!!
那么如何解决上述的线程安全问题?----->加锁
但是解了锁也不就是一定安全了~要具体问题具体分析
eg:
public static B getInstance() {
if (instance == null) {
synchronized (B.instance) {
instance = new B();
}
}
return instance;
}
上述代码给instance = new B();
加锁仍然存在线程不安全的问题提:多线程情况下,如果t1线程执行完判定,另一个t2线程执行到new,那么执行到new的t2线程有锁,执行完判定的t1线程阻塞等待,等到t2new完之后t1才继续执行,可是此时对象已经为非空了,就难以保证单例。
解决方法:保证判定和new是一个原子操作
eg:
public static B getInstance() {
synchronized (B.instance) { //在这里加锁就保证了判定和new是一个原子操作
if (instance == null) {
instance = new B();
}
}
return instance;
}
8.1.3 懒汉模式-多线程版(优化)
加锁的操作其实比较低效,一旦加锁就会涉及到阻塞等待
public static B getInstance() {
synchronized (B.instance) { //在这里加锁就保证了判定和new是一个原子操作
if (instance == null) {
instance = new B();
}
}
return instance;
}
上面加锁的代码无论何时调用getInstance()
都会触发锁的竞争~
其实,此处的线程不安全只出现在首次创建对象这里,一旦对象new好了,后续调用getInstance()
,就只是单纯的读操作,就不会出现线程不安全的问题,就没有必要加锁!
优化代码:
public static B getInstance() {
if(instance == null){
synchronized (instance) {
if (instance == null) {
instance = new B();
}
}
}
return instance;
}
这样是不是就完美了呢?
其实这个代码还有一个很重要的问题!!!!指令重排序!!!
上述代码的instance = new B();
可能触发指令重排序
instance = new B();
分为三步指令:
(1)创建内存
(2)调用构造方法
(3)把内存地址给引用
如果顺序优化成为(1)创建内存(3)把内存地址给引用(2)调用构造方法
在(3)把内存地址给引用 的时候,对象已经为非空,其他堵塞等待的线程就会开始执行,发现对象非空,条件不成立!直接返回对象的引用,由于对象没有调用过构造方法,因此后面就是将错就错了~
最终版本:
class B {
volatile private static B instance = null; //加上volatile后创建实例就会禁止指令重排序
public static B getInstance() {
if(instance == null){
synchronized (instance) {
if (instance == null) {
instance = new B();
}
}
}
return instance;
}
private B() {}
}
public class Thread_Test19 {
public static void main(String[] args) {
B instance1 = B.getInstance();
B instance2 = B.getInstance();
System.out.println(instance1 == instance2);
}
}
8.2 阻塞式队列
8.2.1 什么是阻塞式队列
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列带有阻塞特性:
(1)如果队列空,尝试出队列,就会阻塞等待。等待到队列不为空为止。
(2)如果队列满,尝试入队列,也会阻塞等待。等待到队列不为满为止。
是线程安全的!!
8.2.2 标准库里的阻塞模型
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Thread_Test20 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
//put入队列
queue.put("hello1");
queue.put("hello2");
queue.put("hello3");
//take出队列
String result = null;
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
}
}
上述代码中put了三次,take四次,前三次都很顺利,第四次就堵塞了~~
注意:
(1)BlockingQueue
是一个接口,真正实现的类是LinkedBlockingQueue
(2)put方法用于阻塞式的入队列,take方法用于阻塞式的出队列
8.2.3 生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
eg:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Thread_Test21 {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
//生产者
Thread t1 = new Thread(() -> {
int value = 0;
while (true) {
try {
System.out.println("生产元素:" + value);
queue.put(value);
value++;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
//消费者
Thread t2 = new Thread(() -> {
while (true) {
try {
int value = queue.take();
System.out.println("消费元素:" + value);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t2.start();
}
}
生产者消费者模型主要解决的问题:
(1)可以让上下游模块之间进行更好的“解耦合”
考虑下列场景:A服务器调用B服务器(A给B发送请求,B给A返回响应)
此时如果A和B直接通信,就是耦合比较高的情况~
如果B挂了,对于A会有直接的影响,A也就跟着挂了
如果引入生产者消费者模型,耦合就降低了~:
服务器A不知道服务器B存在,B也不知道A存在,他两就只认识队列,此时B挂了对A没有影响的
阻塞队列服务器的来源:由于阻塞队列特别好使,所以有大佬把阻塞队列这个功能单独拎出来,扩充了更多的功能做成了单独的服务器~
那阻塞队列服务器会挂吗?—>会!
但是挂的概率相比A和B来说小很多,因为队列和业务无关,代码不太会变化,更加稳定。而A和B是业务服务器,和业务相关,要随时修改,支持新的需求功能,此时程序猿很容易就写出来新的bug
(2)削峰填谷
阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
A收到的请求数量是和用户行为相关的,而用户行为是随机的,有些情况下会出现“峰值”突然爆发式得涨一波(eg:双十一)
上图中A和B是直接调用的关系,A受到了请求峰值,B同样也会有这个峰值,此时如果B设计的时候没有考虑峰值的处理,可能就挂了~~
如果加入阻塞队列服务器:
**当A收到的请求多了之后,队列里的元素就多了,但是B仍可以按照之前的速率来取元素,队列帮B分担了压力,这样的方式就叫削峰填谷
**
8.2.4 实现一个阻塞式队列
实现一个阻塞式队列我们分为三部去设计:
(1)先实现一个普通队列
(2)加上线程安全
(3)加上阻塞功能
下来让我们逐步实现:
//基于数组来实现队列
private int[] items = new int[1000];
//约定队列中的有效元素
private int head = 0;
private int tail = 0;
这样有个问题—>怎么区分队列空和队列满?
加入size来计数~
private int size = 0;
此时写出入队列:
class MyBlockingQueue {
//基于数组来实现队列
private int[] items = new int[1000];
//约定队列中的有效元素
private int head = 0;
private int tail = 0;
private int size = 0;
//入队列
public void put(int num) {
while (size == items.length) {
System.out.println("队列已满");
return;
}
//把新元素放在tail位置
items[tail] = num;
tail++;
//此时如果tail达到了末尾,就需要从头再来
if(tail == items.length) {
tail = 0;
}
size++;
}
//出队列
public Integer take() {
//队列为空
if (size == 0) {
return null;
}
int value = items[head];
head++;
//出元素到空了
if (head == items.length) {
head = 0;
}
size--;
return value;
}
}
这样就完成了(1),实现了一个普通的循环队列
下面我们来加上线程安全:
因为方法中涉及很多的修改操作,所以我们用synchronized来加锁,保证线程安全
//入队列
synchronized public void put(int num) {...}
//出队列
synchronized public Integer take() {...}
因为head,tail,size涉及到读操作,用volatile保证内存可见性和禁止指令重排序,防止出现问题
volatile private int head = 0;
volatile private int tail = 0;
volatile private int size = 0;
接下来最后一步,加上阻塞功能:
(1)如果队列空,尝试出队列,就会阻塞等待。等待到队列不为空为止。
//队列空时出队列-->阻塞等待
if (size == 0) {
// return null;
this.wait();
}
//有元素入队列时,唤醒wait
size++;
this.notify();
(2)如果队列满,尝试入队列,也会阻塞等待。等待到队列不为满为止。
//队列满时入队列-->阻塞等待
if (size == items.length) {
// System.out.println("队列已满");
// return;
this.wait();
}
//有元素出队列时,唤醒wait
size--;
this.notify();
完整代码:
class MyBlockingQueue {
//基于数组来实现队列
private int[] items = new int[1000];
//约定队列中的有效元素
volatile private int head = 0;
volatile private int tail = 0;
volatile private int size = 0;
//入队列
synchronized public void put(int num) throws InterruptedException {
if (size == items.length) {
// System.out.println("队列已满");
// return;
this.wait();
}
//把新元素放在tail位置
items[tail] = num;
tail++;
//此时如果tail达到了末尾,就需要从头再来
if(tail == items.length) {
tail = 0;
}
size++;
this.notify();
}
//出队列
synchronized public Integer take() throws InterruptedException {
//队列为空
if (size == 0) {
// return null;
this.wait();
}
int value = items[head];
head++;
//出元素到空了
if (head == items.length) {
head = 0;
}
size--;
this.notify();
return value;
}
}
public class Thread_Test22 {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
//生产者
Thread t1 = new Thread(() -> {
int value = 0;
while (true) {
try {
System.out.println("生产元素:" + value);
queue.put(value);
value++;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
//消费者
Thread t2 = new Thread(() -> {
while (true) {
try {
int value = queue.take();
System.out.println("消费元素:" + value);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t2.start();
}
}
扩展:
注意!!!官方其实不太建议我们把wait放在if里面!
因为wait是可能被其他方法中断的(interrupt方法),此时wait其实等待的条件还没成熟,就提前被唤醒了~~
很可能在别的代码中暗中interrupt,把wait提前唤醒了,还没有满足条件就往下走了
可以改成:
这样就可以在wait唤醒之后再次判定,如果条件不满足可以继续wait
8.3定时器
8.3.1 什么是定时器?
定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码~
8.3.2 标准库中的定时器
在Java的标准库中提供了一个Timer类,其中的核心方法是schedule()。schedule()方法有两个参数:第一个参数表示将要执行的任务代码,第二个参数指定多长时间后执行。
import java.util.Timer;
import java.util.TimerTask;
public class Thread_Test23 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
},1000);
}
}
8.3.3 实现一个定时器
首先我们要知道,定时器要管理的任务可能是非常多的,我们可以用一个或一组工作线程,每次找到这些任务中执行时间最早的,做完后执行第二早的…时间到了就执行,时间没到就等等~~所以我们的核心数据结构就是:堆
而我们的标准库中就有一个线程的堆:
PriorityQueue (带优先级的阻塞队列,并且线程安全)
我们创建类MyTimer:
class MyTimer {
//我们的核心数据结构
private BlockingQueue<> queue = new PriorityBlockingQueue<>();
}
我们的队列中的元素是什么类型呢?
—>这里我们将手动封装一下这里的元素:创建一个类表示执行的任务是啥?以及啥时候执行?
//表示一个任务
class MyTask {
public Runnable runnable;
public long time; //时间戳(当前时间与基准时间1970年01月01日00点的毫秒数之差)
}
class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
}
接下来编写定时器中的schedule方法:
class MyTask {
public Runnable runnable;
public long time; //时间戳(当前时间与基准时间1970年01月01日00点的毫秒数之差)
//MyTask构造方法
public MyTask(Runnable runnable,long delay) {
this.runnable = runnable;
//取当前时间戳加上delay,就是我们任务要执行的时间戳
this.time = delay + System.currentTimeMillis();
}
}
class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay) {
//根据参数构造MyTask,直接插入队列即可
MyTask myTask = new MyTask(runnable,delay);
queue.put(myTask);
}
thread.start();
}
目前我们已经构造了核心数据结构以及他的元素,现在我们可以在MyTimer类中构造一个线程来执行相关的任务了(插入元素)~
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
class MyTask {
public Runnable runnable;
public long time; //时间戳(当前时间与基准时间1970年01月01日00点的毫秒数之差)
//MyTask构造方法
public MyTask(Runnable runnable,long delay) {
this.runnable = runnable;
//取当前时间戳加上delay,就是我们任务要执行的时间戳
this.time = delay + System.currentTimeMillis();
}
}
class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay) throws InterruptedException {
//根据参数构造MyTask,直接插入队列即可
MyTask myTask = new MyTask(runnable,delay);
queue.put(myTask);
}
public MyTimer() {
Thread thread = new Thread(() -> {
while (true) {
try {
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis(); //获取当前时间
if (myTask.time <= curTime) {
//时间到了,执行任务
myTask.runnable.run();
}else {
//时间还没到,把刚才取出的任务重新塞回队列
queue.put(myTask);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
}
public class Thread_Test24 {
public static void main(String[] args) {
}
}
目前我们已经完成了定时器的工作逻辑,上述代码能够实现功能吗?----->NO!
上述代码存在两个非常严重的bug!!!
(1)当前我们队列里的MyTask元素是按照什么规则来表示优先级的?好像并没有比较规则
(2)忙等(通过while(true)来等待,等待的过程会一直占用cpu):在等待到达下一个任务执行时间的过程,cpu没捞着休息~
解决思路:
(1)让MyTask类实现Comparable接口
(2)使用wait方法可以随时提前结束
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable<MyTask>{
public Runnable runnable;
public long time; //时间戳(当前时间与基准时间1970年01月01日00点的毫秒数之差)
//MyTask构造方法
public MyTask(Runnable runnable,long delay) {
this.runnable = runnable;
//取当前时间戳加上delay,就是我们任务要执行的时间戳
this.time = delay + System.currentTimeMillis();
}
@Override
public int compareTo(MyTask o) {
//这样写意味着每次取出的是时间最小的元素
return (int)(this.time - o.time);
}
}
class MyTimer {
private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
//创建一个锁对象
private Object locker = new Object();
public void schedule(Runnable runnable,long delay) throws InterruptedException {
//根据参数构造MyTask,直接插入队列即可
MyTask myTask = new MyTask(runnable,delay);
queue.put(myTask);
synchronized (locker) {
locker.notify();
}
}
public MyTimer() {
Thread thread = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis(); //获取当前时间
if (myTask.time <= curTime) {
//时间到了,执行任务
myTask.runnable.run();
}else {
//时间还没到,把刚才取出的任务重新塞回队列
queue.put(myTask);
locker.wait(myTask.time - curTime); //加上wait等待到规定时间再执行,如果加上了新元素,就唤醒wait,重新取队首元素并判定
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
}
}
public class Thread_Test24 {
public static void main(String[] args) throws InterruptedException {
MyTimer mytimer = new MyTimer();
mytimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 4");
}
},4000);
mytimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3");
}
},3000);
mytimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2");
}
},2000);
mytimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1");
}
},1000);
System.out.println("hello 0");
}
}
这是我们完善后的代码,下面看结果:
扩展:对于加锁位置不同导致的结果不同
8.4 线程池
8.4.1 什么是线程池?
所谓线程池就是提前把线程准备好,创建线程不是从系统申请,而是直接从池子里拿,线程不用了,也是直接还给池子
使用线程池的目的是为了提高效率,可是为什么使用线程池就高效?
eg:
8.4.2 标准库中的线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Thread_Test25 {
public static void main(String[] args) {
//创建固定包含10个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
//此处并非直接new一个对象,而是使用Executros中的一个静态方法完成对象的构造
//这样的模式称为工厂模式(是为了解决构造方法重载的局限性)
pool.submit(new Runnable() { //ExecutorService类中的submit方法可以注册一个任务到线程池中
@Override
public void run() {
System.out.println("haoleiou");
}
});
}
}
Executors 创建线程池的几种方式:
(1)newFixedThreadPool: 创建固定线程数的线程池
(2)newCachedThreadPool: 创建线程数目动态增长的线程池.
(3)newSingleThreadExecutor: 创建只包含单个线程的线程池.
(4)newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor 类的封装
扩展:ThreadPoolExecutor 类
我们从Java的官方文档中去了解ThreadPoolExecutor :
我们找到ThreadPoolExecutor的构造方法:
其中:
corePoolSize—>核心线程数
maximumPoolSize—>最大线程数
怎么理解?—>我们把线程池看作一个公司,其中有正式员工和实习生,而corePoolSize—>核心线程数就相当于正式员工,maximumPoolSize—>最大线程数就相当于正式员工+实习生。
公司会在忙的时候多招聘实习生来增加生产力,不忙的时候就把实习生裁掉~~>如果任务较多,线程池会多创建一些临时线程,如果任务少了空闲了线程池就会把多的临时线程销毁
long keepAliveTime—>允许临时线程摸鱼的最大时间
TimeUnit unit—>存活时间的单位
BlockingQueue<‘Runnable’> workQueue—>线程池内置的一个阻塞队列
线程池要管理很多任务,也是通过阻塞队列来实现的~
ThreadFactory threadFactory—>线程工厂
RejectedExecutionHandler handler—>线程池的拒绝策略(important)
线程池的拒绝策略就是在线程池满了的时候,继续添加任务该如何拒绝!!
这是标准库提供的四种拒绝策略:
8.4.3 实现一个线程池
首先要有一个存放任务的阻塞队列,还要实现submit方法添加任务,然后通过for循环实现一个固定数量的线程池:
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 thread = new Thread(() -> {
try {
//需要在线程池内部有一个循环,保证一有任务就取
while (true) {
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
}
}
public class Thread_Test26 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int num = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello dear " + num);
}
});
}
}
}
结果:
可以发现线程池的执行是无序的~
扩展:
在用线程池时,将线程数设置为多少合适?—>根据任务类型来决定!
任务有两种:CPU密集型任务和IO密集型任务
极端情况下,全是CPU密集型任务是,线程数不应超出CPU核心数(逻辑核心),全是IO密集型任务,线程数可以远远超出CPU核心数~~
在实践中往往要具体测试来确定~~