文章目录
- 线程
-
- 什么是线程和进程?
- 请简要描述线程与进程的关系,区别及优缺点?
- 程序计数器、虚拟机栈、虚拟机栈、堆和方法区
- 如何创建线程?
- 线程的生命周期
- 什么是线程上下文切换?
- Thread#sleep() 方法和 Object#wait() 方法对比
- 为什么 wait() 方法不定义在 Thread 中?
- 为什么 sleep() 方法定义在 Thread 中?
- 可以直接调用 Thread 类的 run 方法吗?
- java线程通信的方式有哪些?
- java中wait、 notify、notifyall作用?
- 多线程
-
- 并发与并行的区别
- 同步和异步的区别
- 单核 CPU 支持 Java 多线程吗?
- 使用多线程可能带来什么问题?
- 如何理解线程安全和不安全?
- 线程池
-
- 什么是线程池?
- 为什么要用线程池?(线程池的好处)
- 如何创建线程池?
- java并发库(ThreadPoolExecutor)提供了哪些线程池实现?有什么区别?
- 线程池常见参数有哪些?如何解释?
- 线程池的核心线程会被回收吗?
- 线程池的拒绝策略有哪些?
- 如果不允许丢弃任务任务,应该选择哪个拒绝策略?
- CallerRunsPolicy 拒绝策略有什么风险?如何解决?
- 线程池常用的阻塞队列有哪些?
- 线程池处理任务的流程了解吗(线程池的工作原理)?
- 线程池中线程异常后,销毁还是复用?
- 如何给线程池命名?
- 如何设定线程池的大小?
- 死锁
-
- 什么是线程死锁?
- 产生死锁的四个必要条件
- 如何检测死锁?
- 如何预防和避免线程死锁?
- 虚拟线程
-
- 什么是虚拟线程(协程)?
- 虚拟线程有什么优点和缺点?
- JMM(JAVA内存模型)
- volatile 关键字
-
- 如何保证变量的可见性?
- 如何禁止指令重排序?
- volatile 可以保证原子性么?
- 乐观锁和悲观锁
-
- 什么是悲观锁?
- 什么是乐观锁?
- 如何实现乐观锁?
- CAS原理
- Java 中 CAS 是如何实现的?
- CAS 算法存在哪些问题?
- ThreadLocal
-
- ThreadLocal 有什么用?为什么要ThreadLocal?
- ThreadLocal 底层原理?
- 为什么在使用ThreadLocal要使用弱引用来防止内存泄漏?
- ThreadLocal 内存泄露问题是怎么导致的,以及该怎么解决?
- java父子线程之间怎么传递数据(了解InheritableThreadLocal吗?)
- ThreadLocal缺点
- synchronized 关键字
-
- synchronized 是什么?有什么用?
- 如何使用 synchronized?
- 构造方法可以用 synchronized 修饰么?
- synchronized 底层原理了解吗?
- JDK1.6 之后的 synchronized 底层做了哪些优化?锁升级原理了解吗?
- synchronized 和 volatile 有什么区别?
- ReentrantLock
-
- ReentrantLock 是什么?
- synchronized 和 ReentrantLock 有什么区别?
- 可中断锁和不可中断锁有什么区别?
- ReentrantReadWriteLock
-
- ReentrantReadWriteLock 是什么?
- ReentrantReadWriteLock 适合什么场景?
- 线程持有读锁还能获取写锁吗?
- 读锁为什么不能升级为写锁?
- StampedLock
-
- StampedLock 是什么?
- Atomic原子类
-
- 有哪些Atomic原子类?
- Future
-
- Future 类有什么用?
- Callable 和 Future 有什么关系?
- CompletableFuture 类有什么用?
- AQS相关
-
- AQS 是什么?
- AQS 的原理是什么?
- AQS的应用
- Semaphore有什么用以及原理?
- CountDownLatch 有什么用以及原理?
- CyclicBarrier 有什么用以及原理?
线程
什么是线程和进程?
进程:进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程:线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
请简要描述线程与进程的关系,区别及优缺点?
下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
程序计数器、虚拟机栈、虚拟机栈、堆和方法区
程序计数器:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
如何创建线程?
- 继承Thread类,重写run方法
- 实现Runnable接口,并重写run方法
- 实现Callable接口
- 可以通过Executors创建线程池,也可以自定义线程池
- 使用CompleteFuture类
不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。
严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()创建。不管是哪种方式,最终还是依赖于new Thread().start()。
线程的生命周期
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
- NEW: 初始状态,线程被创建出来但没有被调用 start() 。
- RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
- BLOCKED:阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
注意:
-
线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
-
在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
-
当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
-
TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。
-
当线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。
-
线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态
加粗样式
什么是线程上下文切换?
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
- 主动让出 CPU,比如调用了 sleep(), wait() 等。
- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
Thread#sleep() 方法和 Object#wait() 方法对比
共同点: 两者都可以暂停线程的执行。
区别:
- sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
- wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
- sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。
为什么 wait() 方法不定义在 Thread 中?
wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。
为什么 sleep() 方法定义在 Thread 中?
因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
可以直接调用 Thread 类的 run 方法吗?
调用 start() 方法方可启动线程并使线程进入就绪状态,而直接执行 run() 方法的话不会以多线程的方式执行,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它。
java线程通信的方式有哪些?
线程间通信方式,从实现本质来讲,主要可以分为两大类共享内存和消息传递。
- 共享变量: 线程之间可以通过共享变量来进行通信。不同的线程可以共享同一个变量,并在变量上进行读写操作。需要注意的是,共享变量可能会引发线程安全问题,需要通过同步机制来确保线程安全。
- 锁机制: 锁机制是一种常用的线程同步机制,可以保证在同一时间只有一个线程能够访问共享资源。Java提供了多种锁类型,如 synchronized 关键字、ReentrantLock 类等。
- 条件变量:条件变量是一种线程间通信机制,它用于在一个共享资源上等待某个条件的成立。Java 提供了 Condition 接口来支持条件变量的实现,在使用 Condition 时需要先获取锁,然后调用 await() 方法等待条件成立,当条件成立时可以通过 signal() 或 signalAll() 方法唤醒等待该条件的线程。
- 信号量:信号量是一种常见的线程同步机制,可用于控制多个线程对共享资源的访问。Java 提供了 Semaphore 类来实现信号量,Semaphore 类有两个常用的方法 acquire() 和 release(),分别用于获取和释放信号量。
- 管道:管道是一种用于线程间通信的高级机制,它可以实现一个线程向另一个线程传送数据。Java 提供了 PipedInputStream 和 PipedOutputStream 两个类来支持管道的实现,其中 PipedInputStream 用于读取数据,PipedOutputStream 用于写入数据。
java中wait、 notify、notifyall作用?
这三个都属于Objecy内中定义的方法,主要用于线程之间的通信和同步,且需要在synchronized修饰的方法或同步块中使用。
- wait()使得当前线程进入等待状态,且会释放锁
- notify()会随机唤醒一个调用wait后等待的线程
- notifyall()会唤醒所有调用wait等待的线程。
多线程
并发与并行的区别
- 并发:两个及两个以上的作业在同一 时间段 内执行。
- 并行:两个及两个以上的作业在同一 时刻 执行。
最关键的点是:是否是 同时 执行
同步和异步的区别
- 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
- 异步:调用在发出之后,不用等待返回结果,该调用直接返回。
单核 CPU 支持 Java 多线程吗?
单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。
使用多线程可能带来什么问题?
并发编程的目的就是为了能提高程序的执行效率进而提高程序的运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。
如何理解线程安全和不安全?
- 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
- 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。
线程池
什么是线程池?
线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。
为什么要用线程池?(线程池的好处)
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
如何创建线程池?
- 通过ThreadPoolExecutor构造函数来创建(推荐)。
- 通过 Executor 框架的工具类 Executors 来创建。
通过Executors工具类可以创建多种类型的线程池
- FixedThreadPool:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
- ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池。
java并发库(ThreadPoolExecutor)提供了哪些线程池实现?有什么区别?
- newFixedThreadPool:线程数固定, 核心线程和最大线程一致,keepAiveTime 是0, 队列是无界队列,所以可能出现OOM 。仅用于提交相对稳定且数量确定的任务场景。
- newWorkStealingPool: 线程数就是服务器可用处理核心数,并行数是核心数-1,每个线程都有自己的双端队列,当自己的任务处理完毕后,可以去别的线程队列尾部拿任务执行,加快任务执行速率。
- newSingleThreadExecutor: 就一个线程,配置的是无界队列,保证任务是按序执行的。
- newCachedTheadPool: 核心线程数为0&#