目录
[toc]
JMM
Java 中的原子性、有序性和可见性是 Java 内存模型(Java Memory Model,JMM)中的三个重要特性。
- 原子性:原子性是指一个操作是不可中断的,即这个操作要么全部执行成功,要么全部不执行。Java 中的原子性操作包括:基本类型的读写操作、引用类型的读写操作(包括 volatile 修饰的引用类型变量)、Atomic 类中的方法(如 AtomicInteger、AtomicLong 等)。
- 可见性:可见性是指当一个线程修改了共享变量的值后,其他线程可以立即看到修改后的值。Java 中保证可见性的方式包括:使用 synchronized、使用 volatile 修饰变量、使用 Lock 接口中的实现类等。
- 有序性:有序性是指在多线程环境下,程序的执行顺序是可以确定的。Java 中可以保证有序性的操作包括:使用 synchronized、使用 volatile 修饰变量、使用 Lock 接口中的实现类等。
为什么要使用线程池,有什么优点
java 多线程使用过哪些类或者工具
线程池需要设定哪些参数
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
下面对参数进行说明:
- corePoolSize:表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到 corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了 corePoolSize,则不再重新创建线程。如果调用了
prestartCoreThread()
或者prestartAllCoreThreads()
,线程池创建的时候所有的核心线程都会被创建并且启动。 - maximumPoolSize:表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过 maximumPoolSize 的话,就会创建新的线程来执行任务。
- keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了 corePoolSize,并且线程空闲时间超过了 keepAliveTime 的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。
- unit:时间单位。为 keepAliveTime 指定时间单位。
- workQueue:阻塞队列。用于保存任务的阻塞队列,关于阻塞队列可以看这篇文章。可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。
- threadFactory:创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。
- handler:饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种:
- AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
- CallerRunsPolicy:只用调用者所在的线程来执行任务;
- DiscardPolicy:不处理直接丢弃掉任务;
- DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务
Java 线程有哪些状态
New Runnable Blocked
- 当前线程 T 调用 Thread. sleep () 方法,当前线程进入阻塞状态。
- 运行在当前线程里的其它线程t2调用join()方法,当前线程进入阻塞状态。
- 等待用户输入的时候,当前线程进入阻塞状态。
Waiting Timed Waiting TERMINATED
线程池的创建方式
- 先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第 2 步;
- 判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第 3 步;
- 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理
线程池的工作过程
线程池的核心线程数设置为多少合适
线程池的队列分为哪些?可以使用无界队列么
阻塞队列,用于保存任务的阻塞队列,关于阻塞队列可以看这篇文章。可以使用
- ArrayBlockingQueue
- LinkedBlockingQueue
- SynchronousQueue
- PriorityBlockingQueue
阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。
线程池有哪些饱和策略
当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种: 1. AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常; 2. CallerRunsPolicy:只用调用者所在的线程来执行任务; 3. DiscardPolicy:不处理直接丢弃掉任务; 4. DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务
线程池有哪些类型
1、newCachedThreadPool
cachedThreadPool
线程池的特点是它的常驻核心线程数为 0
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 这种类型的线程池特点是: 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。 2、newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。 FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。 3、newSingleThreadExecutor 创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
4、newScheduleThreadPool 创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。 5、newSingleThreadScheduledExecutor 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行并且可定时或者延迟执行线程活动。
volatile 作用,特点,使用场景
在 Java 中,volatile 关键字有特殊的内存语义。volatile 主要有以下两个功能:
- 保证变量的内存可见性
- 禁止 volatile 变量与普通变量重排序(JSR133 提出,Java 5 开始才有这个“增强的 volatile 内存语义”)
所谓内存可见性,指的是当一个线程对 volatile
修饰的变量进行写操作时,JMM 会立即把该线程对应的本地内存中的共享变量的值刷新到主内存;当一个线程对 volatile
修饰的变量进行读操作时,JMM 会把立即该线程对应的本地内存置为无效,从主内存中读取共享变量的值。
在保证内存可见性这一点上,volatile有着与锁相同的内存语义,所以可以作为一个“轻量级”的锁来使用。但由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁可以保证整个临界区代码的执行具有原子性。所以在功能上,锁比volatile更强大;在性能上,volatile更有优势。
Synchronized 和 lock
- 存在层面:synchronized 是属于 jvm 层面的的关键字, 底层通过 monitorenter、monitorexit 指令实现的;而 lock 是属于一个类。
- 锁的释放:synchronized 在代码执行异常时或正常执行完毕后,jvm 会自动释放锁;而 lock 不行,使用 lock 必须加上异常处理,而且必须在 finally 块中写上 unlock () 释放锁。
- synchronized不能精确唤醒指定的线程;而lock可以通过Condition精确唤醒。可参考示例。
- 锁的状态:synchronized 无法判断锁的状态,从而无法知道是否获取锁;而 lock 可以判断锁的状态, 可参考示例。
- 锁的类型:Synchronized 是可重入,不可中断,非公平锁;Lock 锁则是 可重入,可判断,可公平锁
synchronized 和 reentrantLock
synchronized
有什么不足之处:
- 如果临界区是只读操作,其实可以多线程一起执行,但使用synchronized的话,同一时间只能有一个线程执行。
- synchronized无法知道线程有没有成功获取到锁
- 使用synchronized,如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。
synchronized 实现原理
synchronized 是 Java 中最基本的同步机制,它可以保证同一时刻只有一个线程可以执行被 synchronized 修饰的代码块或方法。synchronized 的实现基于 Java 对象头的监视器(monitor)机制,其主要流程如下:
- 在 Java 对象头中添加一个标记,标记该对象是否被锁定。如果对象未被锁定,则该标记为“未锁定”状态,如果对象被锁定,则该标记为“已锁定”状态。
- 当一个线程要进入一个被 synchronized 修饰的代码块或方法时,它首先要尝试获取锁定对象的锁,如果锁定对象的标记为“未锁定”,则线程可以获取到该锁,并将该标记设置为“已锁定”状态,同时线程可以执行 synchronized 代码块或方法。
- 如果锁定对象的标记为“已锁定”,则当前线程无法获取该锁,它会进入锁定对象的等待队列中等待锁的释放。
- 当锁定对象的持有者执行完 synchronized 代码块或方法时,它会释放锁定对象的锁,并将该标记设置为“未锁定”状态。同时,JVM 会从等待队列中唤醒一个等待线程,并将其设置为锁定对象的持有者。
需要注意的是,synchronized 的锁定对象可以是任意 Java 对象,但最好使用 final 类型的对象或者专门为锁定而创建的对象来作为锁定对象。另外,synchronized 还支持类锁和对象锁,类锁是指锁定整个类,而不是某个对象,可以使用 synchronized static 方法或者 synchronized(Class.class) 代码块来实现;对象锁是指锁定某个对象,可以使用 synchronized(this) 或者 synchronized(object) 代码块来实现。 总体来说,synchronized 是 Java 中非常重要的同步机制,虽然它的性能比较低,但它可以确保多线程环境下的线程安全性。
ConcurrentHashMap 的实现原理
ConcurrentHashMap 是 Java 中一个线程安全的散列表实现,它相比于 HashMap 具有更好的并发性能,原因如下:
- 分段锁机制:ConcurrentHashMap使用了一种称为分段锁(Segment)的机制来实现并发访问。这种机制将整个散列表分成了多个Segment,每个Segment独立维护一个小的散列表,并拥有自己的锁。这样,在多线程并发访问时,每个线程只需要获取对应的Segment锁,而不需要锁住整个散列表,从而减少了锁的竞争,提高了并发访问效率。
- 对读操作不加锁:ConcurrentHashMap中的读操作不需要加锁,因为每个Segment中的小散列表都是线程安全的。这样,在读多写少的场景下,可以充分利用并发性能,提高了效率。
- 使用CAS操作:ConcurrentHashMap中的put、remove等操作采用了CAS(Compare And Swap)操作来保证并发安全,而不是直接加锁。CAS操作可以实现非阻塞的并发访问,避免了锁的竞争,提高了并发访问效率。
- 分段设计减少冲突:ConcurrentHashMap中的每个Segment都是一个小的散列表,这样可以减少散列表冲突的概率,提高了并发访问效率。
线程间同步的方式
join 原理
ThreadLocal 的原理,使用场景
什么是可重入锁,不可重入会有什么问题
所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。 synchronized 和 ReentrantLock 都是可重入锁。 可重入锁的意义在于防止死锁。
公平锁与非公平锁
公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。 非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。
Java 几种锁
- synchronized 锁:Java 中最基本的一种锁,可以用来保证线程之间的互斥和同步。synchronized 关键字可以修饰方法和代码块,它内部使用了 monitorenter 和 monitorexit 指令实现锁的获取和释放。synchronized 锁有很多缺点,例如只支持独占锁和可重入,不支持公平性,同时也存在死锁和饥饿问题。
- ReentrantLock 锁:Java 并发包中提供的一个可重入锁,可以用来替代 synchronized 锁。ReentrantLock 实现了 Lock 接口,提供了很多高级功能,例如可重入性、公平性、中断等待和超时等待。ReentrantLock 的实现基于 AQS,可以确保在多线程环境下的线程安全性和高性能。
- ReadWriteLock 锁:Java 并发包中提供的一种读写锁,可以用来优化读多写少的并发场景。ReadWriteLock 接口提供了两个锁,一个读锁和一个写锁,读锁可以被多个线程同时持有,但写锁只能被一个线程持有。ReadWriteLock 的实现基于 ReentrantLock 和 Condition,可以确保在多线程环境下的线程安全性和高性能。
- StampedLock 锁:Java 并发包中提供的一种乐观锁,可以用来优化读多写少的并发场景。StampedLock 提供了三种锁,一个写锁、一个悲观读锁和一个乐观读锁。乐观读锁不会阻塞其他线程,只有在写锁被持有或发生冲突时才会转换成悲观读锁。StampedLock 的实现基于 CAS 操作和 volatile 关键字,可以确保在多线程环境下的线程安全性和高性能。
- synchronized 和 Lock 的扩展:Java 并发包中还提供了一些 synchronized 和 Lock 的扩展,例如 ReentrantReadWriteLock、ReentrantLock 的公平锁、重入读写锁等,可以根据实际需求选择不同的锁。
详细介绍一下锁升级
偏向锁
一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁的偏向的线程 ID。当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID。 如果是,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁 ;如果不是,就代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID,这个时候要分两种情况:
- 成功,表示之前的线程不存在了, Mark Word里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
- 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为 0,并设置锁标志位为 00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
轻量级锁
JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为 Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word 里面。 然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。 自旋也不是一直进行下去的,如果自旋到一定程度(和JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。
重量级锁
重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。
总结
每一个线程在准备获取共享资源时: 第一步,检查 MarkWord 里面是不是放的自己的 ThreadId , 如果是,表示当前线程是处于 “偏向锁” 。 第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。 第三步,两个线程都把锁对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord。 第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋 。 第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败 。 第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。
AtomicInteger 的原理
AtomicInteger 是 Java 并发包提供的一个线程安全的整型变量,它可以保证多个线程同时修改该变量时的原子性。AtomicInteger 的实现原理主要基于 CAS(Compare and Swap)机制和 volatile 关键字。
这里我们以 AtomicInteger
类的 getAndAdd(int delta)
方法为例,来看看 Java 是如何实现原子操作的。
先看看这个方法的源码:
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
这里的U其实就是一个Unsafe
对象:
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
所以其实AtomicInteger
类的getAndAdd(int delta)
方法是调用Unsafe
类的方法来实现的:
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
有序性、可见性、原子性;Java 有哪些操作是原子的
Java 中的原子性、有序性和可见性是 Java 内存模型(Java Memory Model,JMM)中的三个重要特性。
- 原子性:原子性是指一个操作是不可中断的,即这个操作要么全部执行成功,要么全部不执行。Java 中的原子性操作包括:基本类型的读写操作、引用类型的读写操作(包括volatile修饰的引用类型变量)、Atomic类中的方法(如AtomicInteger、AtomicLong等)。
- 可见性:可见性是指当一个线程修改了共享变量的值后,其他线程可以立即看到修改后的值。Java 中保证可见性的方式包括:使用synchronized、使用volatile修饰变量、使用Lock接口中的实现类等。
- 有序性:有序性是指在多线程环境下,程序的执行顺序是可以确定的。Java 中可以保证有序性的操作包括:使用synchronized、使用volatile修饰变量、使用Lock接口中的实现类等。
Java 中的原子操作包括:
- 基本类型的读写操作,如int、long、float、double、short、byte、char和boolean。
- 引用类型的读写操作,当用volatile修饰引用类型变量时,对该变量的读取和写入都是原子操作。
- Atomic类中的方法,如AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等类的方法,这些类提供了一些原子性的操作方法,如getAndIncrement、compareAndSet等。
- Unsafe类中的方法,如compareAndSwapInt、compareAndSwapLong等方法,这些方法是底层实现CAS操作的方法。
需要注意的是,Java 中的原子操作仅保证单个变量的原子性操作,对于多个变量的复合操作,如i++,需要使用锁或者Atomic类提供的方法来保证线程安全。
介绍一下 CachedThreadPool 工作机制,线程爆满会怎样
wait 和 sleep 的区别
- sleep 方法是不会释放当前的锁的,而 wait 方法会。
- wait可以指定时间,也可以不指定;而sleep必须指定时间。
- wait 释放 cpu 资源,同时释放锁;sleep 释放 cpu 资源,但是不释放锁,所以易死锁。
- wait 必须放在同步块或同步方法中,而 sleep 可以在任意位置。
CAS 是什么,优缺点
CAS 比较交换的过程可以通俗的理解为 CAS (V, O, N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当 V 和 O 相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值 O 就是目前来说最新的值了,自然而然可以将新值 N 赋值给 V。反之,V 和 O 不相同,表明该值已经被其他线程改过了则该旧值 O 不是最新版本的值了,所以不能将新值 N 赋给 V,返回 V 即可。当多个线程使用 CAS 操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程 优点:
- 高效性:CAS操作不需要加锁,因此避免了锁的开销和线程切换的开销,从而可以实现更高的并发性和吞吐量。
- 确保原子性:CAS操作是原子操作,可以保证多线程之间的共享变量的原子性操作。
- 不会发生死锁:由于CAS操作不需要加锁,因此不会发生死锁问题。
缺点:
- ABA问题:由于CAS操作只是比较内存地址V的值与预期值A是否相等,因此可能存在ABA问题,即在执行CAS操作时,其他线程可能已经将内存地址V的值修改为了新的值C,然后又将其修改回了预期值A,这时CAS操作会认为内存地址V的值没有被修改过,从而执行更新操作。为了解决ABA问题,可以使用带版本号的CAS操作,即比较内存地址V的值和版本号是否与预期值A和版本号相等。
- 自旋次数:如果CAS操作失败,需要重新尝试,直到CAS操作成功为止。如果自旋次数太多,会导致CPU占用过高,从而影响系统的性能。
- 仅适用于单个变量的操作:CAS操作只能用于单个变量的操作,对于多个变量的复合操作(如i++),CAS操作并不能保证线程安全。
总的来说,CAS是一种高效、原子性的并发控制方式,适用于单个变量的操作,但是可能存在ABA问题和自旋次数问题。在实际应用中,需要根据具体场景来选择合适的并发控制方式。
ABA 问题
用锁手写一下生产者消费者
乐观锁和悲观锁
锁可以从不同的角度分类。其中,乐观锁和悲观锁是一种分类方式。 悲观锁: 悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。 乐观锁: 乐观锁又称为“无锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。 由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁天生免疫死锁。 乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。