java八股复习指南-多线程篇

多线程

线程的实现

在 Java 中,实现多线程的主要有以下四种

  1. 继承 Thread 类,重写 run() 方法;
  2. 实现 Runnable 接口,实现 run() 方法,并将 Runnable 实现类的实例作为 Thread 构造函数的参数 target;
  3. 实现 Callable 接口,实现 call() 方法,然后通过 FutureTask 包装器来创建 Thread 线程;
  4. 通过 ThreadPoolExecutor 创建线程池,并从线程池中获取线程用于执行任务;

不使用Executors创建,而是用ThreadPoolExecutor创建

通过工厂模式,将创建产品实例的权利移交工厂,我们不再通过new来创建我们所需的对象,而是通过工厂来获取我们需要的产品。降低了产品使用者与使用者之间的耦合关系;创建线程池没有显式new,而是通过Executors这个静态方法newCaChedThreadPool来完成的;


Thread类与Runnable接口比较:

实现一个自定义的线程类,可以有继承Thread类或者实现Runnable接口的方式,由于java单继承、多实现的特性,Runnable接口使用比Thread更加灵活

Callable接口:

通常来说,我们使用RunnableThread来创建一个新的线程。但是它们有一个弊端,就是run方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值

JDK提供了Callable接口与Future类为我们解决这个问题,这也是所谓的“异步”模型。

线程的六种状态:

// Thread.State 源码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
  1. NEW:
  • 不能反复调用同一个线程的start()方法
  • 处于TERMINATED状态的线程不能再次调用start()方法
  1. RUNNABLE:

运行状态。处于RUNNABLE状态的线程在Java虚拟机中运行,也有可能在等待其他系统资源(比如I/O)。

  1. BLOCKED:

阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。

  1. WAITING

等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。

以下方法使进程进入等待状态:

  • Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;

  • Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;

  • LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

  1. TIMED_WAITING

超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。

  1. TERMINATED

终止状态。此时线程已执行完毕。

Java线程间的通信:

锁与同步

在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。线程和锁的关系,我们可以用婚姻关系来理解。一个锁同一时间只能被一个线程持有。也就是说,一个锁如果和一个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得等这个线程和这个锁“离婚”(释放)。

等待/通知机制

Java多线程的等待/通知机制是基于Object类的wait()方法和notify(), notifyAll()方法来实现的。

信号量

使用volitile关键字实现信号量。volitile关键字能够保证内存的可见性,如果用volitile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。

其他通信相关

join方法

join()方法是Thread类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程。

sleep方法

sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。

wait方法与sleep方法的区别:

sleep仅仅释放cpu资源,不会释放锁,所以易死锁。而wait方法会释放cpu资源,也会释放当前锁。

原理篇

volatitle

valatitle 保证内存可见性且禁止重排序

synchronized与锁

四种状态:无锁、偏向锁、轻量级锁、重量级锁

无锁->偏向锁->轻量级锁->重量级锁

  • 偏向锁:

顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

  • 轻量级锁:

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

  • 重量级锁:

在轻量级锁状态下,如果有第三个来访时,就会自动升级成重量级锁

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行速度较长。

cas与原子操作

乐观锁与悲观锁

悲观锁:

它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

乐观锁:

乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。乐观锁天生免疫死锁

乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

CAS

如果有一个多个线程共享的变量i原本等于5,我现在在线程A中,想把它设置为新的值6;

CAS的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:

  • V:要更新的变量(var),指代变量(i)
  • E:预期值(expected),指旧值(5)
  • N:新值(new),指要设置的值(6)

比较并交换的过程如下:

判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。

线程池原理

主要的任务处理流程:

如何做到线程复用

ThreadPoolExecutor在创建线程时,会将线程封装成工作线程worker,并放入工作线程组中,然后这个worker反复从阻塞队列中拿任务去执行。

首先去执行创建这个worker时就有的任务,当执行完这个任务后,worker的生命周期并没有结束,在while循环中,worker会不断地调用getTask方法从阻塞队列中获取任务然后调用task.run()执行任务,从而达到复用线程的目的。只要getTask方法不返回null,此线程就不会退出。

posted @ 2024-07-17 14:00  forest-pan  阅读(7)  评论(0编辑  收藏  举报