3.1多线程(马士兵教育-左程云)
01.Java中线程的实现方式?
- 继承 Thread 类,重写其中的 run() 方法;
public class MyThread extends Thread {
public void run() {
System.out.println("线程开始执行");
// TODO: 执行需要执行的代码
System.out.println("线程执行结束");
}
}
# 调用
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
2.实现 Runnable 接口,重写run()方法。代码如下:
public class MyRunnable implements Runnable {
public void run() {
System.out.println("线程开始执行");
// TODO: 执行需要执行的代码
System.out.println("线程执行结束");
}
}
# 调用
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
- 实现Callable接口,重写call()方法
public class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
System.out.println("线程开始执行");
// TODO: 执行需要执行的代码
System.out.println("线程执行结束");
return 0;
}
}
# 调用
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(callable);
Thread thread = new Thread(task);
thread.start();
Integer result = task.get();
}
}
4.基于线程池实现。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建线程池,其中参数为线程池大小
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 创建Runnable任务
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getName() + " is running.");
}
};
// 提交任务到线程池中执行
for (int i = 0; i < 5; i++) {
executorService.submit(task);
}
// 关闭线程池
executorService.shutdown();
}
}
# 结果
Thread pool-1-thread-1 is running.
Thread pool-1-thread-3 is running.
Thread pool-1-thread-2 is running.
Thread pool-1-thread-3 is running.
Thread pool-1-thread-1 is running.
总结:追其底层实现逻辑,它只有一种实现方式。因为Thread类实现的Runnable接口,Callable接口继承了Runnable接口,线程池中的Work工作线程也是实现了Runnable接口。
02.Java中线程的状态?
Java中线程的状态分为6种:
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
-
终止(TERMINATED):表示该线程已经执行完毕。
image.png
03.Java中如何停止线程?
1.使用标记位中止线程;
2.使用 stop() 方法强行终止线程;(暴力停止)
3.使用interrupt() 方法中断线程;
04.Java中sleep和wait方法的区别?
1.所属类不同
1.1 wait() 是Object中的实例方法
1.2 sleep()是Thread的静态方法。
2.唤醒机制不同。
2.1 wait() 没有设置最大时间情况下,必须等待对象调用notify() 或notifyAll()方法。
2.2 sleep是到指定时间自动唤醒。
3.锁机制不同:
3.1 wait()释放锁,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进;
3.2 sleep()只是让线程休眠,让出cpu资源,不会释放锁,当休眠时间结束后,线程会恢复到就绪状态,但是不会立刻执行,可能会有其他优先级高的线程抢占资源。
4.使用位置不同。
4.1 wait()必须持有对象锁,写在同步方法或者synchronized()语句块中。
4.2 sleep()可以使用在任意地方。
5.异常处理
5.1 sleep()必须捕获异常
5.2 wait(),notify()和notifyAll()不需要捕获异常。
- 锁的处理
6.1 调用wait()方法的线程会释放持有的对象锁,从而让其他在此对象上等待的线程有机会获得该对象锁;
6.2 sleep()方法在暂停线程时,并不会释放任何锁资源。
05.并发编程的三大特性?
1)可见性
可见性是指当一个线程修改了共享变量后,其他线程能够立即看见这个修改。
2)原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败。
3)有序性
有序性是指程序指令按照预期的顺序执行而非乱序执行,乱序又分为编译器乱序和CPU执行乱序
06 什么是CAS? 有什么优缺点?
6.1什么是CAS?
CAS是compare and swap的缩写,即我们所说的比较交换。CAS是一种基于锁的操作,而且是乐观锁。
6.2 CAS优点?
高效性:CAS能够在无锁的情况下进行并发操作,避免了线程之间的互斥和阻塞。
无死锁:CAS不会引起死锁,因为它不需要加锁。
确保操作的原子性:CAS可以确保并发环境下对于同一内存位置的操作具有原子性,避免数据不一致的问题。
6.3 CAS缺点?
- ABA问题:当一个值从A变成B,再变成A时,如果CAS检查的时候只检查了值是否等于A,那么CAS将认为这个值没有发生变化,可能会引发一些问题。
- 只能保证一个共享变量的原子操作:如果需要对多个变量进行原子操作,CAS就无法满足需求。
- 自旋开销大:在高并发场景下,如果CAS一致失败,就会一直自旋,占用CPU资源,导致性能下降。
- 对CPU的缓存使用可能存在问题:由于CAS需要访问内存,所以在高并发环境下可能会引发CPU缓存一致性的问题。
@Contended 注解有什么用?
Java8引入了@Contented这个新的注解来减少伪共享(False Sharing)的发生。
@sun.misc.Contended注解是被设计用来解决伪共享问题的;
Java8中@Contended和伪共享
@Contended注解有什么用?
Java中的四种引用类型?
Java中的四种引用类型分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。
-
强引用(Strong Reference):是使用最普遍的引用类型,它直接指向对象,并且只要存在强引用,垃圾收集器就不会回收该对象。例如:Object obj = new Object()。
-
软引用(Soft Reference):是一种比较灵活的引用类型,当堆内存不足时,垃圾收集器会优先回收软引用指向的对象。一般用于内存敏感的程序中,如缓存处理等。例如:SoftReference softRef = new SoftReference(obj)。
-
弱引用(Weak Reference):是一种比软引用更弱的引用类型,它的生命周期只能存活到下一次垃圾收集之前,即只要被垃圾收集器扫描到,就会被回收。例如:WeakReference weakRef = new WeakReference(obj)。
-
虚引用(Phantom Reference):是一种最弱的引用类型,无法通过虚引用访问对象本身,仅用于跟踪对象被垃圾回收的状态。例如:
ReferenceQueue queue = new ReferenceQueue();
PhantomReference phantomRef = new PhantomReference(obj,queue)。
ThreadLocal的内存泄漏问题?
为什么会内存泄漏?
ThreadLocal就相当于一个访问工具类,通过操作ThreadLocal对象的方法 来操作存储在当前线程内部的ThreadLocalMap里的值;ThreadLocalMap是一个哈希数组,key为ThreadLocal对象,Value为一个Object类型的值;调用ThreadLocal.get() 方法的时候,会将当前ThreadLocal对象传过去,所以可以获取到指定ThreadLocal设置到当前线程的值;如果ThreadLocal回收了,则此时就没有方式能够访问到val了,所以val就是不会再被程序用到,但是由于Thread还存在就无法回收,那么此时便存在了内存泄漏!
解决办法:
1.ThreadLocal被回收后,及时用remove()方法清理ThreadLocal变量值(ThreadLocalMap中的key是Entry,Entry是弱引用,调用remove()方法会出发GC,回收已经无引用的value);
2.避免使用静态ThreadLocal变量;
Java中锁的分类?
Java中锁分为以下几种:
- 乐观锁、悲观锁
- 独享锁、共享锁
- 公平锁、非公平锁
- 互斥锁、读写锁
- 可重入锁
- 分段锁
- 锁升级(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁) JDK1.6
- 单体锁、分布式锁
Java锁的种类
Synchronized在JDK1.6中的优化?
jdk1.6对synchronized做了优化,分别如下三点:
1 锁消除:
如果synchronized的内容不可能引起同步问题,则编译时忽略synchronized,变成没有锁的代码
2 锁膨胀:
假如在for循环内设置synchronized代码块,每次循环都会导致加锁和解锁,这会极大的浪费性能,因此jdk1.6以及后面的版本,会将synchronized代码块的范围膨胀到for循环外
public void test(){
for (int i=0;i<10;i++){
synchronized (this){
count++;
}
}
}
3 锁升级
先是偏向锁,竞争激烈时转成轻量级锁,如果多次自旋失败,则升级成重量级锁;
Synchronized在JDK1.6中的优化
Synchronized的实现原理?
synchronized是一种对象锁,是可重入的,但是不可中断的(这个不可中断指的是在阻塞队列中排队是不可中断),非公平的锁。
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现。
加了synchronized后,在字节码会有二条指令,如代码第4和13标识位
synchronized (Test3.class){
System.out.println(1);
}
4: monitorenter
5: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
8: iconst_1
9: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
12: aload_1
13: monitorexit
synchronized原理详解
深入理解synchronized底层原理,一篇文章就够了!
什么是AQS?
AQS,即AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架。它维护了一个volatile int state(代表共享资源)和一个FIFO(双向队列)线程等待队列(多线程争用资源被阻塞时会进入此队列);
AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。从图中可以看出,在java的同步组件中,AQS的子类(Sync等)一般是同步组件的静态内部类,即通过组合的方式使用。
抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch
image.pngAQS唤醒节点时,为何从后往前找?
1.node 节点在插入整个 AQS 队列当中时,先把当前节点的上一个指针指向前面的节点,再把 tail 指向自己;这个时候会有一个 CPU 调度问题,如果这个时候我卡在这个位置,那么从前往后找就会造成节点丢失。
2.cancelAcquire 方法也是先去调整上一个指针的指向,next 指针后续才动;
3.所以无论是我们节点插入的过程还是某一个节点取消,更改指针的过程都是先动上一个指针在动 next 的,所以 prev 这个节点指向相对来说优先级更高,或者时效性更好,这样我们就知道它为什么非要从后往前找了,因为从前往后极大的可能错过某一个节点,从而造成某一个 node 在那边被挂起了,但是你之前的线程已经释放锁资源了,并没有唤醒我造成锁饥饿的问题。
AQS 唤醒节点的时候,为什么是从后往前找?
ReentrantLock和synchronized的区别?
synchronized 和 ReentrantLock 都是 Java 中提供的可重入锁,二者的主要区别有以下 5 个:
1.用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
2.获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
3.锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。
4.响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
5.底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
synchronized和ReentrantLock有什么区别?
ReentrantReadWriteLock的实现原理?
简单来说,就是读锁可以共享,但是写锁必须独占;
读写锁用的是同一个Sync同步器,所以有相同的阻塞队列和state
JDK中提供了哪些线程池?
1.newFixedThreadPool:定长线程池;
可控制线程最大并发数,超出的线程会在队列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
2.newSingThreadPool:单例线程池
它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
3.newCachedThreadPool:缓存线程池;
如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
4.newScheduleThreadpool:定时线程池
支持定时及周期性任务执行
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
5.newWorkStealingPool(工作窃取线程池):JDK 8引入,内部构建一个ForkJoinPool,创建持有足够线程来支持给定的并行度的线程池。该线程池使用多个队列,每个线程维护一个自己的队列。当一个线程完成自己队列中的任务后,会从其他线程的队列中窃取任务执行,因此构造方法中把CPU数量设置为默认的并行度。
线程池的核心参数有什么?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
1.核心线程数(Core Pool Size):线程池中最小的线程数,即在线程池中一直保持的线程数量,不受空闲时间的影响;
2.最大线程数(最大池大小);
3.空闲线程存活时间(Keep Alive Time):当线程池中的线程数超过核心线程数时,多余的线程会被回收,此参数即为非核心线程的空闲时间,超过此时间将被回收;
4.工作队列(Work Queue):用于存储等待执行的任务的队列,当线程池中的线程数达到核心线程数时,新的任务将被加入工作队列等待执行;
5.拒绝策略(Reject Execution Handler):当线程池和工作队列都已经达到最大容量,无法再接收新的任务时,拒绝策略将被触发。常见的拒绝策略有抛出异常、直接丢弃任务、丢弃队列中最老的任务等。
6.线程工厂 (Thread Factory):用于创建新的线程,可定制线程名字、线程组、优先级等。
7.时间单位(TimeUnit):。
线程池的拒绝策略?
- AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
- CallerRunsPolicy:由调用线程处理该任务。
- DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
- DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
线程池的状态?
- RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务。
- SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown() 方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用 shutdown() 方法进入该状态)。
- STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。
- TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入 TERMINATED 状态。
-
TERMINATED:在 terminated() 方法执行完后进入该状态,默认 terminated() 方法中什么也没有做。
image.png
线程池的执行流程?
- 提交一个新线程任务,线程池会在线程池中分配一个空闲线程,用于执行线程任务;
- 如果线程池中不存在空闲线程,则线程池会判断当前“存活的线程数”是否小于核心线程数corePoolSize。
- 如果小于核心线程数corePoolSize,线程池会创建一个新线程(核心线程)去处理新线程任务;
- 如果大于核心线程数corePoolSize,线程池会检查工作队列;
- 如果工作队列未满,则将该线程任务放入工作队列进行等待。线程池中如果出现空闲线程,将从工作队列中按照FIFO的规则取出1个线程任务并分配执行;
- 如果工作队列已满,则判断线程数是否达到最大线程数maximumPoolSize;
- 如果当前“存活线程数”没有达到最大线程数maximumPoolSize,则创建一个新线程(非核心线程)执行新线程任务;
线程池添加工作线程的流程?
线程池为何要构建空任务的非核心线程?
线程池使用完毕为何必须shutdown()?
1.释放资源:线程池内部会创建一定数量的线程以及其他相关资源,如线程队列、线程池管理器等。如果不及时关闭线程池,这些资源将一直占用系统资源,可能导致内存泄漏或资源浪费。
2.防止任务丢失:线程池中可能还有未执行的任务,如果不关闭线程池,这些任务将无法得到执行。关闭线程池时,会等待所有已提交的任务执行完毕,确保任务不会丢失。
3.优雅终止:关闭线程池可以让线程池中的线程正常执行完当前任务后停止,避免突然终止线程导致的资源释放不完整或状态不一致的问题。
4.避免程序阻塞:在某些情况下,如果不关闭线程池,程序可能会一直等待线程池中的任务执行完毕,从而导致程序阻塞,无法继续执行后续的逻辑。
线程池的核心参数到底如何设置?
配置线程数量之前,首先要看任务的类型是 IO密集型,还是CPU密集型?
IO密集型:频繁读取磁盘上的数据,或者需要通过网络远程调用接口。
CPU密集型:非常复杂的调用,循环次数很多,或者递归调用层次很深等。
-
IO密集型配置线程数经验值是:2N,其中N代表CPU核数。
-
CPU密集型配置线程数经验值是:N + 1,其中N代表CPU核数。
ConcurrentHashMap在1.8做了什么优化?
JDK1.8放弃了锁分段的做法,采用CAS和synchronized方式处理并发。以put操作为例,CAS方式确定key的数组下标,synchronized保证链表节点的同步效果。
jdk1.8ConcurrentHashMap是数组+链表,或者数组+红黑树结构,并发控制使用Synchronized关键字和CAS操作。