文章目录
- 并发编程
- 一、线程的基本概念
- 1 基础概念
- 1.1 进程和线程
- 1.2 多线程
- 1.3 串行、并行、并发
- 1.4 同步异步、阻塞非阻塞
- 2 线程的创建
- 2.1 继承Thread类,重写run方法
- 2.2 实现Runnable接口,实现run方法
- 2.3 实现Callable接口,实现call方法,配合FutureTask
- 2.4 基于线程池构建线程
- 3 线程的使用
- 3.1 线程的使用
- 3.2 线程常用的方法
- 3.2.1 获取当前线程
- 3.2.2 线程的名字
- 3.2.3 线程的优先级
- 3.2.4 线程的让步
- 3.2.5 线程的休眠
- 3.2.6 线程的抢占
- 3.2.7 线程守护
- 3.2.8 线程的等待和唤醒
- 3.3 线程的结束方式
- 3.3.1 stop 方法(不用)
- 3.3.2 使用共享变量(很少使用)
- 3.3.3 interrupt 方法
并发编程
一、线程的基本概念
1 基础概念
1.1 进程和线程
什么是进程?
- 进程是指运行中的程序。比如我们使用的钉钉、浏览器,需要启动这个程序,操作系统会给这个程序分配一定的资源(占用内存资源)。
什么是线程?
- 线程是CPU调度的基本单位,每个线程执行的都是某一个进程的代码的某个片段。
举个例子:人和房子
比如,现在有一个 100 平米的房子,这个房子就可以看做是一个进程。房子里有人,人就可以看做是一个线程。人在房子里做一些事,比如:吃饭、学习、睡觉,这个就好像现场在执行某个功能的代码。
- 所谓进程就是线程的容器,需要线程利用进程的一些资源,处理一个代码/指令,最终实现进程所预期的结果。
进程和线程的区别?
- 本质不同:进程是操作系统分配的资源,而线程是CPU调度的基本单位。
- 资源不同:统一个进程下的线程共享进程的一些资源,线程同时拥有自身的独立存储空间,进程之间的资源通常是独立。
- 数量不同:进程就是一个进程,而线程是依附于某个进程的,而且一个进程中至少有一个或多个线程。
- 开销不同:毕竟进程和线程不是一个级别的内容,线程的创建和终止时间是比较短的。而且线程之间的切换比进程之间的切换速度要快很多。而且进程之间的通讯很麻烦,一般要借助内核才可以实现;而线程之间通信,相当方便。
- …
1.2 多线程
什么是多线程?
- 多线程是指,单个进程中同时运行多个线程。
- 多线程的目的就是为了提高CPU的利用率,可以通过避免一些网络IO或磁盘IO等需要等待的操作,让CPU去调度其他线程。这样可以大幅度提升程序的效率,提高用户的体验。
比如:Tomcat就可以做并行处理,提升处理的效率,而不是一个一个排队。
再比如:要处理一个网络等待操作,开启一个线程去处理需要网络等待的任务,让当前业务线程可以继续往下执行逻辑,效率可以得到大幅度提升。
多线程的局限性?
- 如果线程数量特别多,CPU在切换上下文时,会额外造成很大的消耗。
- 任务的拆分需要依赖业务场景,有一些异构化的任务,很难对任务拆分,并不是多线程处理就更好。
- 线程安全问题:虽然多线程带来了一定的性能提升,但是在做一个操作时,多线程如果操作临界资源,可能会发生一些数据不一致的问题,甚至涉及到锁的操作时,会造成死锁的问题。
1.3 串行、并行、并发
什么是串行?
- 串行就是一个一个排队,第一个做完,第二个才上。
什么是并行?
- 并行就是同时处理(一起上!!)
什么是并发?
- 这里的并发并不是三高中的高并发问题,这里是指多线程的并发概念(CPU调度线程的概念)。CPU在极短的时间内,反复切换执行不同的线程,看似好像是并行,但只是CPU的高速切换。
- 并行囊括并发,并行就是多核CPU同时调度多个线程,是真正的多个线程同时执行。单核CPU无法实现并行效果,单核CPU是并发。
1.4 同步异步、阻塞非阻塞
同步与异步?
- 执行完某个功能后,被调用者是否会主动反馈信息。
阻塞与非阻塞?
- 执行完某个功能后,调用者是否需要一直等待结果的反馈。
同步阻塞:比如用锅烧水,水开后不会主动通知你;烧水开始执行后,需要一直等待水烧开。
同步非阻塞:比如用锅烧水,水开后不会主动通知你;烧水开始执行后,不需要一直等待水烧开,可以去执行一下其他的操作,但是需要时不时查看水开了没。
异步阻塞:比如用锅烧水,水开后会主动通知你水烧开了;烧水开始执行后,需要一直等待水烧开。
异步非阻塞:比如用锅烧水,水开后会主动通知你水烧开了;烧水开始执行后,不需要一直等待水烧开,可以去执行一下其他的操作。这个效果是最好的,平时开发时,提升效率最好的方式就是采用异步非阻塞的方式处理一些多线程的任务。
2 线程的创建
线程的创建分为三种方式:
2.1 继承Thread类,重写run方法
- 启动线程时,调用 start() 方法,这样会创建一个新的线程,并执行线程的任务。
- 如果直接调用 run() 方法,这样仅仅只会让当前执行 run 方法中的业务逻辑。
public class MyTest {
public static void main(String[] args) {
MyJob t1 = new MyJob();
t1.start();
for (int i = 0; i < 100; i++) {
System.out.println("main:" + i);
}
}
}
class MyJob extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("MyJob:" + i);
}
}
}
2.2 实现Runnable接口,实现run方法
public class MyTest {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread t1 = new Thread(runnable);
t1.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main:" + i);
}
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("MyRunnable:" + i);
}
}
}
最常用的方式:
// 匿名内部类方式:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("匿名内部类:" + i);
}
}
});
// Lambda 方式:
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("Lambda:" + i);
}
});
2.3 实现Callable接口,实现call方法,配合FutureTask
- Callable 一般用于有返回值的非阻塞的执行方法——同步非阻塞
public class MyTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建 MyCallable
MyCallable callable = new MyCallable();
// 2. 创建 FutureTask,传入Callable
FutureTask task = new FutureTask<>(callable);
// 3. 创建 Thread 线程
Thread t1 = new Thread(task);
t1.start();
// 4. 启动线程
t1.start();
// 5. 做一些操作...
// 6. 获取结果
Object count = task.get();
System.out.println("总和为:" + count);
}
}
class MyCallable implements Callable {
@Override
public Object call() throws Exception {
int count = 0;
for (int i = 0; i < 100; i++) {
count += i;
}
return count;
}
}
2.4 基于线程池构建线程
- 底层其实是一种,实现 Runnable。
3 线程的使用
3.1 线程的使用
网上对线程状态的描述有很多,有说 5 种的、6中的、7种的,都可以接受。
- 5 种状态:一般是针对传统的线程状态来说(操作系统层面)
- Java中给线程定义了 6 种状态:
- NEW:Thread 对象被创建出来了,但是还没有执行start方法。
- RUNNABLE:Thread 对象调用了 start 方法,就为 RUNNABLE 状态(CPU调度/没有调度)。
- BLOCKED、WAITING、TIMED_WAITING:都可以理解为是阻塞、等待状态,因为处于这三种状态下,CPU都不会调度当前线程。
- BLOCKED:是 synchronized 没有拿到同步锁,被阻塞的情况;
- WAITING:调用 wait 方法就会处于 WAITING 状态,需要被手动唤醒;
- TIMED_WAITING:调用 sleep 方法或者 join 方法,会被自动唤醒,无需手动唤醒。
- TERMINATED:run方法执行完毕,线程生命周期到头了。
代码演示
- NEW:
public static void main(String[] args) {
Thread t1 = new Thread(() -> {});
System.out.println(t1.getState());
}
- RUNNABLE:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
- BLOCKED:
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
// t1 线程拿不到锁资源,导致变为 BLOCKED状态
synchronized (obj) {
}
});
// main线程拿到obj的锁资源
synchronized (obj) {
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
}
- WAITING:
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
- TIMED_WAITING:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
- TERMINATED:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
System.out.println(t1.getState());
}
3.2 线程常用的方法
3.2.1 获取当前线程
- Thread 的静态方法,获取当前线程对象
public static void main(String[] args) throws InterruptedException {
Thread currentThread = Thread.currentThread();
System.out.println(currentThread);
// "Thread[" + getName() + "," + getPriority() + "," + group.getName() + "]";
// Thread[main,5,main]
}
3.2.2 线程的名字
- 在构建 Thread 对象完毕后,一定要设置一个有意义的名称,方便后期排查错误
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
});
t1.setName("模块-功能-计数器");
t1.start();
}
3.2.3 线程的优先级
- 其实就是 CPU 调度线程的优先级,Java 中给线程设置的优先级有 10 个级别,从 1 ~ 10 任取一个整数,如果超出这个范围,会报出参数异常的错误。
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("t1: " + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("t2:" + i);
}
});
t1.setPriority(1);
t2.setPriority(10);
t2.start();
t1.start();
}
3.2.4 线程的让步
- 可以通过 Thread 的静态方法 yield,让当前线程从运行状态转变为就绪状态。
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
if (i == 50) {
Thread.yield();
}
System.out.println("t1:" + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("t2:" + i);
}
});
t2.start();
t1.start();
}
3.2.5 线程的休眠
- Thread 的静态方法,让线程从运行状态变为等待状态。
- sleep 有两个方法重载:
- 第一个就是 native 修饰的,让线程转为等待状态的效果;
- 第二个是可以传入毫秒和一个纳秒的方法(如果纳秒值大于等于 0.5ms,就给休眠的毫秒值+1;如果传入的毫秒值为0,纳秒值不为0,就休眠1ms)
- sleep会抛出一个 InterruptedException
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(System.currentTimeMillis());
}
3.2.6 线程的抢占
- Thread 的非静态方法 join,需要再某一个线程下去调用这个方法。
- 如果main线程中调用了 t1.join(),那么 main 线程会进入到等待状态,需要等待 t1 线程全部执行完毕,再恢复到就绪状态等待 CPU 调度。
- 如果main线程中调用了 t1.join(2000),那么 main 线程会进入到等待状态,需要等待 t1 执行 2s 后,恢复到就绪状态等待 CPU 调度。如果在等待期间,t1 已经结束了,那么 main 线程自动变为就绪状态等待CPU调度。
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t1:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println("main:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 1) {
try {
t1.join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.2.7 线程守护
- 默认情况下,线程都是非守护线程。JVM 会在程序没有非守护线程时,结束掉当前 JVM。
- 主线程默认是非守护线程,如果主线程执行结束,需要查看当前 JVM 内是否还有非守护线程,如果没有 JVM 会直接停止。
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("t1:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.setDaemon(true);
t1.start();
}
3.2.8 线程的等待和唤醒
- 可以让获取 synchronized 锁资源的线程,通过 wait 方法进入到锁的等待池,并且会释放锁资源。
- 可以让获取 synchronized 锁资源的线程,通过 notify 或者 notifyAll 方法,将等待池中的线程唤醒,添加到锁池中。
- notify 会随机的唤醒等待池中的一个线程到锁池中,notifyAll 会将等待池中的全部线程都唤醒,并添加到锁池中。
- 在调用 wait 方法和notify、notifyAll 方法时,必须在 synchronized 修饰的代码块或者方法内部才可以,因为要操作基于某个对象的锁的信息维护。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
sync();
}, "t1");
Thread t2 = new Thread(() -> {
sync();
}, "t2");
t1.start();
t2.start();
Thread.sleep(12000);
synchronized (MyTest.class) {
MyTest.class.notifyAll();
}
}
private static synchronized void sync() {
try {
for (int i = 0; i < 10; i++) {
if (i == 5) {
MyTest.class.wait();
}
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
3.3 线程的结束方式
- 线程结束的方式很多,最常用的是让线程的 run 方法结束,无论是 return 结束,还是抛异常结束,都可以。
3.3.1 stop 方法(不用)
- 强制让线程结束,无论你在干嘛,不推荐使用这种方式,但是,它确实可以把线程干掉。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(500);
t1.stop();
System.out.println(t1.getState());
}
3.3.2 使用共享变量(很少使用)
- 这种方式用的也不多,有的线程可能会通过死循环来保证一直运行,这个时候可以通过修改共享变量来破坏死循环,让线程退出循环,结束 run 方法。
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) {
// 处理任务
}
System.out.println("任务结束");
});
t1.start();
Thread.sleep(500);
flag = false;
}
3.3.3 interrupt 方法
- 共享变量方式
public static void main(String[] args) throws InterruptedException {
// 线程默认情况下,interrupt标记位:false
System.out.println(Thread.currentThread().isInterrupted());
// 执行interrupt之后,再次查看打断信息
Thread.currentThread().interrupt();
// interrupt标记为:true
System.out.println(Thread.currentThread().isInterrupted());
// 返回当前下线程,并归位为 false,interrupt标记为:true
// 已经归位了
System.out.println(Thread.currentThread().isInterrupted());
// ====
Thread t1 = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 处理业务
}
System.out.println("t1结束");
});
t1.start();
Thread.sleep(500);
t1.interrupt();
}
- 通过打断 WAITING 或者 TIMED_WAITING 状态的线程,从而抛出异常自行处理,这种停止线程的方式是最常用的一种,在框架和 JUC 中也是最常见的。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
// 获取任务
// 拿到任务,执行任务
// 没有任务了,让线程休眠
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("基于打断形式结束当前线程");
return;
}
}
});
t1.start();
Thread.sleep(500);
t1.interrupt();
}
wait 和 sleep 的区别?
- 单词不一样;
- sleep 属于 Thread 类中的静态方法,wait 属于 Object 类的方法;
- sleep 数据 TIMED_WAITING,自动被唤醒,wait 属于 WAITING,需要手动唤醒;
- sleep 方法在持有锁时,执行后不会释放锁资源,wait 在执行后会释放锁资源;
- sleep 可以在持有锁或者不持有锁时执行,wait 方法必须在持有锁时才可以执行。
wait 方法会将持有锁的线程从owner扔到WaitSet集合中,这个操作是在修改ObjectMonitor对象,如果没有持有synchronized锁的话,是无法操作ObjectMonitor对象的。