笔记
并发编程.pdf
并发编程_模式.pdf
多线程应用笔记
并发编程_应用.pdf
Monitor,synchronized等原理
并发编程_原理.pdf
https://gitee.com/gu_chun_bo/java-construct/blob/master/java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/java%E5%B9%B6%E5%8F%911.md

线程基础

创建线程

1. 继承Thread类并重写run方法

  1. public static class Thread1 extends Thread {
  2. @Override
  3. public void run() {
  4. System.out.println("继承 Thread 类并重写 run 的方法");
  5. }
  6. }
  7. public static void main(String[] args) {
  8. Thread1 thread1 = new Thread1();
  9. thread1.start();
  10. }

2. 实现Runnable的run方法

把【线程】和【任务】(要执行的代码)分开
Thread 代表线程
Runnable 可运行的任务(线程要执行的代码)

  1. public static class Task implements Runnable {
  2. public void run() {
  3. System.out.println("实现 Runnable 接口的 run 方法");
  4. }
  5. }
  6. public static void main(String[] args) {
  7. // 创建任务
  8. Task task = new Task();
  9. // 创建线程
  10. Thread thread = new Thread(task);
  11. thread.start();
  12. }

方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
用 Runnable 更容易与线程池等高级 API 配合
用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

image.png
某个接口只有 一个抽象方法,可以用lambda表达式简化

3. 使用FutreTask方法

  1. public static class MyCall implements Callable {
  2. public String call() throws Exception {
  3. return "实现 Callable 接口的 call 方法";
  4. }
  5. }
  6. FutureTask<String> futureTask = new FutureTask<String>(new MyCall());
  7. Thread thread4 = new Thread(futureTask);
  8. thread4.start();
  9. // 阻塞线程,等待返回结果
  10. String result = futureTask.get();
  11. System.out.println("futureTask" + thread4.getId());

线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念
就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能

常用方法

方法名 static 功能说明 注意
start() 启动一个新线
程,在新的线程
运行 run 方法
中的代码
start 方法只是让线程进入就绪,里面代码不一定立刻
运行(CPU 的时间片还没分给它)。每个线程对象的
start方法只能调用一次,如果调用了多次会出现
IllegalThreadStateException
run() 新线程启动后会
调用的方法
如果在构造 Thread 对象时传递了 Runnable 参数,则
线程启动后会调用 Runnable 中的 run 方法,否则默
认不执行任何操作。但可以创建 Thread 的子类对象,
来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程运行结
束,最多等待 n
毫秒
getId() 获取线程长整型的 id id 唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级 java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示,分别为:
NEW, RUNNABLE, BLOCKED, WAITING,
TIMED_WAITING, TERMINATED
isInterrupted() 判断是否被打
断,
不会清除 打断标记
isAlive() 线程是否存活(还没有运行完毕)
interrupt() 打断线程 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记
interrupted() static 判断当前线程是否被打断 会清除 打断标记
currentThread() static 获取当前正在执行的线程
yield() static 提示线程调度器让出当前线程对CPU的使用 主要是为了测试和调试
sleep(long n) static 让当前执行的线程休眠n毫秒,休眠时让出 cpu
的时间片给其它线程

start & run

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

    sleep & yield

    sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 方法代替 Thread 的 sleep 来获得更好的可读性

应用
限制对cpu的使用:在做服务端使用while(true)的代码,会占用大量cpu
详见:并发编程_应用.pdf

yield
  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

    yield就是把当前cpu分的时间片让出去,当前线程从runing变成runnable,如果有其他线程就分给其他线程,如果没有其他线程,cpu还会给runnable分时间片;但是不会给timed waiting分,直到睡眠结束才会分

  2. 具体的实现依赖于操作系统的任务调度器

    join

    等待线程运行结束
    join(long n) : 等待线程运行结束,最多等待 n毫秒

    interrupt 方法详解

    打断 sleep,wait,join 的线程

    这几个方法都会让线程进入阻塞状态
    线程处于阻塞,使用.isInterrupted()返回false
    打断 sleep 的线程, 会清空打断状态,以 sleep 为例

    1. private static void test1() throws InterruptedException {
    2. Thread t1 = new Thread(()->{
    3. sleep(1);
    4. }, "t1");
    5. t1.start();
    6. sleep(0.5);
    7. t1.interrupt();
    8. log.debug(" 打断状态: {}", t1.isInterrupted());
    9. }

    输出

    1. java.lang.InterruptedException: sleep interrupted
    2. at java.lang.Thread.sleep(Native Method)
    3. at java.lang.Thread.sleep(Thread.java:340)
    4. at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    5. at cn.itcast.n2.util.Sleeper.sleep(Sleeper.java:8)
    6. at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59)
    7. at java.lang.Thread.run(Thread.java:745)
    8. 21:18:10.374 [main] c.TestInterrupt - 打断状态: false

    打断正常运行的线程

    打断正常运行的线程, 不会清空打断状态
    线程正常运行使用.isInterrupted()方法返回值是true

    1. private static void test2() throws InterruptedException {
    2. Thread t2 = new Thread(()->{
    3. while(true) {
    4. Thread current = Thread.currentThread();
    5. boolean interrupted = current.isInterrupted();
    6. if(interrupted) {
    7. log.debug(" 打断状态: {}", interrupted);
    8. break;
    9. }
    10. }
    11. }, "t2");
    12. t2.start();
    13. sleep(0.5);
    14. t2.interrupt();
    15. }

    输出

    1. 20:57:37.964 [t2] c.TestInterrupt - 打断状态: true


    守护线程

    守护线程是为其他线程服务的
    垃圾回收线程就是守护线程~
    守护线程有⼀个特点:
    当别的⽤户线程执⾏完了,虚拟机就会退出,守护线程也就会被停⽌掉了。
    也就是说:守护线程作为⼀个服务线程,没有服务对象就没有必要继续运⾏了
    使⽤线程的时候要注意的地⽅

  3. 在线程启动前设置为守护线程,⽅法是 setDaemon(boolean on)

  4. 使⽤守护线程不要访问共享资源(数据库、⽂件等),因为它可能会在任何时候就挂掉了。
  5. 守护线程中产⽣的新线程也是守护线程

    线程状态

    五种线程状态

    image.png
  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

    六种线程状态

    根据 Thread.State 枚举,分为六种状态
    image.png
    new : 刚创建线程没运行
    runnable : 正在运行,就绪状态,io阻塞
    terminated : 运行结束
    waiting : 没有时间的等待,比如t1线程一直运行,t2线程run里面t1.join
    timed_waiting : 有时间限制的等待,比如t1线程sleep(10000),t2线程run里面t1.join,在这10s里就是timed_waiting
    blocked : 等待锁状态,比如t1上锁并一致运行,t2对同一对象也上锁就是blocked

    共享模型之管程

    线程安全问题

    时序图
    image.png

    临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的

  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

    竞态条件 Race Condition

    多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

    成员变量和静态变量是否线程安全?

    如果它们没有共享,则线程安全
    如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    如果只有读操作,则线程安全
    如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

局部变量是线程安全的
但局部变量引用的对象则未必(比如父类的方法被子类重写并开启线程就不安全了)
如果该对象没有逃离方法的作用访问,它是线程安全的
如果该对象逃离方法的作用范围,需要考虑线程安全

常见线程安全类

String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent
包下的类

它们的每个方法是原子的 但注意它们多个方法的组合不是原子的,见后面分析

synchronized解决方案

应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一
时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁
的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

图文理解synchronized

image.png
image.png

synchronized语法

  1. synchronized(对象) // 线程1, 线程2(blocked)
  2. {
  3. 临界区
  4. }
  1. static int counter = 0;
  2. static final Object room = new Object();
  3. public static void main (String[]args) throws InterruptedException {
  4. Thread t1 = new Thread(() -> {
  5. for (int i = 0; i < 5000; i++) {
  6. synchronized (room) {
  7. counter++;
  8. }
  9. }
  10. }, "t1");
  11. Thread t2 = new Thread(() -> {
  12. for (int i = 0; i < 5000; i++) {
  13. synchronized (room) {
  14. counter--;
  15. }
  16. }
  17. }, "t2");
  18. t1.start();
  19. t2.start();
  20. t1.join();
  21. t2.join();
  22. log.debug("{}", counter);
  23. }

时序图

image.png
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

image.png

案例

卖票

image.png
image.png
image.png

  • ticketwindow被多个线程使用,成员变量count属于是共享变量,并且sell( )方法存在读写操作,多线程的访问就会存在并发问题
  • 解决方案 : sell( )方法用synchronized修饰,对ticketWindow对象上锁
  • sellCount如果使用ArrayList也会存在并发问题,因为多线程的arrayList.add( )同样会读写共享变量,所以这里使用Vector

    转账问题

    image.png
    image.png

synchronized底层原理

Monitor,synchronized原理

见原理pfd并发编程_原理.pdf

synchronized优化原理-轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以
使用轻量级锁来优化。
例:
image.png

  1. 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

image.png

  1. 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
    image.png
  2. 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
    image.png
  3. 如果 cas 失败,有两种情况
    1. 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    2. 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

image.png

  1. 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

image.png

  1. 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象
    1. 成功,则解锁成功
    2. 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

      synchronized优化原理-锁膨胀

      如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有
      竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
      image.png
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

image.png

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList (BLOCKED)

image.png

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁
    流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

    synchronized优化原理-自选优化

    重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步
    块,释放了锁),这时当前线程就可以避免阻塞。
    自旋重试成功的情况:
    image.png
    自旋重试失败的情况:
    image.png

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

    synchronized优化原理-偏向锁

    https://www.bilibili.com/video/BV16J411h7Rd?p=83&spm_id_from=pageDriver
    轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
    Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现
    这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

    偏向状态

    image.png
    一个对象创建时:
    如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的
    thread、epoch、age 都为 0
    偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
    如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
    age 都为 0,第一次用到 hashcode 时才会赋值

    撤销

  • 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被
    撤销

  • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  • 调用 wait/notify

    批量重偏向

    如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象
    的 Thread ID
    当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至
    加锁线程

    批量撤销

    当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象
    都会变为不可偏向的,新建的对象也是不可偏向的

wait notify原理

image.png
image.png

sleep(long n) 和 wait(long n) 的区别

1) sleep 是 Thread 方法,而 wait 是 Object 的方法
2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
4) 共同点 : 它们状态都会进入TIMED_WAITING

wait/notify使用

image.png


同步模式-保护性暂停

用在一个线程等待另一个线程的执行结果
image.png

实现

见pdf并发编程_模式.pdf

join原理

通过保护性暂停模式实现

源码

image.png

死锁

image.png

定位死锁

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:

  1. cmd > jps
  2. Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
  3. 12320 Jps
  4. 22816 KotlinCompileDaemon
  5. 33200 TestDeadLock // JVM 进程
  6. 11508 Main
  7. 28468 Launcher
  1. cmd > jstack 33200
  2. Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
  3. 2018-12-29 05:51:40
  4. Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode):
  5. "DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition
  6. [0x0000000000000000]
  7. java.lang.Thread.State: RUNNABLE
  8. "Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry
  9. [0x000000001f54f000]
  10. java.lang.Thread.State: BLOCKED (on object monitor)
  11. at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
  12. - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
  13. - locked <0x000000076b5bf1d0> (a java.lang.Object)
  14. at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
  15. at java.lang.Thread.run(Thread.java:745)
  16. "Thread-0" #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry
  17. [0x000000001f44f000]
  18. java.lang.Thread.State: BLOCKED (on object monitor)
  19. at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
  20. - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
  21. - locked <0x000000076b5bf1c0> (a java.lang.Object)
  22. at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
  23. at java.lang.Thread.run(Thread.java:745)
  24. // 略去部分输出
  25. Found one Java-level deadlock:
  26. =============================
  27. "Thread-1":
  28. waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),
  29. which is held by "Thread-0"
  30. "Thread-0":
  31. waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),
  32. which is held by "Thread-1"
  33. Java stack information for the threads listed above:
  34. ===================================================
  35. "Thread-1":
  36. at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
  37. - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
  38. - locked <0x000000076b5bf1d0> (a java.lang.Object)
  39. at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
  40. at java.lang.Thread.run(Thread.java:745)
  41. "Thread-0":
  42. at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
  43. - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
  44. - locked <0x000000076b5bf1c0> (a java.lang.Object)
  45. at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
  46. at java.lang.Thread.run(Thread.java:745)
  47. Found 1 deadlock.

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束

ReentranLock

相对于 synchronized 它具备如下特点

可中断
可以设置超时时间
可以设置为公平锁
支持多个条件变量
与 synchronized 一样,都支持可重入

语法

  1. // 获取锁
  2. reentrantLock.lock();
  3. try {
  4. // 临界区
  5. } finally {
  6. // 释放锁
  7. reentrantLock.unlock();
  8. }

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

可中断

锁超时

lock.tryLock() : 如果获取不到锁,不会一直等待,避免造成死锁,参数可以设置等待时间,返回值是Boolean,获得锁是true

公平锁

  • synchronize 也是不公平的,拿到对象锁之后,其他线程会处于阻塞状态,释放锁之后,处于阻塞的线程会争夺锁,而不会按照进入阻塞队列的顺序获得锁
  • ReentranLock 默认是不公平的
    1. // 可以使用构造来设置为公平锁
    2. ReentranLock lock = new ReentranLock(true);
    公平锁用于解决饥饿问题,但不推荐使用,可以使用tryLock()这种方式,因为使用公平锁会降低并发度

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:
await 前需要获得锁
await 执行后,会释放锁,进入 conditionObject 等待
await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
竞争 lock 锁成功后,从 await 后继续执行

java内存模型

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

    原子性

    Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性

    可见性

    image.png
    为什么呢?分析一下:
    1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
    2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
    3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
    image.png
    解决方法
    volatile(易变关键字)
    它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

    有序性

    JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码
    这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。
    volatile 修饰的变量,可以禁用指令重排

    volatile原理

    如何保证可见性

    image.png
    image.png

    如何保证有序性

    image.png
    image.png

    线程池

    image.png
    可以把线程池看成一个银行,办理业务的窗口是线程,顾客进来直接去办理业务,1-3窗口都有人,就在等待区等待,

    构造方法

    1. public ThreadPoolExecutor(int corePoolSize,
    2. int maximumPoolSize,
    3. long keepAliveTime,
    4. TimeUnit unit,
    5. BlockingQueue<Runnable> workQueue,
    6. ThreadFactory threadFactory,
    7. RejectedExecutionHandler handler)
    corePoolSize 核心线程数目 (最多保留的线程数)
    maximumPoolSize 最大线程数目
    keepAliveTime 生存时间 - 针对救急线程
    unit 时间单位 - 针对救急线程
    workQueue 阻塞队列
    threadFactory 线程工厂 - 可以为线程创建时起个好名字
    handler 拒绝策略
    newFixedThreadPool
    特点
    核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
    阻塞队列是无界的,可以放任意数量的任务
    评价 适用于任务量已知,相对耗时的任务
    newCachedThreadPool
    特点
    核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
    全部都是救急线程(60s 后可以回收)
    救急线程可以无限创建
    队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交
    货)
    评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线
    程。 适合任务数比较密集,但每个任务执行时间较短的情况
    newSingleThreadExecutor
    使用场景:
    希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
    区别:
    自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一
    个线程,保证池的正常工作
    Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
    FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因
    此不能调用 ThreadPoolExecutor 中特有的方法
    Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
    对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改