文章目录
- Java多线程
-
- 多线程概述
-
- 线程与进程
- 线程调度
- 同步与异步
- 并发与并行
- 线程阻塞
- Java 多线程的实现
-
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 `Callable`接口
- Thread 类讲解
-
- 线程状态`currentThread()`
- 线程休眠`sleep()`
- 线程中断`interrupt()`
- 守护线程`setDaemon()`
- 主线程之前完成`join()`
- 线程安全问题
-
- 线程不安全的演示
- 解决方法 1:同步代码块
- 解决方法 2:同步方法
- 解决方法 3:显示锁 Lock
- 更多 synchronized 用法
- 显示锁的问题
-
- 公平锁和非公平锁
- 怎么实现公平锁
- 多线程经典问题
-
- 线程死锁
- 多线程通信
-
- 生产者和消费者
- 线程的状态
- 线程池
-
- 概念
- 好处
- 线程池的分类
Java多线程 多线程概述 线程与进程
进程
- 是内存中运行的应用程序,每个进程都有一个独立的内存空间
- 值进程的执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程
- 线程实际上是在进程基础上的进一步划分,一个进程启动之后,里面若干的执行路径又可以划分成若干个线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eoKunGnP-1620203162745)(/Users/faro_z/Library/Application Support/typora-user-images/image-20210504211610442.png)]
分时调度
- 所有线程,轮流使用 CPU,平均分配每个线程占用 CPU 的时间
- 当 CPU 空闲的时候,CPU 会抛出一个时间片,谁抢到就是谁的。优先让优先级高的线程使用 CPU;如果线程优先级相同,那么,会随机选择一个(线程随机性),Java 使用的就是抢占式调度
- CPU 使用抢占式调度模式在多个线程之间高速切换,对于 CPU 而言,某时刻,只能执行一个线程,而 CPU 在多个线程切换的速度很快,在我们看来,就像是多个线程在同时执行一样。其实多线程程序并不能提高程序的运行速度(甚至会因为切换的时候的时间消耗,使得总耗时还边长),但是能提高程序的运行效率,让 CPU 使用率更高。
**同步:**排队执行,效率低但是安全
**异步:**同时执行,效率高但是数据不安全
并发与并行
【Java|Java 多线程 万字最详解】并发:指两个或多个事件,在==同一时间段==内发生。(比如说一天内,一小时内)
**并行:**指两个或多个事件,在同一时间发生(同时发生)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UZDhhlwB-1620203162747)(/Users/faro_z/Library/Application Support/typora-user-images/image-20210504212449234.png)]
线程阻塞
一般,比较耗时的操作,可以看成是线程阻塞
比如说,我们要等待用户输入,那么,那一段线程,就是阻塞了
Java 多线程的实现 java 中实现多线程,一共有三种方法
- 继承 Thread 类
- 实现 Runnable 接口
- 实现Callable 接口
public class ThreadDemo {
public static void main(String[] args) {
new ThreadClass().start();
for (int i = 0;
i < 10;
i++) {
System.out.println("锄禾日当午"+i);
}
}
}class ThreadClass extends Thread {@Override
public void run() {
for (int i = 0;
i < 10;
i++) {
System.out.println("汗滴禾下土"+i);
}
}
}
文章图片
文章图片
每个线程都有自己的栈空间,但是共用一个堆空间
实现 Runnable 接口
public class RunnableDemo {
public static void main(String[] args) {//子线程
new Thread(() -> {
for (int i = 0;
i < 10;
i++) {
System.out.println("汗滴禾下土"+i);
}
}, "子线程").start();
//主线程(的一部分,整个 main 都是主线程)
for (int i = 0;
i < 10;
i++) {
System.out.println("锄禾日当午"+i);
}
}
}
实现
Callable
接口Callable,是带返回值的线程
上面两个实现方法,可以看成主线程之外的子线程去完成任务,和主线程无关了
但是 Callable,相等于主线程让子线程去完成任务,等到子线程任务完成了,再给主线程返回一个结果。比如说,主线程可以派发 1000 个任务,给 1000 个线程,等着 1000 个任务执行完毕了,可以获得 1000 个结果。
Callable 的使用步骤如下:
- 编写实现 Callable 接口,实现 call 方法
class Xxx implements Callable {
@Override
public call() throws Exception {
return T;
}
}
- 创建 FutureTask 对象,并传入编写的 Callable 对象
FutureTask task = new FutureTask<>(callable);
- 通过 Thread,启动线程
new Thread(task).start
获取子线程返回结果的方法如下:
- 使用 FutureTask 的
get()
方法
get()
,会让主线程等待子线程执行完毕,然后去获得方法public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyCallable cl = new MyCallable();
FutureTask task = new FutureTask<>(cl);
//执行子线程
new Thread(task).start();
Integer res = task.get();
System.out.println("子线程计算结果为:"+res);
for (int i = 0;
i < 10;
i++) {
Thread.sleep(100);
System.out.println("main"+i);
}}
}class MyCallable implements Callable {@Override
public Integer call() throws Exception {
for (int i = 0;
i < 10;
i++) {
Thread.sleep(1000);
System.out.println("子线程正在执行"+i);
}
return 100;
}
}
文章图片
- 使用使用 FutureTask 的
isDone()
方法
public class CallableDemo2 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyCallable2 cl = new MyCallable2();
FutureTask task = new FutureTask<>(cl);
new Thread(task).start();
for (int i = 0;
i < 10;
i++) {
Thread.sleep(1000);
if (task.isDone()) {
Integer res = task.get();
System.out.println("子线程已经执行完了,结果是:"+res);
}
System.out.println("main"+i);
}}
}class MyCallable2 implements Callable {@Override
public Integer call() throws Exception {
for (int i = 0;
i < 10;
i++) {
Thread.sleep(500);
// System.out.println("子线程正在执行"+i);
}
return 100;
}
}
文章图片
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r16OCaP5-1620203162753)(/Users/faro_z/Library/Application Support/typora-user-images/image-20210505154137082.png)]
Thread 类讲解 线程状态
currentThread()
获取当前线程
public class ThreadDemo1 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
}, "子线程").start();
}
}
文章图片
线程休眠
sleep()
线程休眠
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0;
i < 10;
i++) {
Thread.sleep(1000);
System.out.println("hahaha");
}
}
}
线程中断
interrupt()
在早期,有个
stop()
方法,但是,使用 stop 强行让一个线程死亡,可能导致其没来得及释放资源,导致资源的浪费。所以,我们要使用
interrupt()
来让线程察觉到外部标记,然后自己决定死亡public class ThreadDemo2 {
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {
for (int i = 0;
i < 10;
i++) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+":"+i);
//这里的 InterruptedException ,就是检查有没有被中断的
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println("检测到打扰标记,线程死亡!");
/**
* 在死亡前,可以在这里进行资源释放
* 类似于交代后事
*/
System.out.println("爷要 si 了,资源都释放掉了!");
//如果要线程死亡,我们只要让 run 方法 return 就好了
return;
}
}
}, "子线程");
t1.start();
for (int i = 0;
i < 5;
i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+":"+i);
}/**
* 给线程 t1 添加中断标记
*/
t1.interrupt();
}
}
文章图片
文章图片
守护线程
setDaemon()
用户线程:当一个进程不包括任何一个存货的用户线程时,进行结束
**守护线程:**当最后一个用户线程结束后,守护线程自动死亡
==注意:==守护线程的设置,一定要在线程启动之前
public class ThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0;
i < 10;
i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}, "守护线程");
/**
* 将 t1 设置为守护线程
* 一定要在 start 之前,进行守护线程的设置
*/
t1.setDaemon(true);
t1.start();
for (int i = 0;
i < 5;
i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
文章图片
主线程之前完成
join()
join()
的作用,是保证子线程,在主线程之前完成如果没有设置超时,那么,主线程在子线程完成之前,不会与之抢夺时间片
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {
for (int i = 0;
i < 10;
i++) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "子线程");
t.start();
t.join();
for (int i = 0;
i < 10;
i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
文章图片
join(int time)
还可以设置子线程排在主线程前面的时间:其他代码不变,这里,我们设置子线程排在主线程前面的时间为
文章图片
并为主线程每次打印增加等待
文章图片
可以看到,在两秒后,主线程便开始与子线程抢夺时间片:
文章图片
线程安全问题 线程不安全的演示
public class ThreadDemo4 {
public static void main(String[] args) {MyThread t = new MyThread();
new Thread(t,"A").start();
new Thread(t,"B").start();
new Thread(t,"C").start();
}
}class MyThread implements Runnable {private int ticket = 10;
@Override
public void run() {
while (ticket>0) {
System.out.println(Thread.currentThread().getName()+"开始卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName()+"余票:"+ticket);
}
}
}
文章图片
解决方法 1:同步代码块
public class ThreadDemo5 {public static void main(String[] args) {MyThread2 t = new MyThread2();
new Thread(t,"A").start();
new Thread(t,"B").start();
new Thread(t,"C").start();
}}class MyThread2 implements Runnable {//临界资源区private int ticket = 10;
/*** 锁的对象,是要是唯一的,都可以* 有时候为了不使用自定义的锁,甚至可以使用 Class 作为锁* 比如使用 MyThread2.class*/private static final Object lock=new Object();
@Overridepublic void run() {while (true) {synchronized (lock) {if (ticket<=0) break;
else {System.out.println(Thread.currentThread().getName()+"开始卖票");
try {Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}ticket--;
System.out.println(Thread.currentThread().getName()+"余票:"+ticket);
}}}}}
文章图片
解决方法 2:同步方法
即在方法前,加上 synchronized 关键字
同步方法,其实使用的是隐式锁
如果是同步方法,锁的是当前对象
如果是静态方法,锁的是该类的 Class
解决方法 3:显示锁 Lock
使用 JUC 里的 lock
public class ThreadDemo6 {
public static void main(String[] args) {MyThread3 t = new MyThread3();
new Thread(t,"A").start();
new Thread(t,"B").start();
new Thread(t,"C").start();
}
}class MyThread3 implements Runnable {private int ticket = 10;
private Lock lock = new ReentrantLock();
@Override
public void run() {while (true) {
lock.lock();
if (ticket<=0) break;
else {
System.out.println(Thread.currentThread().getName()+"开始卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName()+"余票:"+ticket);
}
lock.unlock();
}
}
}
文章图片
更多 synchronized 用法
之前我写过一个详细的 synchronized 用法的博客,点此跳转
或者看
java/JUC/多线程.md
显示锁的问题 公平锁和非公平锁
**公平锁:**先来,先得到锁
**非公平锁:**大家争抢这个锁
我们一般使用的,都是非公平锁(synchronized,ReentrantLock)
怎么实现公平锁
在构造 ReentrantLock 时,传入一个 true,表示这是一个公平锁
Lock lock = ReentrantLock(true);
多线程经典问题 线程死锁
什么是死锁?
线程 A,拿到 A锁,执行任务,中途又需要 B 锁
线程 B,拿到 B锁,执行任务,中途又需要 A 锁
如果这两个线程是同时执行的,那 A拿不到 B 锁,B拿不到 A 锁,就会相互僵持,出现死锁
多线程通信
比如说我们想要在下载完以后,自动播放电影
那么,当下载线程结束以后,要去通知播放线程
这,就是多线程通信
多线程通信的实现,是:
Object 里的
wait()
和notify()/notifyAll()
方法生产者和消费者 有了
wait()
和notify()/notifyAll()
方法,我们就可以解决生产者和消费者问题所谓生产者和消费者问题,就是设置一个缓存,当缓存为空,消费者不能消费,生产者必须生产;当缓存已经满了,生产者必须停止,消费者必须消费;剩下的时候,生产者、消费者可以随意。
因为这个缓存是临界区,所以必须加锁
但是,光加锁,不能解决生产者、消费者问题。如果在任何时候,都让生产者和消费者自由争抢时间片,那如果缓存已满,可能生产者还会抢到时间片,这样,就会让缓存溢出。
这个时候,就需要用到线程通信,来解决生产者和消费者问题。
我们定义三个类:
**Cook:**生产者,每次做和上一次不一样的菜
**Waiter:**消费者,每次端出菜
**Food:**食物,缓存长度为 1,即没有菜的时候,cook必须做菜,waiter 不能端菜;有菜的时候,cook 不能做菜,waiter 必须端菜
public class ProducerAndCustomer {
public static void main(String[] args) {
Food food = new Food();
new Thread(new Cook(food)).start();
new Thread(new Waiter(food)).start();
}
}class Waiter implements Runnable {private Food food;
public Waiter(Food food) {
this.food = food;
}@Override
public void run() {
for (int i = 0;
i < 100;
i++) {
/**
* 这里因为要访问临界资源 isEmpty,所以必须加锁
*/
synchronized (food) {
if (!food.isEmpty) {
food.get();
food.isEmpty=true;
food.notifyAll();
try {
food.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}class Cook implements Runnable{private Food food;
public Cook(Food food) {
this.food = food;
}@Override
public void run() {
for (int i = 0;
i < 100;
i++) {
/**
* 这里因为要访问临界资源 isEmpty,所以必须加锁
*/
synchronized (food) {
if (food.isEmpty) {
if (i%2==0) {
food.setName("煎饼果子");
food.setTest("咸味");
} else {
food.setName("豆腐脑");
food.setTest("超甜味");
}System.out.println("厨师把菜做好了,菜品为:"+food.toString());
//厨师做好了菜,将盘子设置为不空
food.isEmpty=false;
//唤醒其他线程
food.notifyAll();
//当前线程休眠
try {
food.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}class Food {
private String name;
private String test;
public boolean isEmpty=true;
public Food(String name, String test) {
this.name = name;
this.test = test;
}public Food() {
}public String getName() {
return name;
}public void setName(String name) {
this.name = name;
}public String getTest() {
return test;
}public void setTest(String test) {
this.test = test;
}public void get() {
System.out.println("服务员端走了菜:"+this.toString());
}@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("名称:").append(this.name).append("\n")
.append("味道:").append(this.test).append("\n");
return sb.toString();
}
}
结果:
我们可以看到,是严格按照先做菜,再端菜的顺序执行的。
文章图片
线程的状态 线程一共有六种状态
文章图片
线程池 概念
如果我们要使用大量线程,每个线程执行的时间很短,那每次创建、销毁线程,就会浪费大量时间。所以,我们要先提前创建部分线程,要用的时候,去里面取,不用的时候,再放回去
好处
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
线程池的分类
一共有四种线程池
- 缓存线程池
/**
缓存线程池. (长度无限制) 执行流程:
1. 判断线程池是否存在空闲线程
2. 存在则使用
3. 不存在,则创建线程 并放入线程池, 然后使用
* * * * * * */
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中 加入 新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
- 定长线程池
/**
* 定长线程池.
* (长度是指定的数值) * 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
- 单线程线程池
效果与定长线程池 创建时传入数值1 效果一致.
/**
* 单线程线程池. 执行流程:
* 1. 判断线程池 的那个线程 是否空闲
* 2. 空闲则使用
* 4. 不空闲,则等待 池中的单个线程空闲后 使用
*/
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
- 周期性任务定长线程池
文章图片
推荐阅读
- java|java多线程学习万字长文总结
- java多线程|万字梳理java多线程的基础知识
- Java|Java多线程学习总结(全面的万字长篇)
- Java 8 判空新写法。。
- 舒服了,踩到一个关于分布式锁的非比寻常的BUG!
- 软帝学院教你Java Applet基础
- 安卓基础(点击事件)
- 通过 SingleFlight 模式学习 Go 并发编程
- 学习笔记|SpringBoot05(自动配置原理)