JUC 笔记

image-20240701204232476

一.基础

并发和并行

并发:是在同一个实体上的多个事件,是在一台处理器上“同时”处理多个任务,同一时刻只有一个事件发生

并行:是在不同实体上的多个事件,是在多台处理器上同时处理多个任务,同一时刻,大家真的都在做事情

管程

管程(Monitor)Monitor 就是一种同步机制,他的义务是保证同一时间只有一个线程可以访问被保护的数据和代码

JVM 中同步是基于进入和退出监视器对象(管程对象)来实现的,每个对象实例都会有一个 Monitor 对象,底层是 C++ 实现的

用户线程和守护线程

用户线程:是系统的工作线程,它会完成这个程序需要完成的业务操作

守护线程:是一种特殊的线程,为其他线程服务的,在后台默默地完成一些系统性的服务(垃圾回收线程就是最好的例子)。没有服务对象就没有必要继续运行了。(当系统只剩下守护线城时,JVM 就会自动退出)

线程的 daemon 属性:true 表示是守护线程,false 表示不是的

小总结:

  1. 如果用户线程全部结束意味着程序要完成的业务操作已经结束,守护进程随着 JVM 一起结束工作
  2. setDaemon(true) 方法必须在 start 前设置,否则抛出 llegalThreadStateException

Future 接口

Future 接口(FutureTask 实现类)定义了操作执行异步任务的一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等

作用:为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务。提供了一种异步并行计算的功能。

如果主线程需要执行一个耗时很长的任务,可以通过 Future 接口把这个任务放到异步线程中执行,主线程继续处理其他任务或先结束,再通过 Future 获取计算结果。

目的:异步多线程任务执行且返回有结果 三个特点:多线程/有返回/异步任务

FutureTask 实现 RunnableFuture -> Future、Runnable

优点:FutureTask + 线程池能显著提高程序的执行效率

多线程实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class FutureThreadPoolDemo {
@SneakyThrows
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);

long startTime = System.currentTimeMillis();
FutureTask<String> futureTask1 = new FutureTask<>(() -> {
TimeUnit.MICROSECONDS.sleep(500);
return "task1 over";
});

FutureTask<String> futureTask2 = new FutureTask<>(() -> {
TimeUnit.MICROSECONDS.sleep(500);
return "task2 over";
});

threadPool.submit(futureTask1);
threadPool.submit(futureTask2);

System.out.println(futureTask1.get());
System.out.println(futureTask2.get());

TimeUnit.MILLISECONDS.sleep(300);
long endTime = System.currentTimeMillis();
System.out.println("----costTime:" + (endTime - startTime) + "ms");
threadPool.shutdown();
}

@SneakyThrows
private static void m1() {
long startTime = System.currentTimeMillis();

long endTime = System.currentTimeMillis();
// 暂停毫米
TimeUnit.MICROSECONDS.sleep(500);
TimeUnit.MICROSECONDS.sleep(500);
TimeUnit.MICROSECONDS.sleep(500);
System.out.println("----costTime:" + (endTime - startTime) + "ms");
System.out.println(Thread.currentThread().getName() + "\t----end");
}
}

缺点:

  1. get 方法容易阻塞(一旦调用必须等到结果才会离开,不管是否计算完成,容易程序堵塞)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @SneakyThrows
    public static void main(String[] args) {
    FutureTask<String> futureTask = new FutureTask<>(() -> {
    System.out.println(Thread.currentThread().getName() + "\t ----come in");
    TimeUnit.SECONDS.sleep(5);
    return "task over";
    });
    Thread t1 = new Thread(futureTask, "t1");
    t1.start();
    System.out.println(futureTask.get());
    System.out.println(Thread.currentThread().getName() + "\t ----忙其他任务了");
    }

    输出结果(先执行子线程,执行完才执行 main 线程 -> 阻塞了主线程的执行):

    1
    2
    3
    t1	 ----come in
    task over
    main ----忙其他任务了

    解决方法:通过指定等待时间来避免,过时不候

    System.out.println(futureTask.get(3, TimeUnit.SECONDS)); 这样子就会抛出超时异常,因为子线程执行时间为 5 秒

  2. isDone 轮询:轮询会耗费无谓的 CPU 的资源,也不能及时获取计算结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @SneakyThrows
    public static void main(String[] args) {
    FutureTask<String> futureTask = new FutureTask<>(() -> {
    System.out.println(Thread.currentThread().getName() + "\t ----come in");
    TimeUnit.SECONDS.sleep(5);
    return "task over";
    });
    Thread t1 = new Thread(futureTask, "t1");
    t1.start();
    System.out.println(Thread.currentThread().getName() + "\t ----忙其他任务了");
    while(true) {
    if (futureTask.isDone()) {
    System.out.println(futureTask.get());
    break;
    } else {
    TimeUnit.MILLISECONDS.sleep(500);
    System.out.println("正在处理中,不要在催了,越催越慢,再催熄火");
    }
    }
    }

结论:Future 对于结果的获取不是很友好,只能通过阻塞方式或轮询的方式获取任务的结果

CompletableFuture

作用:

  1. 可将多个异步任务的计算结果组合起来,后一个异步任务的计算结果依赖前一个结果
  2. 将两个或多个异步计算合成一个计算,这个几个异步计算相互独立

CompletableFuture 提供了一种类似观察者模式的机制,当任务执行完成会通知监听方

类架构说明

image-20240703210335363

CompletionStage 代表异步计算过程中的某一阶段,一个阶段完成后会触发另一个阶段

获取结果和触发计算

创建异步任务的 2 种方式:

image-20240703211447152

  1. runAsync 无返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @SneakyThrows
    public static void main(String[] args) {
    CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
    System.out.println(Thread.currentThread().getName() + "\t -----come in");
    try {
    TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("task is over");
    });
    System.out.println(completableFuture.get());
    }

    输出内容:

    1
    2
    3
    ForkJoinPool.commonPool-worker-9	 -----come in
    task is over
    null
  2. supplyAsync 有返回值

    • get 方法阻塞主线程
    • get(timeout, TimeUnit) 过时不候,未在规定时间内执行完就抛异常
    • getNow 立即获取结果,计算完就获取计算完的结果,否则就获取设定的值。立即获取结果不阻塞
    • complet 方法返回的是布尔值,true 代表打断了 get 方法,反之代表未打断

CompletableFuture 的优点:

  1. 异步任务结束后会自动回调某个对象的方法
  2. 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可顺序执行
  3. 异步任务出错时,会自动回调某个对象的方法

image-20240704203719101

CompletableFuture get 方法和 join 方法的区别:

  1. 异常处理

    • get() 方法声明了 throws InterruptedException, ExecutionException,因此必须处理这两个异常,否则会编译错误。
    • join() 方法没有声明抛出任何受检异常,因此在处理结果时更加方便,不需要显式捕获异常。
  2. 返回值

    • get() 方法返回 T 类型的结果或抛出异常(ExecutionException 包装实际异常)。
    • join() 方法直接返回 T 类型的结果,或者如果有异常,会抛出 CompletionException,该异常会包装实际异常。
  3. 使用场景

    • 如果你希望在获取结果时能够处理异常,可以使用 join() 方法,因为它简化了异常处理的过程。
    • 如果你需要对 InterruptedExceptionExecutionException 进行精细的处理或转换,你可能更倾向于使用 get() 方法。

对计算结果进行处理

thenApply

  1. 计算结果存在依赖关系,这两个线程串行化
  2. 异常相关,当前出现异常,不走下一步,并且后面都不走

handle

  1. 计算结果存在依赖关系,这两个线程串行化
  2. 异常相关,当前出现异常,不走下一步,但是后面的继续走,根据带的异常参数进一步处理

image-20240704220124675

总结:简单一点理解就是一个是并行的一个是串行的,串行的A步骤G了,就直接去处理异常的步骤了,并行的调用步骤A G了,就当没发生过直接去调用步骤B

任务之间顺序执行

image-20240704221113218

对计算结果进行消费

image-20240705210126340

对计算结果进行选用

那个线程执行快,applyToEither 方法就返回谁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class CompletableFutureFastDemo {
public static void main(String[] args) {
CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
System.out.println("A come in");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "playA";
});

CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
System.out.println("B come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "playB";
});

CompletableFuture<String> result = playA.applyToEither(playB, f -> {
return f + "is winner";
});
System.out.println(Thread.currentThread().getName() + "\t" + result.join());
}
}

对计算结果合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CompletableFutureCombineDemo {
public static void main(String[] args) {
CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t ----启动");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return 10;
});

CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t ----启动");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return 20;
});

completableFuture1.thenCombine(completableFuture2, (x, y) -> {
System.out.println("开始两个结果合并");
return x + y;
});
}

说说那些”锁“事

乐观锁和悲观锁

乐观锁:认为使用数据时不会有别的线程修改数据或资源,因此不会加锁,而是通过版本号或者 CAS 算法来判断有无被修改(适合读操作多的场景)

悲观锁:认为使用数据时有别的线程会修改数据,因此会加锁来保证数据不被其他线程修改(适合写操作多的场景)

Synchronized 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* 题目:谈谈你对多线程锁的理解,8锁案例说明
* 口诀:线程
* 操作
* 资源类
* 8锁案例说明:
* 1.标准访问有 α、b 两个线程,请问先打印邮件还是短信
* 2.sendEmail方法中加入暂停3秒钟,请问先打印邮还是短信
* 3.添加一个普通的hello方法,清间先打印邮件还是hello
* 4.有两部手机,请间先打印邮件还是短信
* 5.有两个静态同步方法,有1部手机,请间先打印邮件还是短信
* 6.有两个静态同步方法,有2部手机,请间先打印那件还是短信
* 7.有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短后
* 8.有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短局
*/

/**
* 笔记总结:
* 1-2:一个对象里有多个 synchronized 方法,锁的是对象,前提方法不被 static 修饰 -> 同一个对象调用两个方法,谁先调用谁先执行
* 3-4:普通方法和同步锁无关 -> 所以普通方法先执行
* 5-6:对于普通同步方法锁的是当前实例对象,对于静态同步方法锁的是当前 Class 对象,对于同步方法块,锁的是 synchronized 括号内的对象
* 7-8:静态同步方法锁的是类,普通同步方法锁的是对象 -> 后者优先级大于前者
*/
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------sendEmail");
}

public synchronized void sendMsg() {
System.out.println("------sendMsg");
}

public void hello() {
System.out.println("-------hello");
}
}

public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();

new Thread(() -> {
phone.sendEmail();
}, "a").start();

try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

new Thread(() -> {
phone2.hello();
}, "b").start();
}
}

公平锁非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的 Lock lock=new ReentrantLock(true); //true表示公平锁,先来先得

1
ReentrantLock lock = new ReentrantLock(true);

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)

1
ReentrantLock lock = new ReentrantLock(false);

如何选择?

如果为了更高的吞吐量,显然是非公平锁合适,因为节省了很多线程切换时间。反之选择公平锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();

new Thread(() -> {
for (int i=0;i<55;i++) {
ticket.sale();
}
}, "a").start();

new Thread(() -> {
for (int i=0;i<55;i++) {
ticket.sale();
}
}, "b").start();

new Thread(() -> {
for (int i=0;i<55;i++) {
ticket.sale();
}
}, "c").start();
}
}

public class Ticket {
private int number = 50;

ReentrantLock lock = new ReentrantLock(true);

public void sale () {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
}
} finally {
lock.unlock();
}
}
}

可重入锁种类

是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前己经获取过还没释放而阻塞。(一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁

隐式锁

隐式锁:即 synchronized 关键字使用的锁默认是可重入锁

同步代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ReEntryLockDemo {
public static void main(String[] args) {
final Object object = new Object();

new Thread(() -> {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t ------外层调用");
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t ------中层调用");
}
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t -----内层调用");
}
}
}, "t1").start();
}
}

同步方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ReEntryLockDemo {
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + "\t -----come in");
m2();
System.out.println(Thread.currentThread().getName() + "\t -----end m1");
}

public synchronized void m2() {
System.out.println(Thread.currentThread().getName() + "\t -----come in");
m3();

}

public synchronized void m3() {
System.out.println(Thread.currentThread().getName() + "\t -----come in");
}

public static void main(String[] args) {
ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
new Thread(() -> {
reEntryLockDemo.m1();
}, "t1").start();
}

private static void reEntryM1 () {

}
}
synchronized 的可重入实现机制

image-20240709213538016

显示锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ReEntryLockDemo {
public static void main(String[] args) {
ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t -----come in 外层调用");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t -----come in 内层调用");
} finally {
// lock.unlock();
}
} finally {
lock.unlock();
}
}, "t1").start();

new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t -----come in 外层调用");
} finally {
lock.unlock();
}
}, "t2").start();
}
}

以上 t2 线程拿不到锁,出现死锁。加锁几次就要释放锁几次。

死锁及排查

死锁案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class DeadLockDemo {
public static void main(String[] args) {
final Object objectA = new Object();
final Object objectB = new Object();

new Thread(() -> {
synchronized (objectA) {
System.out.println(Thread.currentThread().getName() + "\t 自己持有 A 锁,希望获得 B 锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectB) {
System.out.println(Thread.currentThread().getName() + "\t 成功获得 B 锁");
}
}
}, "A").start();

new Thread(() -> {
synchronized (objectB) {
System.out.println(Thread.currentThread().getName() + "\t 自己持有 B 锁,希望获得 A 锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (objectA) {
System.out.println(Thread.currentThread().getName() + "\t 成功获得 A 锁");
}
}
}, "B").start();
}
}

结果:

1
2
A	 自己持有 A 锁,希望获得 B 锁
B 自己持有 B 锁,希望获得 A 锁

如何排查死锁?

image-20240711213613095

通过图形化界面查找死锁:

image-20240711213746546

二、中级

什么是中断机制?

  • 一个线程不该由其他线程来强制中断或停止,而是由自己自行停止
  • 在 Java 中没发立即停止一个线程,但停止线程十分重要,因此 Java 提供了一种用于停止线程的协商机制——中断
  • 中断是一种协商机制,中断的过程需要自己实现
  • 想要中断一个线程,需要手动调用该线程的 interrupt 方法,这也仅仅将线程对象的中断标识改为 true。

中断的相关 API 方法之三大方法说明

image-20240711220230729

1
2
3
interrupt() // 设置线程的中断状态为 true,发起中断协商而非立即中断 (实例方法)
interrupted() // 1.返回当前线程中断状态 2.将中断状态改为 false (两次调用该方法,第二次返回 false) (静态方法)
isInterrupted() // 判断当前线程是否为中断状态 (实例方法)

大厂面试题中断机制考点

如何停止中断运行中的线程?

  1. 通过 volatile 变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class InterruptDemo {

    static volatile boolean isStop = false;

    public static void main(String[] args) {
    new Thread(() -> {
    while (true) {
    if (isStop) {
    System.out.println(Thread.currentThread().getName() + "\t isStop 被修改为 true,程序停止");
    break;
    }
    System.out.println("------hello volatile");
    }
    }, "t1").start();

    try {
    TimeUnit.MILLISECONDS.sleep(20);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }

    new Thread(() -> {
    isStop = true;

    }, "t2").start();
    }
    }
  2. 通过 AtomicBoolean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class InterruptDemo {

    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);

    public static void main(String[] args) {
    new Thread(() -> {
    while (true) {
    if (atomicBoolean.get()) {
    System.out.println(Thread.currentThread().getName() + "\t atomicBoolean 被修改为 true,程序停止");
    break;
    }
    System.out.println("------hello atomicBoolean");
    }
    }, "t1").start();

    try {
    TimeUnit.MILLISECONDS.sleep(20);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }

    new Thread(() -> {
    atomicBoolean.set(true);

    }, "t2").start();
    }
    }
  3. 通过 Thread 类自带的 isInterrupted 方法

总结:

image-20240712193751216

当前线程的中断标识为 true,是不是线程就立刻停止了?

不是

谈谈你对 Thread.interrupted() 的理解

返回当前线程的中断状态,将中断状态改为 false

image-20240712194824019

LockSupport 是什么

LockSupport 中的 park 和 unpark 作用分别是阻塞线程和解除阻塞线程

线程等待和唤醒的方法

  1. Object 的 wait 和 notify 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class LockSupportDemo {
    public static void main(String[] args) {
    Object objectLock = new Object();
    new Thread(() -> {
    synchronized (objectLock) {
    System.out.println(Thread.currentThread().getName() + "\t ----come in");
    try {
    objectLock.wait();
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    System.out.println(Thread.currentThread().getName() + "\t ----被唤醒");
    }
    }, "t1").start();

    new Thread(() -> {
    synchronized (objectLock) {
    objectLock.notify();
    System.out.println(Thread.currentThread().getName() + "\t ----发出通知");
    }
    }, "t2").start();
    }
    }

    结果:

    1
    2
    3
    t1	 ----come in
    t2 ----发出通知
    t1 ----被唤醒

    注意:notify 要放在 wait 方法之后,这两个方法必须要在锁块中才能正常使用

  2. Condition 的 await 和 signal 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class LockSupportDemo {
    public static void main(String[] args) {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    new Thread(() -> {
    lock.lock();
    try {
    System.out.println(Thread.currentThread().getName() + "\t ----come in");
    condition.await();
    System.out.println(Thread.currentThread().getName() + "\t ----被唤醒");
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    lock.unlock();
    }
    }, "t1").start();

    try {
    TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }

    new Thread(() -> {
    lock.lock();
    try {
    condition.signal();
    } finally {
    lock.unlock();
    }
    }, "t2").start();
    }
    }

    注意:await 要放在 signal 方法之后,这两个方法必须要在锁块中才能正常使用