有需要互关的小伙伴,关注一下,有关必回关,争取今年认证早日拿到博客专家
Java高频面试之总纲篇
Java高频面试之集合篇
Java高频面试之异常篇
Java高频面试之并发篇
Java高频面试之SSM篇
Java高频面试之Mysql篇
Java高频面试之Redis篇
Java高频面试之消息队列与分布式篇
50道SQL面试题
奇奇怪怪的面试题
五花八门的内存溢出
并行和并发有什么区别?
并行是同时执行多个任务,而并发是多个任务在一段时间内交替执行。
并行(Parallel)是指同时执行多个任务或操作,通过同时利用多个计算资源来提高系统的处理能力。在并行处理中,任务被划分为多个子任务,并且这些子任务可以同时执行,每个子任务分配给不同的处理单元(如多核处理器或分布式系统中的多个计算节点)。通过并行执行,可以加快任务的完成速度,提高系统的吞吐量。
并发(Concurrent)是指多个任务在一段时间内交替执行。这并不意味着同时执行多个任务,而是任务在时间上重叠执行。在并发处理中,系统可能只有一个处理单元,但任务通过时间片轮转或其他调度算法进行切换,以实现多个任务的看似同时执行。并发可以提高系统的响应能力和资源利用率,特别是在涉及输入/输出等等待时间的情况下。
总结
并行:同时执行多个任务,通过利用多个计算资源提高系统处理能力。
并发:多个任务在一段时间内交替执行,提高系统的响应能力和资源利用率。
并行可以在多个计算资源上同时执行多个任务,而并发是在一个计算资源上交替执行多个任务。
线程和进程的区别?
定义:进程是程序的执行实例,它具有独立的内存空间和系统资源。线程是进程中的一个执行单元,多个线程可以共享进程的内存空间和系统资源。
线程切换开销相对小,但需要进行同步操作;进程是具有独立地址空间和资源的执行实例,切换开销大,但数据隔离性好。
守护线程是什么?
守护线程(Daemon /ˈdiːmən/ Thread)是在程序运行过程中在后台提供服务的线程。当所有的非守护线程结束时,守护线程也会随之自动结束,无论它是否执行完任务。
守护线程在后台默默地运行,为其他线程提供支持和服务,如垃圾回收、监控、自动保存等
与非守护线程相比,守护线程有以下特点:
生命周期:守护线程的生命周期与程序的生命周期相同,即当所有的非守护线程结束时,守护线程也会被终止。
任务执行:守护线程通常用于执行一些支持性任务,不负责执行核心业务逻辑。
程序退出:如果只剩下守护线程在运行,程序会自动退出而不等待守护线程执行完任务。
Thread daemonThread = new Thread(new Runnable() {
public void run() {
// 守护线程的任务逻辑
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start(); // 启动线程
创建线程的几种方式?
- 继承Thread类重写run方法(无参构造方法)
class MyThread extends Thread {
public void run() {
// 线程的执行逻辑
}
}
// 创建并启动线程
MyThread myThread = new MyThread();
myThread.start();
-
创建Thread,传入一个Runnable对象(有参构造)
class MyRunnable implements Runnable { public void run() { // 线程的执行逻辑 } } // 创建并启动线程 MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start();
创建Runnable的方法
- 实现Runnable接口,重写run方法,并将其作为参数传递给Thread的构造方法
class MyRunnable implements Runnable {
public void run() {
// 线程的执行逻辑
}
}
- 使用匿名内部类
Thread thread = new Thread(new Runnable() {
public void run() {
// 线程的执行逻辑
}
});
thread.start();
- 使用lambda表达式
Thread thread = new Thread(() -# {
// 线程的执行逻辑
});
thread.start();
通常情况下,推荐使用实现Runnable接口或使用Lambda表达式的方式,因为它们能更好地支持代码的组织和重用,同时避免了单继承的限制
Runnable 和 Callable 有什么区别?
Runnable和Callable接口都用于创建可并发执行的任务,但它们有以下区别:
-
返回值:Runnable接口的run()方法没有返回值,它表示一个没有返回结果的任务。而Callable接口的call()方法可以返回任务的执行结果,它是一个带有泛型参数的接口,用于定义具有返回值的任务。
-
异常处理:Runnable接口的run()方法不能抛出检查异常,只能捕获并处理。而Callable接口的call()方法可以抛出检查异常,调用者需要显式处理这些异常或将其向上抛出。
@FunctionalInterface public interface Runnable { /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run(); }
@FunctionalInterface public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }
-
使用方式:Runnable接口通常作为线程的执行体,将任务逻辑放在run()方法中实现。Callable接口通常与ExecutorService一起使用,通过submit()方法提交Callable任务给线程池执行,并通过返回的Future对象获取任务的执行结果。
-
返回结果的获取:Runnable任务执行完毕后,无法直接获取任务的执行结果。而Callable任务执行完毕后,可以通过Future对象的get()方法获取任务的执行结果,该方法会阻塞调用线程直到任务执行完毕并返回结果。
// 使用Runnable接口创建任务
Runnable myRunnable = new Runnable() {
public void run() {
// 任务逻辑
}
};
// 使用Callable接口创建任务
Callable<Integer> myCallable = new Callable<Integer>() {
public Integer call() throws Exception {
// 任务逻辑
return 42; // 返回结果
}
};
在Java中,通常使用ExecutorService来执行Runnable和Callable任务。ExecutorService提供了submit()方法用于提交任务,并返回代表任务执行结果的Future对象。使用Future对象可以判断任务是否完成,获取任务的执行结果,或取消任务的执行。
线程状态及转换?
操作系统中线程的5种状态
java中线程的6种状态
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* /ˈtɜːmɪneɪtɪd/
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
https://blog.csdn.net/acc__essing/article/details/127470780?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168404937816782425125388%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=168404937816782425125388&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-4-127470780-null-null.142^v87^control_2,239^v2^insert_chatgpt&utm_term=java%20%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E4%BB%A5%E5%8F%8A%E5%88%87%E6%8D%A2&spm=1018.2226.3001.4187
https://my.oschina.net/goldenshaw?tab=newest&catalogId=3277710
sleep() 和 wait() 的区别?
-
sleep() 用于暂停当前线程一段时间,不涉及锁或线程之间的协作(无任何同步语义,让出cpu,但不释放锁)。
wait() 用于线程之间的协作,调用该方法后,线程会释放对象锁并进入等待状态,直到其他线程唤醒它(让出cpu,释放锁)。
-
sleep() 是线程的静态方法,可以直接调用;而 wait() 是对象的方法,必须在同步代码块或同步方法中使用,并且需要配合 notify() 或 notifyAll() 方法使用。
线程的 run() 和 start() 有什么区别?
run() 方法只是在当前线程中同步执行线程代码(run方法的逻辑),而 start() 方法会创建一个新线程,并在新线程中异步执行线程代码。通常情况下,我们使用 start() 方法来启动线程,以便实现多线程并发执行的效果。
简单说就是一个在当前线程中执行run(),一个是新建一个线程执行run()
在 Java 程序中怎么保证多线程的运行安全?
- 同步代码块(Synchronized Blocks)
synchronized (sharedObject) {
// 需要同步的代码块
}
- 同步方法(Synchronized Methods)
public synchronized void myMethod() {
// 需要同步的代码
}
- 锁(Lock)
Lock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 需要同步的代码
} finally {
lock.unlock(); // 释放锁
}
- 原子类(Atomic Classes
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet(); // 原子递增操作
- 使用线程安全的数据结构:在多线程环境下,使用线程安全的数据结构(如 ConcurrentHashMap、CopyOnWriteArrayList 等)来存储和操作共享数据,以确保线程安全。
Java 线程同步的几种方法?
- synchronized 关键字:使用 synchronized 关键字可以实现对代码块或方法的同步。当线程进入 synchronized 代码块或方法时,会自动获取对象的锁,并在执行完成后释放锁,确保同一时间只有一个线程可以执行该代码块或方法。
- Lock 类:ReentrantLock 是 java.util.concurrent.locks 包中提供的锁实现类,它提供了显式锁定和解锁的功能。使用 ReentrantLock 可以在代码中显式地指定锁定和解锁的位置,从而实现对共享资源的同步控制。
- LockSupport
- 线程安全的类:AtomicInteger /ConcurrentHashMap。
- volatile 关键字:使用 volatile 关键字可以标记变量为“可见性变量”,确保不同线程对该变量的修改在内存中可见。虽然 volatile 关键字不能解决复合操作的原子性问题,但它在某些场景下可以用于简单的线程同步。
Thread.interrupt() 方法的工作原理是什么?
Thread.interrupt() 向目标线程发送中断信号(即将目标线程的中断状态设置为 true,仅此而已)。Thread.interrupt() 方法并不能直接终止目标线程的执行。它只是改变了目标线程的中断状态,需要目标线程自行检查中断状态并作出相应的响应(一个线程不应该由其他线程强行终止)。
具体工作原理如下:
如果目标线程处于阻塞状态(如调用了 Object.wait()、Thread.sleep()、BlockingQueue.take() 等方法),它将立即抛出 InterruptedException 异常(并且唤醒目标线程),并且中断状态会被清除(重置为 false)。
如果目标线程在执行过程中检查了中断状态(通过 Thread.interrupted() 或 Thread.isInterrupted() 方法),则中断状态为 true。线程可以根据中断状态采取适当的操作,例如终止线程的执行或进行清理工作。
需要注意的是,Thread.interrupt() 方法仅仅是改变了目标线程的中断状态,具体的中断响应逻辑由目标线程自行决定。通常情况下,目标线程在合适的时机检查中断状态并采取相应的处理措施,例如退出执行循环或释放资源。
总结而言,Thread.interrupt() 方法通过改变目标线程的中断状态来请求目标线程中断,但具体的中断响应逻辑由目标线程自行处理。
Thread thread = new Thread(() -> {
System.out.println("线程被启动了");
while (!Thread.interrupted()) {
System.out.println(System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (Thread.interrupted()) {
System.out.println("线程被中断了");
break;
}
}
});
thread.start();
// 确保thread已经进入等待状态
Thread.sleep(500);
thread.interrupt();
线程被启动了
1684204424982
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at Xxx.lambda$main$0(Xxx.java:10)
at java.lang.Thread.run(Thread.java:748)
1684204425483
1684204426483
1684204427484
...
谈谈对 ThreadLocal 的理解?
Thread中有个成员变量threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap中有一个数组
private Entry[] table;
Entry继承自WeakReference<ThreadLocal<?>>,key为ThreadLocal,value为Object(也就是我们存放的值)
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal 是 Java 中的一个线程封闭机制,它允许将某个对象与当前线程关联起来,使得每个线程都拥有自己独立的对象副本。简单来说,ThreadLocal 提供了一种在多线程环境下,每个线程都可以独立访问自己的对象副本的机制。
ThreadLocal 的主要特点和应用如下:
-
线程隔离:ThreadLocal 可以实现线程间数据的隔离,每个线程都可以独立地操作自己的 ThreadLocal 实例,而不会受其他线程的影响。这对于一些需要线程独享的数据非常有用,如线程安全的 SimpleDateFormat、数据库连接等。
-
线程上下文传递:ThreadLocal 可以用于在方法之间传递数据,而不需要显式地传递参数。某个方法可以将数据存储在 ThreadLocal 中,然后其他方法可以直接从 ThreadLocal 中获取这些数据,避免了方法之间参数传递的麻烦。
-
线程状态保存:ThreadLocal 可以用于保存线程的状态信息。在多个方法调用之间,线程可以将状态信息存储在 ThreadLocal 中,以便后续方法可以方便地访问和使用。
在哪些场景下会使用到 ThreadLocal?
ThreadLocal 在以下场景下常被使用:
-
线程安全的对象:当一个对象是线程安全的,即可以被多个线程同时访问,但每个线程需要独立拥有自己的对象副本时,可以使用 ThreadLocal。典型的例子是线程安全的日期格式化器 SimpleDateFormat,每个线程可以拥有自己的日期格式化器副本,避免了多线程访问共享的 SimpleDateFormat 对象时可能出现的线程安全问题。
-
保存线程上下文信息:在多个方法调用之间需要传递一些上下文信息,但不希望在每个方法中显式传递参数时,可以使用 ThreadLocal。例如,在 Web 应用程序中,可以将当前用户信息存储在 ThreadLocal 中,这样在不同的方法中可以方便地获取用户信息,而不需要显式地传递用户参数。
-
数据库连接管理:在多线程环境中,为每个线程管理一个独立的数据库连接是常见的需求。通过使用 ThreadLocal,可以为每个线程维护一个独立的数据库连接副本,从而避免了线程间的数据库连接竞争和冲突。
-
事务管理:在一些需要实现线程级别的事务管理的场景下,可以使用 ThreadLocal 来存储当前线程的事务上下文。这样,每个线程都可以独立地管理自己的事务状态,而不会影响其他线程的事务。
-
日志跟踪:在日志系统中,通过 ThreadLocal 可以轻松地将每条日志的相关信息(如请求 ID、用户 ID)与当前线程关联起来,使得日志记录更加准确和可追踪。
总的来说,ThreadLocal 在需要实现线程隔离、线程上下文传递或线程状态保存的场景下非常有用。它提供了一种方便的机制,使得每个线程都可以独立地操作自己的对象副本,而不会受其他线程的干扰。
说一说对于 synchronized 关键字的了解?
synchronized
是 Java 中的关键字,用于实现线程的同步和互斥。它可以用于修饰方法、代码块和静态方法,用于控制对共享资源的访问。
synchronized
关键字的特性包括:
- 互斥性:
synchronized
保证了同一时间只有一个线程可以获得锁并执行同步代码块或方法,防止多线程并发访问共享资源导致的数据不一致性和冲突问题。 - 可见性:
synchronized
释放锁时会将修改的共享变量的值刷新到主内存,使得其他线程可以看到最新的值,保证了多线程间的数据一致性。 - 内置锁:
synchronized
使用的是内置锁(也称为监视器锁或互斥锁),每个对象都有一个与之关联的内置锁。当线程进入同步代码块时,它会尝试获取对象的内置锁,如果锁被其他线程持有,则线程进入阻塞状态,直到锁可用。
如何在项目中使用 synchronized 的?
同步代码块
同步方法
静态同步方法
说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?
-
锁升级
锁升级是指在多线程并发访问中,锁的状态从低级别的锁形式逐渐升级到高级别的锁形式,以提高并发性能和减少开销。下面介绍Java中的锁升级过程:
- 无锁状态(01):初始状态下,对象没有被线程持有锁,任何线程都可以访问。
- 偏向锁(101):当一个线程获得锁时,JVM会将锁标记置为偏向锁,并将线程ID记录在锁对象的对象头中。此时,其他线程可以继续访问该对象,无需进行同步操作(修改锁对象对象头中的线程id)。只有当线程竞争锁时(一个线程在同步代码外要进去,一个线程在同步代码里面),偏向锁会被撤销,锁状态升级为轻量级锁。
- 轻量级锁(00):当偏向锁撤销后,JVM将锁状态升级为轻量级锁。轻量级锁使用CAS(Compare and Swap)与自旋实现,当自旋一定次数后获取不到锁,锁状态将进一步升级。
- 重量级锁(10):当轻量级锁的CAS操作失败,JVM将锁状态升级为重量级锁。重量级锁使用操作系统提供的互斥量来实现线程之间的同步,线程在获取锁时会进入阻塞状态。重量级锁保证了多个线程之间的互斥访问,但会增加线程上下文切换的开销。
-
锁粗化
将多个连续的细粒度锁操作合并成一个更大的粗粒度锁操作,从而减少锁的获取和释放频率,提高性能.
优化前代码
Object lock = new Object();
synchronized (lock){
// 操作1
}
synchronized (lock){
// 操作2
}
synchronized (lock){
// 操作3
}
优化后代码
Object lock = new Object();
synchronized (lock){
// 操作1
// 操作2
// 操作3
}
-
锁消除
在编译器级别消除不必要的锁操作,从而提高程序性能。
优化前
public class LockEliminationExample {
public void performOperation() {
StringBuilder sb = new StringBuilder();
// 不会被多线程共享的局部变量
String localVariable = "Local";
// 锁对象只在当前方法内使用
synchronized (sb) {
// 执行需要同步的操作
sb.append("Hello");
sb.append("World");
sb.append(localVariable);
}
System.out.println(sb.toString());
}
}
优化后
public class LockEliminationExample {
public void performOperation() {
StringBuilder sb = new StringBuilder();
// 不会被多线程共享的局部变量
String localVariable = "Local";
// 执行需要同步的操作,锁消除
sb.append("Hello");
sb.append("World");
sb.append(localVariable);
System.out.println(sb.toString());
}
}
谈谈 synchronized 和 ReenTrantLock 的区别?
synchronized和ReentrantLock都是Java中用于实现线程同步的机制,它们有以下区别:
-
锁的获取方式:synchronized是Java语言层面的关键字,它在获取锁时会隐式地获取和释放锁。而ReentrantLock是Java提供的一个类,需要显式地调用
lock()
方法获取锁,以及调用unlock()
方法释放锁。 -
可中断性:ReentrantLock提供了一种可中断的获取锁的方式。在获取锁时,线程可以选择等待一段时间,并且可以通过调用
lockInterruptibly()
方法实现在等待期间响应中断。而synchronized关键字的锁获取过程是不可中断的,一旦获取不到锁,线程将一直处于阻塞状态,直到获取到锁或者抛出异常。 -
公平性:ReentrantLock可以是公平锁或非公平锁,而synchronized关键字默认情况下是非公平锁。公平锁会按照线程请求的顺序来获取锁,而非公平锁则允许插队获取锁。在高并发情况下,公平锁可能会导致线程饥饿,但可以避免线程的不公平竞争。
-
锁的可重入性:ReentrantLock和synchronized都支持锁的可重入性(也称为递归锁)。同一个线程可以多次获取同一个锁而不会发生死锁。在ReentrantLock中,线程必须显式地调用
unlock()
方法相同次数的释放锁。而synchronized关键字则会自动进行锁的释放。 -
扩展功能:ReentrantLock相对于synchronized提供了一些额外的功能,如可定时的、可轮询的、可中断的锁获取方式,以及可选择公平性。此外,ReentrantLock还支持多个条件变量(Condition),可以通过多个条件变量来控制线程的等待和唤醒。
需要注意的是,synchronized是Java语言内置的关键字,使用简单方便,适用于大多数的同步场景。而ReentrantLock是一个类,提供了更多灵活性和扩展功能,适用于需要更精细控制的同步场景。在选择使用synchronized还是ReentrantLock时,可以根据具体需求和场景来进行选择。
synchronized 和 volatile 的区别是什么?
synchronized和volatile是Java中用于实现线程安全的关键字,它们有以下区别:
- 作用范围:synchronized修饰方法或代码块。volatile用于修饰变量。
- 同步性质:synchronized保证可见性、原子性(这里的原子性指的是.同一时刻只能有一个线程操作,中间不能被其他线程操作,没有要么同时成功要么同时失败的语义)和一定程度的禁重排(synchronized内部的和外部的不能重排,但是内部是可以重排的,证据:双重检查为什么要加volatile修饰)。volatile只保证可见性和禁止指令重排(内存屏障),不保证原子性(多线程对volatile修饰的变量的i++操作).
- synchronized进入后会从主内存读,出来后会写会主内存,保证了可见性.volatile写立马刷会主内存,并让其他线程中对应的volatile修饰的变量失效,以此来保证内存的可见性.
- 应用场景:synchronized适用于多线程对共享资源进行并发访问和修改的情况,它提供了互斥访问的能力。volatile确保多个线程之间对变量的修改可见(没有复合操作可以用volatile),但不提供互斥访问的能力。
需要注意的是,synchronized提供了更强的线程安全性和数据一致性,但在使用时需要考虑锁的开销和可能的死锁情况。volatile关键字的使用更轻量,适用于简单的变量读写操作,但无法解决复合操作的原子性问题。在选择使用synchronized还是volatile时,需要根据具体的场景和需求来进行选择。
谈一下你对 volatile 关键字的理解?
当多个线程访问共享变量时,为了保证可见性和避免指令重排序带来的问题,可以使用volatile关键字来修饰变量。
volatile关键字具有以下特性:
-
可见性(Visibility):对于被volatile修饰的变量,在一个线程中对其进行修改后,其他线程能够立即看到最新的值。这是因为volatile关键字会告知编译器和处理器不要对该变量进行缓存,每次读取时都要从主内存中获取最新值。
-
禁止指令重排序(Ordering):volatile关键字会防止编译器和处理器对volatile变量的指令进行重排序。这意味着volatile变量的读写操作都会按照代码的顺序执行,不会被重新排序。
需要注意的是,volatile关键字仅适用于单个变量的读写操作,并不能保证复合操作的原子性。例如,volatile关键字无法保证i++这种操作的原子性,因为该操作包括读取、自增和写回三个步骤,而volatile关键字只能保证单个变量的读写操作的原子性。
另外,volatile关键字的使用相对于synchronized来说更轻量,因为它不需要获取锁来实现线程间的同步,但在某些情况下可能需要额外的控制机制来保证一致性。
总结起来,volatile关键字主要用于保证共享变量的可见性,确保对变量的修改能够及时被其他线程看到,并且禁止编译器和处理器对该变量进行重排序。它适用于对变量的简单读写操作,但不能解决复合操作的原子性问题。
说下对 ReentrantReadWriteLock 的理解?
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SharedResource {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private int value;
public int getValue() {
lock.readLock().lock();
try {
return value;
} finally {
lock.readLock().unlock();
}
}
public void setValue(int newValue) {
lock.writeLock().lock();
try {
value = newValue;
} finally {
lock.writeLock().unlock();
}
}
}
说下对悲观锁和乐观锁的理解?
-
悲观锁:假设容易发生冲突,因此会加锁操作,确保只有一个线程能够访问资源,其他线程需要等待。悲观锁适用于对数据修改频繁的场景。
-
乐观锁:假设不容易发生冲突,因此不需要加锁,而是在更新数据之前先进行验证,如果没有发现冲突,就进行更新,否则进行相应的冲突处理,例如重试或放弃更新。适用于读多写少的场景
乐观锁常见的两种实现方式是什么
乐观锁常见的两种实现方式是版本号(Versioning)和比较并交换(Compare and Swap,CAS)。
- 版本号(Versioning):
版本号是乐观锁的一种实现方式。在这种方式中,每个数据项都会有一个与之关联的版本号(或时间戳)。当一个线程要更新数据时,它首先读取数据的当前版本号,并在更新之前将版本号加一。然后,它尝试将更新后的数据写回,并比较写回操作前后的版本号是否一致。如果一致,则表示没有其他线程修改了数据,更新成功;如果不一致,则表示发生了冲突,需要进行相应的冲突处理。
版本号的实现可以是一个整数字段,例如使用 Java 中的 AtomicInteger
,或者是一个时间戳字段,记录数据的修改时间。这样,每次更新操作都会更新版本号,从而标识数据的变化。
- 比较并交换(Compare and Swap,CAS):
比较并交换是另一种常见的乐观锁实现方式,它是一种原子操作。CAS 操作包括三个操作数:内存地址(或引用)、预期值和新值。它会比较内存地址处的值与预期值是否相等,如果相等,则将新值写入内存地址;否则,表示数据已被其他线程修改,CAS 操作失败。
CAS 操作可以通过硬件的原子指令来实现,是一种非阻塞的操作。在多线程环境下,多个线程可以同时尝试执行 CAS 操作来更新数据,但只有一个线程的 CAS 操作会成功,其他线程需要重试或进行相应的处理。
Java 提供了 java.util.concurrent.atomic
包下的原子类,例如 AtomicInteger
、AtomicLong
等,它们使用 CAS 操作来实现线程安全的原子操作,可以作为乐观锁的一种实现方式。
这两种乐观锁的实现方式都是基于无锁的思想,避免了线程阻塞和上下文切换的开销,适用于读多写少的场景。选择哪种方式取决于具体的应用需求和环境。
乐观锁的缺点有哪些?
乐观锁虽然在某些场景下可以提供高性能和并发量,但也存在一些缺点:
-
冲突处理:乐观锁需要处理可能发生的冲突。如果在验证阶段发现数据已经被其他线程修改,当前线程必须进行相应的冲突处理,例如重试操作或放弃更新。这种处理过程可能会引入额外的开销,特别是在高并发环境下,如果冲突频繁发生,重试的开销可能会增加系统的负载。
-
无法保证一致性:乐观锁无法保证数据的强一致性。由于它在读取和更新之间不加锁,其他线程可能会修改数据,导致最终数据的不一致。乐观锁适用于那些可以容忍一定程度的数据不一致性的场景,例如并发读取的数据不要求绝对实时的一致性。
-
自旋开销:在乐观锁的实现中,当发生冲突时,通常会采用自旋的方式进行重试,即反复尝试更新数据,直到成功或达到一定的重试次数。自旋过程会占用 CPU 时间,可能导致额外的开销。如果冲突频繁发生,自旋次数过多会浪费大量的 CPU 资源。
-
难以应对长事务:乐观锁对于长事务的处理相对困难。在长时间的事务中,其他并发事务可能修改了事务操作的数据,导致更新失败。为了避免这种情况,需要进行额外的控制和冲突处理,增加了实现的复杂性。
综上所述,乐观锁的主要缺点是需要处理冲突、无法保证一致性、自旋开销和难以应对长事务。在选择使用乐观锁时,需要综合考虑应用场景、并发访问模式和数据一致性要求,权衡其优点和缺点。
CAS 和 synchronized 的使用场景?
cas适用于冲突少的场景
synchronized 适用于冲突多的场景
简单说下对 Java 中的原子类的理解?
Java中的原子类是一组线程安全的类,它们提供了原子操作的功能,可以在多线程环境下进行线程安全的操作。这些原子类位于java.util.concurrent.atomic
包中,常见的原子类包括AtomicInteger
、AtomicLong
、AtomicBoolean
等。
原子类的主要特点如下:
- 原子操作:原子类提供了一系列的原子操作方法,这些方法在执行过程中不会被其他线程中断,保证了操作的原子性。
- 无锁的实现:原子类使用了底层的硬件支持或CAS(Compare and Swap)操作来实现无锁的线程安全操作。
- 原子类的操作是基于变量的:原子类主要针对基本数据类型(如整型、长整型、布尔型)进行原子操作。对于复杂的数据结构,需要使用原子类的组合操作或其他线程安全的机制来实现。
- 线程安全:原子类的操作都是线程安全的,多个线程可以并发地访问和修改原子变量,而不需要额外的同步控制。原子类使用了底层的原子操作指令或CAS操作来保证线程安全。
原子类在多线程编程中非常有用,特别适用于高并发环境下对共享变量进行原子操作的场景。它们提供了一种高效、简洁和线程安全的方式来操作共享变量,避免了使用传统的锁机制所带来的线程阻塞和上下文切换的开销。通过使用原子类,可以编写出更高效、可靠和可维护的多线程代码。
atomic 的原理是什么?
说下对同步器 AQS 的理解?
AQS 的原理是什么?
AQS 对资源的共享模式有哪些?
AQS 底层使用了模板方法模式,你能说出几个需要重写的方法吗?
java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquire
说下对信号量 Semaphore 的理解?
CountDownLatch 和 CyclicBarrier 有什么区别?
说下对线程池的理解?为什么要使用线程池?
创建线程池的参数有哪些?
线程池的常见拒绝策略
AbortPolicy
拒绝执行,并抛出异常RejectedExecutionException
.为默认拒绝策略CallerRunsPolicy
让调用者自己执行DiscardPolicy
丢弃DiscardOldestPolicy
丢弃最早的
如何创建线程池?
// 七大参数
int corePoolSize = 3;
int maximumPoolSize = 9;
long keepAliveTime = 30;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable# workQueue = new LinkedBlockingQueue<>(9);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
线程池中的的线程数一般怎么设置?需要考虑哪些问题?
-
经验值
cup密集型N+1
IO密集型2N
-
计算公式
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
很显然线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
虽说最佳线程数目算法更准确,但是线程等待时间和线程CPU时间不好测量,实际情况使用得比较少,一般用经验值就差不多了。再配合系统压测,基本可以确定最适合的线程数。
执行 execute() 方法和 submit() 方法的区别是什么呢?
-
返回值:execute()没有返回值,submit() 有返回值
-
异常处理:
execute()
方法没有提供任何异常处理机制。如果提交的任务在执行过程中抛出异常,它将由内部的UncaughtExceptionHandler
处理。而submit()
在获取结果的时候可以捕获到异常,然后自己处理。public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(1); Runnable runnable = () -# { int i = 10 / 0; }; pool.execute(runnable); pool.shutdown(); }
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero at Xxx.lambda$main$0(Xxx.java:14) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(1); Runnable runnable = () -# { int i = 10 / 0; }; Future<?# submit = pool.submit(runnable); try { submit.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } pool.shutdown(); }
public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(1); Runnable runnable = () -# { int i = 10 / 0; }; Future<?# submit = pool.submit(runnable); try { submit.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } pool.shutdown(); }
java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero at java.util.concurrent.FutureTask.report(FutureTask.java:122) at java.util.concurrent.FutureTask.get(FutureTask.java:192) at Xxx.main(Xxx.java:14) Caused by: java.lang.ArithmeticException: / by zero at Xxx.lambda$main$0(Xxx.java:10) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
-
任务的包装:
execute()
方法接受一个Runnable
接口或其子类的任务对象作为参数。而submit()
方法既可以接受Runnable
接口的任务对象,也可以接受Callable
接口的任务对象。java.util.concurrent.Executor#execute
void execute(Runnable command);
java.util.concurrent.ExecutorService#submit
<T> Future<T> submit(Callable<T> task); Future<?> submit(Runnable task);
-
Future 对象:
submit()
方法返回一个Future
对象,可以通过该对象来管理和获取任务的执行状态和结果。
execute()
方法用于提交不需要返回结果的任务,而 submit()
方法用于提交需要返回结果的任务,并提供更多的控制和异常处理机制。如果你关心任务的返回结果或需要更精细的异常处理,那么使用 submit()
方法会更加灵活和方便。如果你只需要提交简单的任务而不关心结果,execute()
方法也是一个简单的选择。
说下对 Fork 和 Join 并行计算框架的理解?
JDK 中提供了哪些并发容器?
- ConcurrentHashMap:这是一个线程安全的哈希表,它支持高并发的读和一定程度上的并发写。它通过将数据分割为多个段(Segment)来实现并发操作,并使用锁粒度更小的方式来提高并发性能。
- CopyOnWriteArrayList:这是一个线程安全的动态数组,它通过在写操作时创建一个底层数组的副本来实现安全性。这意味着写操作不会影响正在进行的读操作,从而提供了较好的读多写少场景的性能。
- ConcurrentLinkedQueue:这是一个线程安全的无界非阻塞队列,基于链表实现。它提供高效的并发插入和删除操作,并且不需要显式的同步。适用于多生产者多消费者的场景。
- ConcurrentSkipListMap和ConcurrentSkipListSet:这是基于跳表(Skip List)数据结构实现的并发容器,提供了有序的键值映射和有序的集合操作。它们支持高并发读写操作,并且具有较好的查找性能。
- BlockingQueue接口的实现:JDK提供了多个BlockingQueue接口的实现,如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等。这些队列可以用于线程间的数据交换,支持阻塞的插入和删除操作。
- ConcurrentLinkedDeque:这是一个线程安全的双端队列,基于链表实现。它提供高效的并发插入和删除操作,可以在队列的两端进行操作。
谈谈对 CopyOnWriteArrayList 的理解?
谈谈对 BlockingQueue 的理解?分别有哪些实现类?
谈谈对 ConcurrentSkipListMap 的理解?
Java高频面试之总纲篇
Java高频面试之集合篇
Java高频面试之异常篇
Java高频面试之并发篇
Java高频面试之SSM篇
Java高频面试之Mysql篇
Java高频面试之Redis篇
Java高频面试之消息队列与分布式篇
50道SQL面试题
奇奇怪怪的面试题
五花八门的内存溢出