Java并发篇:6个必备的Java并发面试种子题目
线程创建和生命周期
线程的创建和生命周期涉及到线程的产生、执行和结束过程。让我们继续深入探索这个主题:
线程的创建方式有多种,你可以选择适合你场景的方式:
继承Thread类: 创建一个类,继承自Thread类,并重写run()方法。通过实例化这个类的对象,并调用start()方法,系统会自动调用run()方法执行线程逻辑。
public class MyThread extends Thread {
public void run() {
// 线程逻辑代码
}
}
// 创建并启动线程
MyThread thread = new MyThread();
thread.start();
实现Runnable接口: 创建一个类,实现Runnable接口,并实现run()方法。通过将实现了Runnable接口的对象作为参数传递给Thread类的构造函数,然后调用start()方法启动线程。
public class MyRunnable implements Runnable {
public void run() {
// 线程逻辑代码
}
}
// 创建并启动线程
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
实现Callable接口: 创建一个类,实现Callable接口,并实现call()方法。通过创建一个FutureTask对象,将Callable对象作为参数传递给FutureTask构造函数,然后将FutureTask对象传递给Thread类的构造函数,最后调用start()方法启动线程。
public class MyCallable implements Callable<Integer> {
public Integer call() {
// 线程逻辑代码
return 1;
}
}
// 创建并启动线程
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
通过线程池创建线程: 使用Java的线程池ExecutorService来管理线程的生命周期。通过提交Runnable或Callable任务给线程池,线程池会负责创建、执行和终止线程。
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(new Runnable() {
public void run() {
// 线程逻辑代码
}
});
executorService.shutdown();
线程的生命周期经历以下几个状态:
- 创建状态: 通过实例化Thread对象或者线程池来创建线程。此时线程处于新建状态。
- 就绪状态: 线程被创建后,调用start()方法使其进入就绪状态。在就绪状态下,线程等待系统分配执行的时间片。
- 运行状态: 一旦线程获取到CPU的时间片,就进入运行状态,执行run()方法中的线程逻辑。
- 阻塞状态(Blocked/Waiting/Sleeping): 在某些情况下,线程需要暂时放弃CPU的执行权,进入阻塞状态。阻塞状态可以分为多种情况:
- 中断状态: 可以通过调用线程的interrupt()方法将线程从运行状态转移到中断状态。线程可以检查自身是否被中断,并根据需要作出适当的处理。
- 终止状态: 线程执行完run()方法中的逻辑或者通过调用stop()方法被终止后,线程进入终止状态。终止的线程不能再次启动。
理解线程的创建和生命周期对于处理并发编程非常重要。通过选择合适的创建方式和正确地管理线程的生命周期,可以确保线程安全、高效地运行,从而优化程序性能。
深入剖析synchronized
synchronized关键字在Java中用于实现线程安全的代码块,在其背后使用JVM底层内置的锁机制。synchronized的设计考虑了各种并发情况,因此具有以下优点:
- 优点: 由于官方对synchronized进行升级优化,如当前锁升级机制,因此它具有不断改进的潜力。JVM会进行锁的升级优化,以提高并发性能。
然而,synchronized也有一些缺点: - 缺点: 如果使用不当,可能会导致锁粒度过大或锁失效的问题。此外,synchronized只适用于单机情况,对于分布式集群环境的锁机制不适用。
synchronized的锁机制包括以下几个阶段的升级过程:
- 无锁状态: 初始状态为无锁状态,多个线程可以同时访问临界区。
- 偏向锁: 当只有一个线程访问临界区时,JVM会将锁升级为偏向锁,以提高性能。在偏向锁状态下,偏向线程可以直接获取锁,无需竞争。
- (自旋)轻量级锁: 当多个线程竞争同一个锁时,偏向锁会升级为轻量级锁。在轻量级锁状态下,线程会自旋一定次数,尝试获取锁,而不是直接阻塞。
- 重量级锁: 当自旋次数超过阈值或者存在多个线程竞争同一个锁时,轻量级锁会升级为重量级锁。重量级锁使用了传统的互斥量机制,需要进行阻塞和唤醒操作。
需要注意的是,如果在轻量级锁状态下,有线程获取对象的HashCode时,会直接升级为重量级锁。这是因为锁升级过程中使用的mark头将HashCode部分隐去,以确保锁升级过程的正确性。
底层实现中,synchronized使用了monitor enter和monitor exit指令来进行进入锁和退出锁的同步操作。对于用户来说,这些操作是不可见的。synchronized锁的等待队列存储在对象的waitset属性中,用于线程的等待和唤醒操作。
双重检查单例模式解析
示例代码:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
// 私有构造方法
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
为什么需要使用volatile: 虽然synchronized关键字可以确保线程安全,但是如果没有volatile修饰,可能会发生指令重排的问题。volatile关键字的主要作用是防止指令重排,保证可见性和有序性。尽
管在实际工作中很少遇到指令重排导致的问题,但是理论上存在这种可能性,因此使用volatile修饰变量可以避免出现意外情况。
指令重排原因及影响: 指令重排是为了优化程序的执行速度,由于CPU的工作速度远大于内存的工作速度,为了充分利用CPU资源,处理器会对指令进行重新排序。例如在创建一个对象的过程中,通常被拆分为三个步骤:1)申请空间并初始化,2)赋值,3)建立地址链接关系。如果没有考虑逃逸分析,可能会发生指令重排的情况。
这种重排可能导致的问题是,当一个线程在某个时刻执行到步骤2,而另一个线程在此时获取到了对象的引用,但是这个对象还没有完成初始化,导致使用到未完全初始化的对象,可能会出现异常或不正确的结果。通过使用volatile关键字,可以禁止指令重排,确保对象的完全初始化后再进行赋值操作。
抽象队列同步器(Abstract Queued Synchronizer)解析
抽象队列同步器(Abstract Queued Synchronizer)是Java并发编程中非常重要的同步框架,被广泛应用于各种锁实现类,如ReentrantLock、CountDownLatch等。AQS提供了基于双端队列的同步机制,支持独占模式和共享模式,并提供了一些基本的操作方法。
在AQS中,用来表示是否是独占锁的Exclusive属性对象非常重要。它可以控制同一时间只有一个线程能够获取锁,并且支持重入机制。另外,AQS的state属性也非常关键,state的含义和具体用途是由具体的子类决定的。子类可以通过对state属性的操作来实现不同的同步逻辑。例如,在ReentrantLock中,state表示锁的持有数;在CountDownLatch中,state表示还需要等待的线程数。
此外,AQS还使用两个Node节点来表示双端队列,用于存储被阻塞的线程。这些节点会根据线程的不同状态(如等待获取锁、等待释放锁)被添加到队列的不同位置,从而实现线程同步和调度。
以下是一个简化的示例代码,展示了如何使用ReentrantLock和AQS进行线程同步:
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
lock.lock();
try {
// 执行线程1的逻辑
} finally {
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
lock.lock();
try {
// 执行线程2的逻辑
} finally {
lock.unlock();
}
});
thread1.start();
thread2.start();
}
}
在上述代码中,我们使用了ReentrantLock作为锁工具,它内部使用了AQS来实现线程同步。通过调用lock()方法获取锁,并在finally块中调用unlock()方法释放锁,确保线程安全执行。这样,只有一个线程能够获取到锁,并执行相应的逻辑。
总之,AQS作为Java线程同步的核心框架,在并发编程中起到了至关重要的作用。它提供了强大的同步机制,可以支持各种锁的实现,帮助我们实现线程安全的代码。
利用Java线程池
使用Java线程池是一种优化并行性的有效方式。线程池可以管理和复用线程,减少了线程创建和销毁的开销,提高了系统的性能和资源利用率。
在Java中,可以使用ExecutorService接口来创建和管理线程池。ExecutorService提供了一些方法来提交任务并返回Future对象,可以用于获取任务的执行结果。
在创建线程池时,可以根据实际需求选择不同的线程池类型。常用的线程池类型包括:
- FixedThreadPool:固定大小的线程池,线程数固定不变。
- CachedThreadPool:可根据需要自动调整线程数的线程池。
- SingleThreadExecutor:只有一个线程的线程池,适用于顺序执行任务的场景。
- ScheduledThreadPool:用于定时执行任务的线程池。
使用线程池时,可以将任务分解为多个小任务,提交给线程池并发执行。这样可以充分利用系统资源,提高任务执行的并行性。
同时,线程池还可以控制并发线程的数量,避免系统资源耗尽和任务过载的问题。通过设置合适的线程池大小,可以平衡系统的并发能力和资源消耗。
探索Java中的Fork/Join框架
Fork/Join框架是Java中用于处理并行任务的一个强大工具。它基于分治的思想,将大任务划分成小任务,并利用多线程并行执行这些小任务,最后将结果合并。
在Fork/Join框架中,主要有两个核心类:ForkJoinTask和ForkJoinPool。ForkJoinTask是一个可以被分割成更小任务的任务,我们需要继承ForkJoinTask类并实现compute()方法来定义具体的任务逻辑。ForkJoinPool是一个线程池,用于管理和调度ForkJoinTask。
下面是一个简单的例子,展示如何使用Fork/Join框架来计算一个整数数组的总和:
import java.util.concurrent.*;
public class SumTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 10;
private int[] array;
private int start;
private int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= THRESHOLD) {
int sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);
leftTask.fork(); // 将左半部分任务提交到线程池
rightTask.fork(); // 将右半部分任务提交到线程池
int leftResult = leftTask.join(); // 等待左半部分任务的完成并获取结果
int rightResult = rightTask.join(); // 等待右半部分任务的完成并获取结果
return leftResult + rightResult;
}
}
public static void main(String[] args) {
int[] array = new int[100];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
ForkJoinPool forkJoinPool = new ForkJoinPool();
SumTask sumTask = new SumTask(array, 0, array.length);
int result = forkJoinPool.invoke(sumTask); // 使用线程池来执行任务
System.out.println("Sum: " + result);
}
}
在这个例子中,我们定义了一个SumTask类,继承自RecursiveTask类,并实现了compute()方法。在compute()方法中,我们判断任务的大小是否小于阈值,如果是,则直接计算数组的总和;如果不是,则将任务划分成两个子任务,并使用fork()方法将子任务提交到线程池中,然后使用join()方法等待子任务的完成并获取结果,最后返回子任务结果的和。
在main()方法中,我们创建了一个ForkJoinPool对象,然后创建了一个SumTask对象,并使用invoke()方法来执行任务。最后打印出结果。
通过使用Fork/Join框架,我们可以方便地处理并行任务,并利用多核处理器的性能优势。这个框架在处理一些需要递归分解的问题时非常高效。
总结
文章涉及了几个常见的并发编程相关的主题。首先,线程的创建和生命周期是面试中常被问及的话题,面试官可能会询问如何创建线程、线程的状态转换以及如何控制线程的执行顺序等。其次,synchronized关键字是用于实现线程同步的重要工具,面试中可能会涉及到它的使用场景以及与其他同步机制的比较。此外,抽象队列同步器(AQS)是Java并发编程中的核心概念,了解其原理和应用场景可以展示对并发编程的深入理解。最后,面试中可能会考察对Java线程池和Fork/Join框架的了解,包括它们的使用方法、优势和适用场景等。种子题目务必学会
Java并发篇:6个必备的Java并发面试种子题目的更多相关文章
- Java高级程序员(5年左右)面试的题目集
Java高级程序员(5年左右)面试的题目集 https://blog.csdn.net/fangqun663775/article/details/73614850?utm_source=blogxg ...
- java提高篇(三)-----理解java的三大特性之多态
面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承 ...
- java提高篇(二)-----理解java的三大特性之继承
在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事情.在这句 ...
- 003java面试笔记——【java基础篇】从团八百失败面试总结的java面试题(未完待续)
8.java 线程 1)线程概念,线程与进程 线程:线程是“进程”中某个单一顺序的控制流.也被称为轻量进程.线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程.线 ...
- Java提高篇(三三)-----Map总结
在前面LZ详细介绍了HashMap.HashTable.TreeMap的实现方法,从数据结构.实现原理.源码分析三个方面进行阐述,对这个三个类应该有了比较清晰的了解,下面LZ就Map做一个简单的总结. ...
- Java提高篇(二六)-----hashCode
在前面三篇博文中LZ讲解了(HashMap.HashSet.HashTable),在其中LZ不断地讲解他们的put和get方法,在这两个方法中计算key的hashCode应该是最重要也是最 ...
- java基础篇---I/O技术(三)
接上一篇java基础篇---I/O技术(二) Java对象的序列化和反序列化 什么叫对象的序列化和反序列化 要想完成对象的输入或输出,还必须依靠对象输出流(ObjectOutputStream)和对象 ...
- Java自动化测试框架-11 - TestNG之annotation与并发测试篇 (详细教程)
1.简介 TestNG中用到的annotation的快速预览及其属性. 2.TestNG基本注解(注释) 注解 描述 @BeforeSuite 注解的方法只运行一次,在当前suite所有测试执行之前执 ...
- Java并发篇
Java并发篇 作者:星晴(当地小有名气,小到只有自己知道的杰伦粉) 1. Java锁 1.1 乐观锁 1.2 悲观锁 1.3 自旋锁 1.4 Synchronized 同步锁 1.4.1 核心组件 ...
- [ Java面试题 ]并发篇
1.Java内存模型是什么? Java内存模型规定和指引Java程序在不同的内存架构.CPU和操作系统间有确定性地行为.它在多线程的情况下尤其重要.Java内存模型对一个线程所做的变动能被其它线程可见 ...
随机推荐
- Nginx的负载均衡策略
Nginx的负载均衡策略 个人博客地址: https://note.raokun.top 拥抱ChatGPT,国内访问网站:https://www.playchat.top 共六种: 轮询.权重.ip ...
- 基于 Rainbond 的混合云管理解决方案
内容概要:文章探讨了混合云场景中的难点.要点,以及Rainbond平台在跨云平台的混合云管理方面的解决方案.包括通过通过统一控制台对多集群中的容器进行编排和管理,实现了对混合云中应用的一致性管理.文章 ...
- 2020-11-16:手写代码:leetcode第406题。假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。
福哥答案2020-11-16: ①排序.按照[身高]降序排列.如果[身高]一样,按照[人数]升序排列.②插入.遍历这个队列,按照[人数]插入相应位置. 采用leetcode里的代码,golang代码如 ...
- 2021-04-02:给定一个正方形或者长方形矩阵matrix,实现zigzag打印。[[0,1,2],[3,4,5],[6,7,8]]的打印顺序是0,1,3,6,4,2,5,7,8。
2021-04-02:给定一个正方形或者长方形矩阵matrix,实现zigzag打印.[[0,1,2],[3,4,5],[6,7,8]]的打印顺序是0,1,3,6,4,2,5,7,8. 福大大 答案2 ...
- 2021-09-18:给定一个只包括 ‘(‘,‘)‘,‘{‘,‘}‘,‘[‘,‘]‘ 的字符串 s ,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合
2021-09-18:给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效.有效字符串需满足:左括号必须用相同类型的右括号闭合.左括号必须以正确的顺序闭合 ...
- Grafana系列-统一展示-9-Jaeger数据源
系列文章 Grafana 系列文章 配置 Jaeger data source Grafana内置了对Jaeger的支持,它提供了开源的端到端分布式跟踪.本文解释了针对Jaeger数据源的配置和查询. ...
- url函数
url() 函数看起来的格式象:url(r^/account/$', views.index, name=index),它可以接收四个参数,分别是两个必选参数:regex.view 和两个可选参数:k ...
- 如何在Docker Compose中限制容器的CPU和内存使用?
最近我在使用开源统计Umami过程中,发现CPU和内存经常占满,导致其它服务都不能正常使用.Umami我使用的docker-compose进行部署,于是打算使用docker-compose限制下容器的 ...
- 用好kafka,你不得不知的那些工具
前言 工欲善其事,必先利其器.本文主要分享一下消息中间件kafka安装部署的过程,以及我平时在工作中针对kafka用的一些客户端工具和监控工具. kafka部署架构 一个kafka集群由多个kafka ...
- 代码随想录算法训练营Day20 二叉树| 235. 二叉搜索树的最近公共祖先 701.二叉搜索树中的插入操作 450.删除二叉搜索树中的节点
代码随想录算法训练营 235. 二叉搜索树的最近公共祖先 题目链接:235. 二叉搜索树的最近公共祖先 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先. 百度百科中最近公共祖先的定义为:& ...