1.1 JUC
在 Java 5.0 提供了 java.util.concurrent
(简称JUC)包,在此包中增加了在并发编程中很常用的工具类。此包包括了几个小的、已标准化的可扩展框架,并提供一些功能实用的类,没有这些类,一些功能会很难实现或实现起来冗长乏味。
1.2 进程和线程
进程:
进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:
- 通常在一个进程中可以包含一个或若干个线程。线程可以利用进程所拥有的资源,在引入线程的操作系统中。
- 通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和调度的基本单位。
- 由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
生活实例:
- 大四的时候写论文,用word写论文,同时用QQ音乐放音乐,同时用QQ聊天,这些是多个进程。
- 使用QQ,查看进程一定有一个QQ.exe的进程,我可以用qq和A文字聊天,和B视频聊天,给C传文件,给D发一段语音,QQ支持录入信息的搜索,这些是进程中的线程。
- word如没有保存,停电关机,再通电后打开word可以恢复之前未保存的文档,word也会检查你的拼写,其中包含两个线程:容灾备份,语法检查
1.3 并发和并行
核心思想:
- 并发是指多个任务在同一个时间段内交替执行
- 并行是指多个任务在同一个时刻同时执行
并发(Concurrency):
并发指的是多个任务在同一个时间段内交替执行。在并发场景下,多个任务可以同时存在。但实际上每个任务只能以一种交替的方式执行,即任务之间可能会进行快速的切换或分时执行。
这种交替执行的方式可以通过操作系统的时间片轮转或线程调度算法来实现。
例如在多线程编程中,可以使用并发来提高应用程序的响应能力和资源利用率。虽然并发可以使得多个任务同时存在,但由于任务之间的线程切换开销,实际上可能会出现一些等待时间,因此并发并不一定能够加速任务的执行。
例子:
- 限量抢购
- 春运抢票
- 电商秒杀
并行(Parallelism):并行指的是多个任务在同一时刻同时执行。在并行场景下,多个任务可以同时进行,每个任务拥有自己的处理单元(例如CPU核心),从而实现真正的并行执行。并行可以通过硬件支持和多线程编程技术来实现。
并行通常用于同时处理大规模的任务集合,可以显著提高任务的执行速度和吞吐量。通过将任务划分为更小的子任务,并分配给不同的处理单元并行执行,可以加快整体任务的完成时间。
例子:
一边烧水,一边洗衣服,同时看电视
1.4 同步和异步
同步和异步是两种不同的工作方式,处理任务的执行方式是否与顺序有关:
同步:在同步操作中,任务按照顺序依次执行,每个任务必须等待前一个任务完成后才能开始执行。这意味着任务之间是连续的、有序的,一个任务的完成通常会阻塞后续任务的执行,直到它自己完成。
例子:
想象你在餐馆点餐,每个顾客都必须等待前面的顾客点完餐并完成付款,然后才能点餐。
这是一个同步的过程,每个任务(点餐和付款)按顺序执行,一个任务完成后才能开始下一个。
异步:在异步操作中,任务可以同时执行,而不需要等待前一个任务完成。任务的执行不按照固定的顺序,可以随时开始和结束,任务之间是相互独立的。
例子:
想象你使用手机上的社交媒体应用,你可以同时发送消息给多个朋友,而不必等待一个朋友回复后才能给下一个朋友发送消息。
这是一个异步的过程,每个任务(发送消息给不同的朋友)可以独立执行,而不会相互阻塞。
1.5 线程状态
新建
(New):线程被创建但尚未启动执行。就绪
(Runnable):线程等待CPU时间片(其他条件都满足)以便执行,也就是处于就绪状态阻塞
(Blocked):线程暂停执行,通常是因为等待某些条件满足,例如等待I/O操作完成、等待锁释放等,阻塞是一种主动的行为无限期等待
(Waiting):线程无限期地等待某个条件的发生,通常需要其他线程来唤醒它有限期等待
(Timed Waiting):线程等待一段时间,超过指定时间后会自动唤醒终止
(Terminated):线程执行完成或者异常终止,进入终止状态
在 Thread 类中可以看到线程的状态
线程之前状态转换的关系:
1.6 创建线程
继承 Thread 类
class T1 extends Thread {
@Override
public void run() {
System.out.println("线程名字:" + Thread.currentThread().getName());
}
}
public class ThreadDemo {
public static void main(String[] args) {
T1 t1 = new T1();
t1.setName("T1线程");
t1.run();
t1.start();
}
}
从图中的结果不难看出,run
和start
的运行不是一回事
- run 不会启动一个新线程,只是执行主线程,顺序执行不会并发
- start 会创建一个新线程,并且进入
就绪
状态,JVM 调度这个线程,其并发执行run
方法中的代码,同样的方法,却并行
如果想并行执行线程,那就是用 start 方法,而不是 run 方法。
实现 Runnable 接口
三种创建方式
- 新建类实现 Runnable 接口
- 匿名内部类
- Lambda 表达式
通过这种方式,发现没有start
方法,而且就是在执行主线程
class T2 implements Runnable {
@Override
public void run() {
System.out.println("线程名字:" + Thread.currentThread().getName() + " Runnable接口");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
T2 t2 = new T2();
t2.run();
}
}
这时候在定义一个Thread
类时发现是可以传递,实现Runnable
接口的 Thread 类的
如果通过直接创建出来,就可以实现继承 Thread 类的创建方式,并且结果也如我们所料。
class T2 implements Runnable {
@Override
public void run() {
System.out.println("线程名字:" + Thread.currentThread().getName() + " Runnable接口");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
new Thread(new T2(), "T1线程").start();
new Thread(new T2(), "T2线程").run();
}
}
匿名内部类
只是将新创建类的方式,放到了里面而已。
class T2 implements Runnable {
@Override
public void run() {
System.out.println("线程名字:" + Thread.currentThread().getName() + " Runnable接口");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程名字:" + Thread.currentThread().getName() + "匿名内部类 Runnable接口");
}
},"T1线程").start();
new Thread(new T2(), "T2线程").run();
}
}
Lambda 表达式
package com.anran.user;
public class ThreadDemo2 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程名字:" + Thread.currentThread().getName() + " 匿名内部类 Runnable接口");
}
},"T1线程").start();
new Thread(() -> {
System.out.println("线程名字:" + Thread.currentThread().getName() + " Lambda表达式 Runnable接口");
}, "T2线程").start();
}
}
实现 Callable 接口
见5、Callable
接口,借助FutureTask
类实现。
Supplier<>
类就不算了吗?Supplier接口本身并不是一种直接创建线程的方式,但可以通过与其他线程创建机制结合使用来间接实现线程创建。
Java 线程池
见后续线程池ThreadPool
。
ComplatableFuture
是对Future
接口的一种改进,FutureTask
也是其一种实现类。
1.7 Synchronized 隐式锁
排他锁、悲观锁、独占锁、互斥锁
synchronized:方法、代码块
方法:
- 静态方法,锁的是当前类的对象
- 非静态方法,锁的是当前的this对象
静态代码块:
//当前实例对象是锁
synchronized (this){
}
//当前类对象是锁
synchronized (Phone.class){
}
Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
- 对于普通同步方法,锁是
当前实例对象
。 - 对于
静态
同步方法,锁是当前类的Class对象
。 - 对于同步代码块,锁是Synchonized括号里配置的对象
- 而静态同步方法(Class对象锁)与非静态同步方法(实例对象锁)之间是不会有竞争的。