多线程概念
线程
:线程是操作系统能够运行的调度的最小单位。它被包含在进程
中,是进程的实际运作单位
简单理解为:应用软件中互相独立
,可以同时运行
的功能
例如,联想的电脑管家,我们运行起来它就是个进程
而它左边导航栏中的这几个功能就可以看做六个线程
所以综上所述,多线程的意思就是
运行的功能比较多
,就形成了多线程
进程
:进程是程序的基本执行实体
那么我们可以理解为,只要我们打开一个程序,程序执行后就是一个进程。例如
为什么要使用多线程
那么为什么我们要用多线程呢?
我们可以先来看下面这个例子
小白在工厂打螺丝,只负责一条线路,但是这条线路上只会每隔十分钟发一个货物下来,那么小白就可以办完一次货物,休息个九分多钟,黑老板看到了当然不愿意了,所以由给它安排了几条线路
这下小白就不能偷懒了,本来原本可以休息十分钟,现在可能只能休息三分钟
同样我们程序也是如此,以往我们写的代码都是单线程的程序
从第一条编译到最后一条,每条代码都要等着前一条代码执行完才能够执行下一条,浪费了很多时间,所以要基于多线程去操作程序
多线程的应用场景
例如,我们打游戏的时候进入游戏界面,就是在等待加载一些资源文件,这里就不得不提一个二字游戏
这里就是在等待游戏多线程加载
并发和并行
并发
:在同一时刻,有多个指令在单个
CPU上交替
执行
并行
:在同一时刻,有多个指令在多个
CPU上同时
执行
这样说有点抽象,那么我们来看一张图
上面图就能反应并发的效果,我们在玩电脑时,一会儿要弄鼠标,一会儿要拿可乐,一会儿要来根小烟,那么我们的手就当成是CPU,其他的当成线程,这就是并发
同样我们可以看下面这张图
看到这里可能有些人会有疑惑,我电脑只有一个CPU啊,确实我们电脑都是只有一个CPU,但是我们CPU标注多少核多少线程的
这些线程的数量就是我们能够同时运行多少个线程,当我们运行很多线程的时候,线程就会在在这些线程中随机运行,来回切换
简单来说可以理解为,
单车道并发,多车道并行
多线程的实现方式
在多线程中一共有三种方式实现多线程
- 继承Thread类的方式进行实现
- 实现Runnable接口的方式进行实现
- 实现Callable接口和Future接口方式实现
Thread实现
那么我们来进行第一种方法继承Thread
,首先我们创建一个类继承Thread
然后我们来重写run方法
那么我们在这里打印100次helloword吧
这里的getName方法是获取线程的名称,那么我们来创建我们自己写的线程利用setName设置名字,然后运行
然后我们看运行结果就可以看到线程1和2交替运行
完整代码:
//EXthread
public class EXThread extends Thread{
@Override
public void run() {
//书写要线程干什么事情
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"helloword");
}
}
}
//main
public static void main(String[] args) {
EXThread t1 = new EXThread();
t1.setName("线程1");
EXThread t2 = new EXThread();
t2.setName("线程2");
t1.start();
t2.start();
}
Runnable接口实现
接着我们来实现第二种方式,实现Runnable接口,首先我们创建一个类然后实现Runnable接口,我们同样写上打印一百个Helloword
解释一下上面代码,为啥我们需要利用Thread.currentThread
来获取当前线程的对象
首先第一种直接继承Thread是让类直接与Thread产生父子关系
,子类当然可以直接调用父类的方法,而在实现Runnable接口的时候,我们其实在定义一个“任务”
,而不是定义一个具体的线程。
这个“任务”可以被多个线程执行。因此,当我们把这个“任务”传递给一个Thread
对象并启动线程来执行时,我们需要通过Thread.currentThread().getName()
来获取当前执行任务的线程名称。
然后我们来创建主类
正如上面所说,我们需要把我们定义的任务传递给线程,让线程来执行,然后我们看运行效果
完整代码:
//IMRunnable
public class IMRunnable implements Runnable{
@Override
public void run() {
Thread t = Thread.currentThread();
//这里面写需要进行多线程的方法
for (int i = 0; i < 100; i++) {
System.out.println(t.getName() +"helloword");
}
}
}
//MyRunnable
public class MyRunnable {
public static void main(String[] args) {
//创建IMRunnable对象
//表示多线程要执行的任务
IMRunnable i1 = new IMRunnable();
//创建线程对象
Thread t1 = new Thread(i1);
t1.setName("线程1");
Thread t2 = new Thread(i1);
t2.setName("线程2");
//启动
t1.start();
t2.start();
}
}
Callable和Future接口实现
看了前面俩个多线程的实现,我们可以发现一个共同点,那就是如果我们需要获取多线程结果怎么办 ,那我们就可以考虑实现Callable接口
可以看到我们的Callable是有返回值的,接着我们来实现主类
挨个解释一下,Callable也是和Runnable一样是创建一个任务,但是Callable因为有返回对象
是传递进Future中进行管理,然后传入线程对象
完整代码
//MyCallable
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum+=i;
}
return sum;
}
}
//ThreadDemo
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//表示要执行的任务
MyCallable mc = new MyCallable();
//管理多线程的运行结果
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建多线程对象
Thread t1 = new Thread(ft);
//启动
t1.start();
//获取多线程结果
Integer result = ft.get();
System.out.println(result);
}
}
Thread和Runnable区别
可以再深入讲讲Thread和Runnable的区别的,
因为使用MyThread会创建多个实例,而Thread(MyRunnable)创建的多线程可’共用’一个MyRunnable实例。
所以,在MyThread中,synchronized锁是必须static的,且若想把while中的代码块直接抽取成synchronized修饰方法也会导致锁失效,因为同步方法默认使用的锁是this(this指向MyThread的多个实例),除非同时再加上static;
而在接口实现的Thread(MyRunnable)中反而可以不使用static修饰synchronized锁,或者while中的代码块直接抽取成非static的synchronized修饰方法也没问题(this指向的MyRunnable唯一实例)。
记得在哪里也看到过不建议直接继承重写成MyThread类。我认为用Thread(Runnable)实现好在,能把MyRunnable通过面向对象的思想去调用,比如new一个MyRunnable实现,即创建了一个新的xx电影的票池和一个新的xx电影的锁。
常见的成员方法
三种线程都实现后,我们来看看有哪些常用的方法
前置准备:为了方便演示,我们来创建一个线程对象,还是打印100个helloworld
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+" "+"helloWorld");
}
}
}
1.getname()
作用:获取线程名字
这个方法我们在前面已经用过了,就是获取线程的名字,那么我们如不获取名字,直接getname会发生什么呢
public static void main(String[] args) {
//创建线程对象
Thread t1 = new MyThread();
Thread t2 = new MyThread();
//开启线程
t1.start();
t2.start();
}
可以看到如果我们不设置线程名字会默认Thread-XX这种形式出现
那么我们来看一下源码是怎么回事,按Ctrl+N 搜索Thread
然后我们ctrl+F12找到它的空参构造
然后我们就可以看到
然后我们可以看看nextThreadNum是干什么的
从这里看出这就是个序号,默认自增开始值为0
2.setname()
作用:设置线程名字
setname()方法呢,我们上面已经使用过了,就是用来设置线程的名字所以我这里就直接贴代码和运行结果
public class ThreadDemo {
public static void main(String[] args) {
//创建线程对象
Thread t1 = new MyThread();
Thread t2 = new MyThread();
t1.setName("线程1");
//开启线程
t1.start();
t2.start();
}
}
可以看到没有设置名字的默认还是Thread-XX的形式,同样的我们可以在创建线程的时候传递名字,只需要我们重写构造方法
即可
//MyThread
public class MyThread extends Thread{
public MyThread() {
super();
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+" "+"helloWorld");
}
}
}
//Mythread
public static void main(String[] args) {
//创建线程对象
Thread t1 = new MyThread();
Thread t2 = new MyThread("线程2");
t1.setName("线程1");
//开启线程
t1.start();
t2.start();
}
3.currentThread()
作用:获取当前线程
使用这个方法,我们可以获取到当前执行的线程,那么我们如果将我们前面的方法都注释掉,获取到的又是什么呢?
我们运行一下
可以看到输出了一个main
这里可以给大家补充一点JVM知识
当JVM虚拟器启动之后,会自动的启动多条线程
其中有一条线程就是main线程
它的作用就是去调用main方法,并去执行里面的代码
所以我们写过的所有代码,都是运行在main线程当中
4.sleep()
作用:让线程睡眠多少时间 单位为毫秒,当时间到后线程会自动的醒来,继续执行其他的代码
我们直接来进行测试,由于这里是延迟测试,不直观,大家可以自行测试
线程优先级问题
接下来的俩个方法,我们先来补充一点知识
线程调度问题
-
抢占式调度
- 解释:指多个线程去
抢占
CPU的执行权,CPU在执行哪个线程是不确定的,执行时间也是不确定的。 - 特点:随机性
- 解释:指多个线程去
-
非抢占式调度
- 解释:指多个线程
轮流
的去执行,每个线程的执行的时间是差不多的
- 解释:指多个线程
那么在Java中是采取了第一种抢占式调度,所以我们只需要在意俩个字随机
,而随机的方式是跟优先级有关
,下面俩个方法就是跟优先级有关,优先级越大抢到CPU的概率就是越大的,优先级分为
1-10
,1最小10最大,没有设置默认是5
5.getPriority()
作用:获取线程优先级
t1.getPriority())
我们来看看输出的结果
6.setPriority()
作用:设置线程优先级
看了上面的默认线程数量,现在我们来调整优先级,看看抢占情况
//创建线程对象
MyThread mt = new MyThread();
Thread t1 = new Thread(mt,"线程1");
Thread t2 = new Thread(mt,"线程2");
t1.setPriority(1);
t2.setPriority(10);
//开启线程
t1.start();
t2.start();
注意:优先级高并不代表百分百抢占到线程只是概率会优先运行完
这里我就没有过多测试,大家有兴趣可以试一试
守护线程
作用:为其他线程提供服务。通俗来讲就是非守护线程执行完毕后,守护线程也会陆续结束
例如:清空过时的缓存项、计时器线程
或者这里在举一个通俗的例子,我们使用的QQ
我们把聊天窗口当做一个线程,当我们在传输文件的时候,开启了另一个线程,如果此时我把聊天窗口关闭,传输文件的线程也会结束。
上面的传输文件就属于守护线程
前置demo
//MyThread1
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i <=10 ; i++) {
System.out.println(getName() + " "+i);
}
}
}
//MyThread2
@Override
public void run() {
for (int i = 0; i <=100 ; i++) {
System.out.println(getName() + " "+ i);
}
}
注意两者区别,守护线程我打印的数量会多一点
7.setDaemo()
作用:设置为守护线程
//ThreadDemo3
public class ThreadDemo3 {
public static void main(String[] args) {
//创建线程
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
//设置名字
t1.setName("非守护");
t2.setName("守护");
//将第二个线程设置为守护
t2.setDaemon(true);
//开启线程
t1.start();
t2.start();
}
}
我们运行程序,看看效果
出让/礼让线程
作用:顾名思义就是线程执行一次后,让出执行权,再去争夺
前置demo:
//threadDemo
public static void main(String[] args) throws InterruptedException {
//创建线程对象
MyThread1 t1 = new MyThread1();
MyThread1 t2 = new MyThread1();
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
8.yield()
我们直接上代码
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i <=10 ; i++) {
System.out.println(getName() + " "+i);
//让出
Thread.yield();
}
}
}
我们看看运行结果
可以看到基本上都是均匀的每个线程一次执行
插入线程/插队线程
前置demo
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i <=10 ; i++) {
System.out.println(getName() + " "+i);
//让出
Thread.yield();
}
}
}
9.join()
作用:插队,提前先执行线程
我们来看demo
public static void main(String[] args) throws InterruptedException {
//创建线程对象
com.methodDemo4.MyThread1 t1 = new com.methodDemo4.MyThread1();
t1.setName("线程1");
//开启线程
t1.start();
for (int i = 0; i <=10 ; i++) {
System.out.println("main " + i);
}
}
上面这段程序运行结果,是main线程跟我们创建的线程一起抢夺CPU,结果会发生什么呢
可以看到main线程优先执行完,然后才是我们自己创建的线程,那么我们想把我们自己创建的线程优先执行完呢?这个时候就可以用join方法了
我们在运行一次
线程的生命周期
接下来我们来看看线程的生命周期
线程一共有五个状态
新建(New)
:当一个Thread类的实例被创建时,它处于新建状态。此时还没有调用start()方法启动线程。可运行(Runnable)
:线程进入运行状态后,可以通过调用start()方法来启动线程,线程处于可运行状态,并没有被挂起,可能正在执行也可能等待CPU调度。阻塞(Blocked)
:线程在某些情况下会被挂起,如等待I/O操作完成、获取锁等。在这种状态下,线程不会占用CPU资源,直到阻塞条件被解除。等待(Waiting)
:当线程调用wait()方法、join()方法或LockSupport.park()方法时,线程会进入等待状态,直到其他线程调用notify()、notifyAll()方法或join()中的线程执行完毕时,线程才会重新进入可运行状态。终止(Terminated)
:线程执行完任务或者调用stop()方法结束线程时,线程进入终止状态。一旦线程终止,它就不能再进入可运行状态。
线程安全的问题
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
那么我们来写一下这个demo
//MyThread
public class MyThread extends Thread{
int tickets = 1;
@Override
public void run() {
while(true){
if (tickets>100){
break;
}
System.out.println("正在卖第 "+tickets+" 张票");
tickets++;
}
}
}
//ThreadDemo6
public class ThreadDemo6 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
我们运行一下程序看看效果
可以看到如果我们这样设计,他会造成每个窗口都卖了100的现象,那么如果我们将tickets
设置成静态变量
在运行一次
可以看到这次又会造成票重复卖的情况,那么这样的情况我们该如何解决呢
同步
经过上面的练习,我们可以看到俩个或俩个以上的线程需要共享对同一个数据的存取
时,俩个线程会相互覆盖,导致多线程破坏共享数据
,接下来我们就来学习如何解决这个问题
synchronized()
同步代码块
同步代码块:在代码中把操作共享的数据的代码锁起来
注意点:
括号内需要传入一个唯一的锁对象,例如Object、当前文件字节码文件对象例如MyThread.class,如果不唯一则锁失效
特点:
- 锁
默认打开
,有一个线程进去了,锁自动关闭
- 里面的代码全部执行完毕,线程出来,锁自动打开
接下来我们改变一下代码
static int tickets = 1;
//锁对象,一定要是唯一
static Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj){
if (tickets>100){
break;
}
try {
System.out.println(getName()+ "正在卖第 "+tickets+" 张票");
tickets++;
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
然后我们运行查看结果
可以看到这下就不会出现超卖的情况
同步方法
作用:将方法锁起来
特点:
- 同步方法是锁住方法里面所有的代码
- 锁对象不能是自己指定
- 非静态:this
- 静态:当前类的字节码文件
接下来我们用Runnable实现
//ThreadDemo
public class ThreadDemo7 {
public static void main(String[] args) {
//创建任务
MyThread mt = new MyThread();
//创建线程
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
Thread t3 = new Thread(mt);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//启动
t1.start();
t2.start();
t3.start();
}
}
//MyThread
public class MyThread implements Runnable{
int tickets = 0;
@Override
public void run() {
while (true){
if (method()){
break;
}
}
}
//锁对象为方法
private synchronized boolean method() {
if (tickets==100){
return true;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
tickets++;
System.out.println(Thread.currentThread().getName()+" 正在卖 "+ tickets+" 张票");
return false;
}
}
注意这里变化tickets
没有静态,因为我们Runable是个唯一作为任务传入Thread
Lock锁
看了上面的方法,其实我们并没有理解到在哪里加上了锁和在哪释放了锁,所以下面我们来看看新的锁对象Lock
方法:
- void lock() 获得锁
- viod unlock() 释放锁
注意:Lock是接口不能直接实例化,这里采用了它的实现类ReentrantLock()
来实例化
我们来看demo
// ThreadDemo8
public class ThreadDemo8 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
//MyThread
public class MyThread extends Thread{
static int tickets = 1;
//创建锁对象
static Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
//加锁
lock.lock();
if (tickets>100){
break;
}else {
Thread.sleep(10);
System.out.println(getName()+ "正在卖第 "+tickets+" 张票");
tickets++;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}
}
主要是修改了三处地方,第一个就是创建锁对象,第二步就是在需要的地方加锁,第三处就是释放锁,利用finally保证锁能够被释放,不会造成锁没有释放
死锁
关于死锁的原因:给大家举一个例子即可
假设有两个人,Alice 和 Bob,他们共享着两个资源,一个是打印机,另一个是扫描仪。他们都需要先打印一份文件,然后扫描该打印出来的文件。
现在,Alice 拿到了打印机,开始打印她的文件。与此同时,Bob 拿到了扫描仪,开始扫描他的文件。但是,Alice 在打印完文件后想要扫描,而 Bob 在扫描完文件后想要打印。由于他们各自持有对方需要的资源,因此他们无法继续进行操作,陷入了死锁状态。
简单来说 就是A等B B等A 然后就卡死了
不要进行锁嵌套
public class ResourceSharing {
public static void main(String[] args) {
final Object printer = new Object();
final Object scanner = new Object();
// Alice线程
Thread alice = new Thread(() -> {
synchronized (printer) {
System.out.println("Alice is printing.");
try {
Thread.sleep(100); // 模拟打印时间
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (scanner) {
System.out.println("Alice is scanning.");
}
}
});
// Bob线程
Thread bob = new Thread(() -> {
synchronized (scanner) {
System.out.println("Bob is scanning.");
try {
Thread.sleep(100); // 模拟扫描时间
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (printer) {
System.out.println("Bob is printing.");
}
}
});
alice.start();
bob.start();
}
}
生产者和消费者(等待唤醒机制)
生产者消费者模式是一个十分经典的多线程协作的模式
正常情况下,我们在运行俩条线程的时候,顺序一般是随机的
而在该模式下 ,会让线程轮流执行
消费者等待
关于消费者等待就如上图所示,如果桌上没有东西,消费者就会等待(wait)
然后释放掉CPU的执行权
,此时一定是生产者抢到CPU做好后,利用唤醒(notify)消费者
生产者等待
如上图,如果是生产者抢到CPU,此时桌子上没有面条,生产者做好面条,释放掉了CPU的执行权,然后生产者又抢到
了CPU的执行权,但桌上已经有了面条,厨师只能等待(wait)
,然后消费者就会抢到
等待唤醒机制
那么完整的机制就是如下图
生产者和消费者在执行前都会先去判断桌上是否有食物,消费者发现有则开吃,生产者发现没有就开始制作
常见方法
那么我们来看看Demo
下面会创建四个类
- Cook 厨师
- Foodie 食客
- Desk 桌子
- 控制消费者和生产者
- ThreadDemo 主方法
- 实现线程轮流交替执行
//Desk
public class Desk {
/**
* 公共变量,控制Cook 和 Foodie
*/
//定义锁对象
public static Object lock = new Object();
//总个数
public static int count = 10;
//是否有面条 1:有面条 0:无面条
public static int foodFlag = 0;
}
//Cook
public class Cook extends Thread{
/**
* 生产食物
*/
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if (Desk.count==0){
break;
}else {
//判断桌子上是否有食物
if (Desk.foodFlag == 0){
//没有就开始生产
Desk.foodFlag = 1;
System.out.println("厨师已经生产了一个食物");
//让线程跟锁进行绑定
//唤醒该锁下面的全部线程
Desk.lock.notifyAll();
}else {
//有食物就开始等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
}
//Foodlie
public class Foodlie extends Thread {
@Override
public void run() {
while (true) {
//创建同步代码块
synchronized (Desk.lock) {
//判断还能吃多少碗
if (Desk.count == 0) {
break;
} else {
//判断桌上是否有食物
if (Desk.foodFlag == 0) {
//没有则等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//有则开吃
Desk.count--;
System.out.println("还能吃 "+Desk.count+" 碗");
//将桌子状态调整为没有食物
Desk.foodFlag = 0;
//唤醒厨师
Desk.lock.notifyAll();
}
}
}
}
}
}
//ThreadDemo
public class ThreadDemo {
public static void main(String[] args) {
Foodlie f = new Foodlie();
Cook c = new Cook();
//启动线程
f.start();
c.start();
}
}
代码解释:
Desk.lock.wait();
Desk.lock.notifyAll();
以上代码是让线程跟锁进行绑定,可以让使用同一个锁的线程等待或唤醒
运行结果
等待唤醒机制(阻塞队列方式实现)
跟上图所示,我们利用阻塞队列的方式,将食物放在队列中,队列的特点先进先出,所以我们消费者可以依次拿取
这里需要注意
- 生产者和消费者必须处于同一个阻塞队列
阻塞队列的继承结构
阻塞队列一共以上实现了四个接口,可以看到阻塞队列我们可以利用迭代器或者增强for来进行遍历,我们要创建的是下面俩个实现类
那么我们接下来来看看实现Demo
//Foodie
public class Foodie extends Thread{
ArrayBlockingQueue<String> arrayBlockingQueue ;
public Foodie(ArrayBlockingQueue arrayBlockingQueue) {
this.arrayBlockingQueue = arrayBlockingQueue;
}
@Override
public void run() {
while (true){
//判断食物数量
if (Desk.count==0){
break;
}else {
try {
String food = arrayBlockingQueue.take();
System.out.println(food + " "+Desk.count);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
//Cook
public class Cook extends Thread{
ArrayBlockingQueue<String> arrayBlockingQueue ;
public Cook(ArrayBlockingQueue arrayBlockingQueue) {
this.arrayBlockingQueue = arrayBlockingQueue;
}
@Override
public void run() {
while (true){
try {
//判断食物数量
if (Desk.count==0){
break;
}else {
arrayBlockingQueue.put("面条");
Desk.count--;
System.out.println("厨师放了一碗面条");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//Desk
public class Desk {
//食物数量
public static int count = 10;
}
//ThreadDemo10
public class ThreadDemo10 {
public static void main(String[] args) {
//每次只能通过一个食物
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1) ;
Cook cook = new Cook(arrayBlockingQueue);
Foodie foodie = new Foodie(arrayBlockingQueue);
cook.start();
foodie.start();
}
}
这里解释一下,为啥没有锁关键字,其实已经在队列方法中中创建了
然后这里的队列是由我们传递进去的通过构造方法,确保俩个线程都使用同一个队列
然后我们来看看运行结果
注意这里运行结果为啥会出现俩个,是因为我们锁是在队列中,而不是锁住的整个方法或者代码块,所以打印语句可能会出现多次打印情况,但是运行是不影响的。不可以在加个锁进行嵌套否则会出现死锁
线程状态
注意这里Java中是没有运行状态的,原因是因为抢线程抢到CPU后JVM虚拟机将当前线程交给操作系统管理
但是在八股文中主要是六种状态(这里参照的是Java核心卷I):
- New(新建)----------------> 创建线程
- Runnable(可运行)----------------> start方法
- Blocked(阻塞)----------------> 无法获得锁对象
- Waiting(等待)----------------> wait方法
- Timed waiting(计时等待)----------------> sleep方法
- Terminated(终止)----------------> 全部代码运行完毕
线程池
概念
理解线程池概念,我们先来看看一张图
这个碗就是我们的线程A,当我们用的时候,我们就去买一个碗
然后我们吃完饭后就把碗摔掉(线程消失)
当我们又需要吃饭的时候怎么办呢?我们又要去买
如此重复,造成大量的浪费。那么我们怎么解决这个问题呢
我们弄一个碗柜(线程池),这样每次吃完我们就把碗放回到碗柜中,就不会浪费碗了
那么接下来我们来看看其他效果图帮助我们更好的理解
可以看上面这张图,我们创建了一个线程池,最大线程数量为3
此时有五个任务需要执行,但是线程就只有三个,后俩个只能等着
核心原理
- 创建一个池子,池子中是空的
- 提交任务的时候,池子会创建新的线程池对象,任务执行完毕,线程会归还给池子,下次在提交任务,也不需要新的线程,直接复用已有的线程即可
- 但是如果提交任务,池子中没有空闲的线程,也无法创建新的线程,任务就会排队等待
代码实现
Executors:
线程池的工具类通过调用方法返回不同类型的线程池对象。
注意第一个也是有上线的,是Int的最大数,但是相信没有电脑能抗住这么多个线程数。
线程池只能接受Callable 或者 Runnable
newCachedThreadPool()
那么我们来看实现Demo
//MyRunnable
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i < 100 ; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
//MyThread
public class MyThread {
public static void main(String[] args) {
//创建线程池
ExecutorService pool = Executors.newCachedThreadPool();
//提交任务
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
//销毁线程池
// pool.shutdown();
}
}
注意这里的销毁线程池一般是不用的,如果销毁了整个线程池就没有了
可以看到变化,如果大家想看到线程复用的效果只需要在每次提交后休眠几秒
即可看到效果
可以看到任务都是线程1执行的
newFixedThreadPool()
创建该线程需要指定线程最大数量,其他代码可以不用更改
大家可以运行一下看看是不是只有3个线程在运行
我们可以Debug运行一下
可以看如上图,我们代码运行完第三个 活动线程已经达到最大三个,等待任务还没有出现,我们继续运行
可以看到已经有一个任务在等待了
自定义线程池
Java给我们提供的创建线程方式是方便了我们使用,但是不够灵活,例如我们排队时间过多时,我们想拒绝访问,是无法实现的
同样我们来看一组图片,来方便我们自定义线程池
当我们开一家高级饭店,进行一对一服务的时候,不可能来多少桌就找多少人,也不可能一直没人,所以我们规定有正式员工和临时工,就如同外包好吧。
那么如果餐厅如果生意太好,我们无法接待那么多,也需要有规则
比如在箭头处就让其他顾客回家,不要继续等待,这就是一个线程池的使用
同样我们来看下面这张图
这里需要我们注意的点
- 临时线程不会第一时间创建,必须要等队伍长度达到上线,才会创建临时线程
- 任务执行顺序不会根据我们提交的顺序提交,可能后提交的先被执行完
- 如果提交任务超过了 核心线程+ 临时线程+队伍长度,就会触发拒绝策略
拒绝策略
代码实现
//myRunnable
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i < 100 ; i++) {
System.out.println(Thread.currentThread().getName() + " " +i );
}
}
}
//MyThread2
public class MyThread2 {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数
6,//最大线程数
60,//最大存活时间
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(3),//指定队伍长度
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//拒绝策略
);
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
}
}
运行结果大家自行测试就行