1.前言
为了更好地掌握Java并发编程技术,建议从以下几个方面入手:
深入理解Java内存模型(JMM):JMM是Java并发编程的基础,它定义了线程间共享变量的可见性、原子性和有序性等规则。
熟练掌握多线程编程:了解线程的创建、启动、同步、通信等基本操作,以及如何使用锁来保证线程安全。
学习并发容器:了解Java提供的并发容器,如ConcurrentHashMap、CopyOnWriteArrayList等,以及它们的使用场景和性能特点。
掌握线程池技术:了解线程池的原理和使用方法,以及如何选择合适的线程池类型和参数配置。
学习并发编程的最佳实践:了解如何避免常见的并发问题,如死锁、活锁等,以及如何提高程序的性能和可维护性。
2.Java多线程&并发
2.1 JAVA 线程实现/创建方式
继承Thread类
实现Runable接口
实现Callable接口
创建线程池Executors工具类
继承 Thread 类
static class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
myThread1.start();
}
实现 Runnable 接口
static class MyThread implements Runnable{
public void run() {
System.out.println("Runnable.run()");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
}
Callable、Future 有返回值线程
static class MyCallable implements Callable<String>{
int i=0;
public MyCallable(int i) {
this.i = i;
}
@Override
public String call() throws Exception {
return i+"";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < 10; i++) {
Callable c = new MyCallable(i);
// 执行任务并获取 Future 对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res:" + f.get().toString());
}
}
基于线程池的方式
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true) {
threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
2.2 线程生命周期(状态)
在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换
阻塞状态(BLOCKED):
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
等待阻塞(o.wait->等待对列):
运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
同步阻塞(lock->锁池)
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
其他阻塞(sleep/join)
运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
2.3 sleep() 和 wait() 有什么区别?
1、相同点
sleep()和wait()都可以暂停线程的执行。
2、不同点
所在类不同
sleep()是Thread类的静态方法。
wait()是Object类的方法。
java.lang.Thread#sleep
public static native void sleep(long millis) throws InterruptedException;
java.lang.Object#wait
public final native void wait(long timeout) throws InterruptedException;
锁释放不同
sleep()是不释放锁的。
wait()是释放锁的。
Object lock = new Object();
synchronized (lock) {
try {
lock.wait(3000L);
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
如上代码所示,wait 可以释放当前线程对 lock 对象锁的持有,而 sleep 则不会
用途不同
sleep()常用于一定时间内暂停线程执行。
wait()常用于线程间交互和通信。
用法不同
sleep()方法睡眠指定时间之后,线程会自动苏醒。
wait()方法被调用后,可以通过notify()或notifyAll()来唤醒wait的线程。
2.4 线程的run()和start()有什么区别?
start方法用来启动相应的线程;
run方法只是thread的一个普通方法,在主线程里执行;
需要并行处理的代码放在run方法中,start方法启动线程后自动调用run方法;
run方法必须是public的访问权限,返回类型为void。
2.5 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
线程的run()方法是由java虚拟机直接调用的,如果我们没有启动线程(没有调用线程的start()方法)而是在应用代码中直接调用run()方法,那么这个线程的run()方法其实运行在当前线程(即run()方法的调用方所在的线程)之中,而不是运行在其自身的线程中,从而违背了创建线程的初衷,也就是说并没有创建一个新的线程。
代码举例说明:
public class ManageThread extends Thread {
//在该方法中实现线程的任务逻辑
@Override
public void run() {
//获取当前正在执行的线程名称
System.out.println(Thread.currentThread().getName());
}
}
public class TestDemo {
public static void main(String[] args) {
Thread manageThreadFirst = new ManageThread(); //创建线程(动态规划)
manageThreadFirst.run(); //直接调用run()方法
System.out.println(Thread.currentThread().getName());//打印当前线程的名称
System.out.println("==============上方是未调用start()的打印结果");
Thread manageThreadSecond = new ManageThread(); //创建线程
manageThreadSecond.start(); //启动线程,start()自动调用run()
}
}
运行结果:
main
main
==============上方是未调用start()的打印结果
Thread-1
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
2.6 notify() 和 notifyAll() 有什么区别?
notify()
唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。
直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
notifyAll():
唤醒在此对象监视器上等待的所有线程。线程通过调用其中一个 wait 方法,在对象的监视器上等待。
直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
2.7 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?
Java中,任何对象都可以作为锁,既然wait是放弃对象锁,当然就要把wait定义在这个对象所属的类中。更通用一些,由于所有类都继承于Object,我们完全可以把wait方法定义在Object类中,这样,当我们定义一个新类,并需要以它的一个对象作为锁时,不需要我们再重新定义wait方法的实现,而是直接调用父类的wait(也就是Object的wait),此处,用到了Java的继承。
有的人会说,既然是线程放弃对象锁,那也可以把wait定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。
综上所述,wait()、notify()和notifyAll()方法要定义在Object类中。
2.8 Thread 类中的 yield 方法有什么作用?
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
2.9 Java中interrupt、interrupted和isInterrupted的区别?
interrupt:interrupt()常用于停止线程,但是单独调用并不会停止线程,需要加入一个判断才能停止线程。下面是源码:
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
interrupted和isInterrupted:这两个方法用于判断线程是否停止,
interrupted() 是静态方法,判断当前执行线程是否停止。
isInterrupted()是对象方法,判断该对象是否已经中断;
这两个的区别是:interrupted具有清楚状态的功能,如果两次调用interrupt()方法,则第二次会将中断状态清除,变为false;而isInterrupt()没有这个功能;
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
下面对interrupt()清除状态演示;
public static void main(String[] args){
Thread.currentThread().interrupt();
System.out.println("当前线程是否停止,第一次调用结果"+Thread.interrupted());
System.out.println("当前线程是否停止,第二次调用结果"+Thread.interrupted());
System.out.println("当前线程是否停止,第三次调用结果"+Thread.interrupted());
Thread thread=new Thread();
System.out.println("当前线程是否停止"+thread.isInterrupted());
System.out.println("当前线程是否停止"+thread.isInterrupted());
}
结果
当前线程是否停止,第一次调用结果true
当前线程是否停止,第二次调用结果false
当前线程是否停止,第三次调用结果false
当前线程是否停止false
当前线程是否停止false
2.10 Java 如何实现多线程之间的通讯和协作?
Java中线程通信协作的最常见的两种方式:
syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()线程间直接的数据交换:
通过管道进行线程间通信:1)字节流;2)字符流
2.12 什么是线程同步和线程互斥,有哪几种实现方式?
实现线程同步的方法
同步代码方法:sychronized 关键字修饰的方法
同步代码块:sychronized 关键字修饰的代码块
使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义
2.13 如何让 Java 的线程彼此同步?
synchronized
volatile
ReenreantLock
使用局部变量实现线程同步
2.14 Java 中的线程池是如何实现的?
创建一个阻塞队列来容纳任务,在第一次执行任务时创建足够多的线程,并处理任务,之后每个工作线程自动从任务队列中获取线程,直到任务队列中任务为0为止,此时线程处于等待状态,一旦有工作任务加入任务队列中,即刻唤醒工作线程进行处理,实现线程的可复用性。
线程池一般包括四个基本组成部分:
(1)线程池管理器
用于创建线程池,销毁线程池,添加新任务。
(2)工作线程
线程池中线程,可循环执行任务,在没有任务时处于等待状态。
(3)任务队列
用于存放没有处理的任务,一种缓存机制。
(4)任务接口
每个任务必须实现的接口,供工作线程调度任务的执行,主要规定了任务的开始和收尾工作,和任务的状态。
2.15 创建线程池几种方式?
线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):
Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置。
2.16 创建线程池的几个核心构造参数(线程池7大参数)?
ThreadPoolExecutor7大参数,代码如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// 省略...
}
corePoolSize:核心线程数,线程池中始终存活的线程数。
maximumPoolSize:最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。
keepAliveTime:最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
TimeUnit:单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:
TimeUnit.DAYS:天
TimeUnit.HOURS:小时
TimeUnit.MINUTES:分
TimeUnit.SECONDS:秒
TimeUnit.MILLISECONDS:毫秒
TimeUnit.MICROSECONDS:微妙
TimeUnit.NANOSECONDS:纳秒
workQueue:一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻 塞方法。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关。
threadFactory:线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。
handler:拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:默认策略为 AbortPolicy。
AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
2.17 线程池原理?
线程池ThreadPoolExecutor执行流程如下:
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。
线程拒绝策略:
我们来演示一下 ThreadPoolExecutor 的拒绝策略的触发,我们使用 DiscardPolicy 的拒绝策略,它会忽略并抛弃当前任务的策略,实现代码如下:
public static void main(String[] args) {
// 任务的具体方法
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("当前任务被执行,执行时间:" + new Date() +
" 执行线程:" + Thread.currentThread().getName());
try {
// 等待 1s
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 创建线程,线程的任务队列的长度为 1
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
new ThreadPoolExecutor.DiscardPolicy());
// 添加并执行 4 个任务
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
}
我们创建了一个核心线程数和最大线程数都为 1 的线程池,并且给线程池的任务队列设置为 1,这样当我们有 2 个以上的任务时就会触发拒绝策略,执行的结果如下图所示:
从上述结果可以看出只有两个任务被正确执行了,其他多余的任务就被舍弃并忽略了。其他拒绝策略的使用类似,这里就不一一赘述了。
自定义拒绝策略
除了 Java 自身提供的 4 种拒绝策略之外,我们也可以自定义拒绝策略,示例代码如下:
public static void main(String[] args) {
// 任务的具体方法
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("当前任务被执行,执行时间:" + new Date() +
" 执行线程:" + Thread.currentThread().getName());
try {
// 等待 1s
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 创建线程,线程的任务队列的长度为 1
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 执行自定义拒绝策略的相关操作
System.out.println("我是自定义拒绝策略~");
}
});
// 添加并执行 4 个任务
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
}
程序的执行结果如下:
究竟选用哪种线程池?
经过以上的学习我们对整个线程池也有了一定的认识了,那究竟该如何选择线程池呢?
我们来看下阿里巴巴《Java开发手册》给我们的答案:
【强制要求】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
所以综上情况所述,我们推荐使用 ThreadPoolExecutor 的方式进行线程池的创建,因为这种创建方式更可控,并且更加明确了线程池的运行规则,可以规避一些未知的风险。
总结
本文我们介绍了线程池的 7 种创建方式,其中最推荐使用的是 ThreadPoolExecutor 的方式进行线程池的创建,ThreadPoolExecutor 最多可以设置 7 个参数,当然设置 5 个参数也可以正常使用,ThreadPoolExecutor 当任务过多(处理不过来)时提供了 4 种拒绝策略,当然我们也可以自定义拒绝策略,希望本文的内容能帮助到你。
2.18 线程池7中创建方式?@2.16已具体介绍过
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。
1.FixedThreadPool
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
使用示例如下:
public static void fixedThreadPool() {
// 创建 2 个数据级的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 创建任务
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
}
};
// 线程池执行任务(一次添加 4 个任务)
// 执行任务的方法有两种:submit 和 execute
threadPool.submit(runnable); // 执行方式 1:submit
threadPool.execute(runnable); // 执行方式 2:execute
threadPool.execute(runnable);
threadPool.execute(runnable);
}
执行结果如下:
如果觉得以上方法比较繁琐,还用更简单的使用方法,如下代码所示:
public static void fixedThreadPool() {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 执行任务
threadPool.execute(() -> {
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
});
}
2.CachedThreadPool
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
使用示例如下:
public static void cachedThreadPool() {
// 创建线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
// 执行任务
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
});
}
}
执行结果如下:
从上述结果可以看出,线程池创建了 10 个线程来执行相应的任务。
3.SingleThreadExecutor
创建单个线程数的线程池,它可以保证先进先出的执行顺序。
使用示例如下:
public static void singleThreadExecutor() {
// 创建线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 执行任务
for (int i = 0; i < 10; i++) {
final int index = i;
threadPool.execute(() -> {
System.out.println(index + ":任务被执行");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
});
}
}
执行结果如下:
4.ScheduledThreadPool
创建一个可以执行延迟任务的线程池。
使用示例如下:
public static void scheduledThreadPool() {
// 创建线程池
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
// 添加定时执行任务(1s 后执行)
System.out.println("添加任务,时间:" + new Date());
threadPool.schedule(() -> {
System.out.println("任务被执行,时间:" + new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
}, 1, TimeUnit.SECONDS);
}
执行结果如下:
从上述结果可以看出,任务在 1 秒之后被执行了,符合我们的预期。
5.SingleThreadScheduledExecutor
创建一个单线程的可以执行延迟任务的线程池。
使用示例如下:
public static void SingleThreadScheduledExecutor() {
// 创建线程池
ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
// 添加定时执行任务(2s 后执行)
System.out.println("添加任务,时间:" + new Date());
threadPool.schedule(() -> {
System.out.println("任务被执行,时间:" + new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
}, 2, TimeUnit.SECONDS);
}
执行结果如下:
从上述结果可以看出,任务在 2 秒之后被执行了,符合我们的预期。
6.newWorkStealingPool
创建一个抢占式执行的线程池(任务执行顺序不确定),注意此方法只有在 JDK 1.8+ 版本中才能使用。
使用示例如下:
public static void workStealingPool() {
// 创建线程池
ExecutorService threadPool = Executors.newWorkStealingPool();
// 执行任务
for (int i = 0; i < 10; i++) {
final int index = i;
threadPool.execute(() -> {
System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
});
}
// 确保任务执行完成
while (!threadPool.isTerminated()) {
}
}
执行结果如下:
从上述结果可以看出,任务的执行顺序是不确定的,因为它是抢占式执行的。
7.ThreadPoolExecutor
最原始的创建线程池的方式,它包含了 7 个参数可供设置。
使用示例如下:
public static void myThreadPoolExecutor() {
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
// 执行任务
for (int i = 0; i < 10; i++) {
final int index = i;
threadPool.execute(() -> {
System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
执行结果如下:
2.19 如果你提交任务时,线程池队列已满,这时会发生什么?
这里区分一下:
(1)如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
(2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
2.20 在 Java 程序中怎么保证多线程的运行安全?
使用安全类,比如 Java. util. concurrent 下的类。
使用自动锁 synchronized。
使用手动锁 Lock。
保证一个或者多个操作在CPU执行的过程中不被中断。
保证一个线程对共享变量的修改,另外一个线程能够立刻看到。
保证程序执行的顺序按照代码的先后顺序执行。
2.21 终止线程 4 种方式
正常运行结束
使用退出标志退出线程
Interrupt 方法结束线程
stop 方法终止线程(线程不安全)
3.并发编程
3.1.Java中垃圾回收有什么目的?什么时候进行垃圾回收?
java中的垃圾回收(Garbage Collection,简称GC)主要有以下目的:
自动内存管理:垃圾回收的主要目的是自动管理程序的内存。在Java中,内存分配和回收都是自动进行的,程序员无需手动进行这些操作。这大大降低了内存泄漏的可能性,并使得程序员可以更加专注于程序的开发,而不是内存管理。
提高内存使用效率:垃圾回收能够实时回收程序中不再使用的对象所占用的内存,使得这部分内存可以被重新利用。这样可以避免内存浪费,提高内存的使用效率。
提高程序性能:如果程序中存在大量的无用对象,那么这些对象会占用大量的内存空间,从而降低程序的性能。通过垃圾回收,可以定期清理这些无用对象,释放它们占用的内存空间,从而提高程序的性能。
Java的垃圾回收主要在以下时机进行:
当内存不足时:当Java虚拟机(JVM)中的堆内存空间不足时,垃圾回收器会被触发,以回收不再使用的对象所占用的内存空间。
定时进行:除了在内存不足时触发外,垃圾回收器还会在特定的时间点进行回收操作。这个时间点通常是由JVM根据程序的运行情况动态确定的。
显式调用:虽然Java的垃圾回收是自动进行的,但程序员也可以通过调用System.gc()方法来显式地触发垃圾回收。不过需要注意的是,System.gc()方法只是一个建议给JVM进行垃圾回收的提示,JVM可能会忽略这个提示。
总的来说,Java的垃圾回收是一个非常重要的机制,它可以帮助程序员有效地管理内存,提高程序的性能和稳定性。
3.2.synchronized 的作用?
在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。
3.3.说说自己是怎么使用 synchronized 关键字,在项目中用到了吗?
synchronized关键字最主要的三种使用方式:
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
双重校验锁实现对象单例(线程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
3.4.说一下 synchronized 底层实现原理?
1)synchronized作用:
原子性,有序性,可见性
2)synchronized的使用:
修饰实例方法,给当前实例对象加锁
修饰静态方法,给当前类加锁
修饰代码块,给synchronized括号内对象加锁
3)实现原理
JVM基于进入和退出Monitor对象实现同步方法和代码块的。
方法及的是隐式的,JVM可以通过常量池中的方法表结构的ACC_SYNCHRONIZED访问标志区分一个方法是否同步。
代码块的同步是利用monitorenter和monitorexit这两个字节码指令。他们分别位于代码块的开始和结束位置。当JVM执行到monitorenter指令事,当前线程视图获取Monitor对象的所用权。
3.5.volatile关键字
1.volatile概述:
虚拟机提供轻量级的同步机制
2.volatile的特性:
保证原子性,不保证可见性,禁止指令重排
3.6.volatile和synchronized 的区别
1.volatile是变量修饰符;synchronized可以修饰方法和代码块
2.volatile保证可见性,不保证原子性;synchronized则可以保证可见性和原子性。
3.7.CAS原理及使用
1.CSA是什么?
CAS(比较并交换)全称为Compare-And-Swap.它是一条CPU并发原语。
2.Unsafe类
3.CAS缺点
循环时间长开销大
只能保证一个共享变量的原子操作
引出来ABA问题
4.解决方案:原子引用加版本号->AtomicStampedReference 带时间戳的原子引用
3.8.synchronized、volatile、CAS 比较
synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
volatile 提供多线程共享变量可见性和禁止指令重排序优化。
CAS 是基于冲突检测的乐观锁(非阻塞)
3.9.synchronized 和 Lock 有什么区别?
首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
3.10.synchronized 和 ReentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量。
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
相同点:两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
主要区别如下:
ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
4.Lock锁
对锁的理解
1)乐观锁和悲观锁理解
乐观锁,顾名思义就是很乐观,每次拿数据的时候都认为被人不会修改,所以不上锁,但是在更新的时候会判断下期间有没有人更新这个数据,可以使用版本号机制
悲观锁,就是假象最坏的情况,每次拿数据的时候都会认为别人会修改,所以每次那数据的时候都会上锁,别人拿这个数据就会阻塞直到它拿到锁。
乐观锁的实现有版本号机制,或者是CAS算法。
2)公平锁和非公平锁理解
公平锁,是指在并发环境下,每个线程在获取锁时先查看此锁维护的等待队列,如果为空,或者是当前线程是等待队列的第一个就占有锁。否则就加入等待队列中,以后会按照FIFO(先进先出)的规则中取到自己
非公平锁,分配锁时不考虑队列等待情况,上来就直接尝试占有锁,如果尝试失败就采用类似公平锁的方式
3)独占锁 (写锁)/共享锁(读锁)
独占锁:指的是该锁只能被一个线程可以持有。ReentrantLock和Synchronized而言都是独占锁
共享锁:指的是该所被多个线程所持有。
4)可重入锁(递归锁)
可重入锁:线程可以进入任何一个它拥有锁的同步代码块。
5.什么是AQS锁?
AQS是一个抽象类,可以用来构造锁和同步类,如ReentrantLock,Semaphore,CountDownLatch,CyclicBarrier。
AQS的原理是,AQS内部有三个核心组件,一个是state代表加锁状态初始值为0,一个是获取到锁的线程,还有一个阻塞队列。当有线程想获取锁时,会以CAS的形式将state变为1,CAS成功后便将加锁线程设为自己。当其他线程来竞争锁时会判断state是不是0,不是0再判断加锁线程是不是自己,不是的话就把自己放入阻塞队列。这个阻塞队列是用双向链表实现的
可重入锁的原理就是每次加锁时判断一下加锁线程是不是自己,是的话state+1,释放锁的时候就将state-1。当state减到0的时候就去唤醒阻塞队列的第一个线程。
5.1 为什么AQS使用的双向链表?
因为有一些线程可能发生中断 ,而发生中断时候就需要在同步阻塞队列中删除掉,这个时候用双向链表方便删除掉中间的节点
5.2 有哪些常见的AQS锁
AQS分为独占锁和共享锁
ReentrantLock(独占锁):可重入,可中断,可以是公平锁也可以是非公平锁,非公平锁就是会通过两次CAS去抢占锁,公平锁会按队列顺序排队
Semaphore(信号量):设定一个信号量,当调用acquire()时判断是否还有信号,有就获取一个信号量,没有就阻塞等待其他线程释放信号量,当调用release()时释放一个信号量,唤醒阻塞线程。
应用场景:允许多个线程访问某个临界资源时,如上下车,买卖票
CountDownLatch(倒计数器):给计数器设置一个初始值,当调用CountDown()时计数器减一,当调用await() 时判断计数器是否归0,不为0就阻塞,直到计数器为0。
应用场景:启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行
CyclicBarrier(循环栅栏):给计数器设置一个目标值,当调用await() 时会计数+1并判断计数器是否达到目标值,未达到就阻塞,直到计数器达到目标值
应用场景:多线程计算数据,最后合并计算结果的应用场景
评论