Java高频面试之并发篇

有需要互关的小伙伴,关注一下,有关必回关,争取今年认证早日拿到博客专家

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(); // 启动线程

创建线程的几种方式?

  1. 继承Thread类重写run方法(无参构造方法)
class MyThread extends Thread {
    public void run() {
        // 线程的执行逻辑
    }
}

// 创建并启动线程
MyThread myThread = new MyThread();
myThread.start();

  1. 创建Thread,传入一个Runnable对象(有参构造)

    class MyRunnable implements Runnable {
        public void run() {
            // 线程的执行逻辑
        }
    }
    
    // 创建并启动线程
    MyRunnable myRunnable = new MyRunnable();
    Thread thread = new Thread(myRunnable);
    thread.start();
    

创建Runnable的方法

  1. 实现Runnable接口,重写run方法,并将其作为参数传递给Thread的构造方法
class MyRunnable implements Runnable {
    public void run() {
        // 线程的执行逻辑
    }
}
  1. 使用匿名内部类
Thread thread = new Thread(new Runnable() {
public void run() {
// 线程的执行逻辑
}
});
thread.start();
  1. 使用lambda表达式
Thread thread = new Thread(() -# {
    // 线程的执行逻辑
});
thread.start();

通常情况下,推荐使用实现Runnable接口或使用Lambda表达式的方式,因为它们能更好地支持代码的组织和重用,同时避免了单继承的限制

Runnable 和 Callable 有什么区别?

Runnable和Callable接口都用于创建可并发执行的任务,但它们有以下区别:

  1. 返回值:Runnable接口的run()方法没有返回值,它表示一个没有返回结果的任务。而Callable接口的call()方法可以返回任务的执行结果,它是一个带有泛型参数的接口,用于定义具有返回值的任务。

  2. 异常处理: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;
    }
    
  3. 使用方式:Runnable接口通常作为线程的执行体,将任务逻辑放在run()方法中实现。Callable接口通常与ExecutorService一起使用,通过submit()方法提交Callable任务给线程池执行,并通过返回的Future对象获取任务的执行结果。

  4. 返回结果的获取: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种状态

image.png

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;
}

image.png

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() 的区别?

  1. sleep() 用于暂停当前线程一段时间,不涉及锁或线程之间的协作(无任何同步语义,让出cpu,但不释放锁)。

    wait() 用于线程之间的协作,调用该方法后,线程会释放对象锁并进入等待状态,直到其他线程唤醒它(让出cpu,释放锁)。

  2. sleep() 是线程的静态方法,可以直接调用;而 wait() 是对象的方法,必须在同步代码块或同步方法中使用,并且需要配合 notify() 或 notifyAll() 方法使用。

线程的 run() 和 start() 有什么区别?

run() 方法只是在当前线程中同步执行线程代码(run方法的逻辑),而 start() 方法会创建一个新线程,并在新线程中异步执行线程代码。通常情况下,我们使用 start() 方法来启动线程,以便实现多线程并发执行的效果。

简单说就是一个在当前线程中执行run(),一个是新建一个线程执行run()

在 Java 程序中怎么保证多线程的运行安全?

  1. 同步代码块(Synchronized Blocks)
synchronized (sharedObject) {
    // 需要同步的代码块
}
  1. 同步方法(Synchronized Methods)
public synchronized void myMethod() {
    // 需要同步的代码
}
  1. 锁(Lock)
Lock lock = new ReentrantLock();

lock.lock(); // 获取锁
try {
    // 需要同步的代码
} finally {
    lock.unlock(); // 释放锁
}
  1. 原子类(Atomic Classes
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet(); // 原子递增操作
  1. 使用线程安全的数据结构:在多线程环境下,使用线程安全的数据结构(如 ConcurrentHashMap、CopyOnWriteArrayList 等)来存储和操作共享数据,以确保线程安全。

Java 线程同步的几种方法?

  1. synchronized 关键字:使用 synchronized 关键字可以实现对代码块或方法的同步。当线程进入 synchronized 代码块或方法时,会自动获取对象的锁,并在执行完成后释放锁,确保同一时间只有一个线程可以执行该代码块或方法。
  2. Lock 类:ReentrantLock 是 java.util.concurrent.locks 包中提供的锁实现类,它提供了显式锁定和解锁的功能。使用 ReentrantLock 可以在代码中显式地指定锁定和解锁的位置,从而实现对共享资源的同步控制。
  3. LockSupport
  4. 线程安全的类:AtomicInteger /ConcurrentHashMap。
  5. 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 的主要特点和应用如下:

  1. 线程隔离:ThreadLocal 可以实现线程间数据的隔离,每个线程都可以独立地操作自己的 ThreadLocal 实例,而不会受其他线程的影响。这对于一些需要线程独享的数据非常有用,如线程安全的 SimpleDateFormat、数据库连接等。

  2. 线程上下文传递:ThreadLocal 可以用于在方法之间传递数据,而不需要显式地传递参数。某个方法可以将数据存储在 ThreadLocal 中,然后其他方法可以直接从 ThreadLocal 中获取这些数据,避免了方法之间参数传递的麻烦。

  3. 线程状态保存:ThreadLocal 可以用于保存线程的状态信息。在多个方法调用之间,线程可以将状态信息存储在 ThreadLocal 中,以便后续方法可以方便地访问和使用。

在哪些场景下会使用到 ThreadLocal?

ThreadLocal 在以下场景下常被使用:

  1. 线程安全的对象:当一个对象是线程安全的,即可以被多个线程同时访问,但每个线程需要独立拥有自己的对象副本时,可以使用 ThreadLocal。典型的例子是线程安全的日期格式化器 SimpleDateFormat,每个线程可以拥有自己的日期格式化器副本,避免了多线程访问共享的 SimpleDateFormat 对象时可能出现的线程安全问题。

  2. 保存线程上下文信息:在多个方法调用之间需要传递一些上下文信息,但不希望在每个方法中显式传递参数时,可以使用 ThreadLocal。例如,在 Web 应用程序中,可以将当前用户信息存储在 ThreadLocal 中,这样在不同的方法中可以方便地获取用户信息,而不需要显式地传递用户参数。

  3. 数据库连接管理:在多线程环境中,为每个线程管理一个独立的数据库连接是常见的需求。通过使用 ThreadLocal,可以为每个线程维护一个独立的数据库连接副本,从而避免了线程间的数据库连接竞争和冲突。

  4. 事务管理:在一些需要实现线程级别的事务管理的场景下,可以使用 ThreadLocal 来存储当前线程的事务上下文。这样,每个线程都可以独立地管理自己的事务状态,而不会影响其他线程的事务。

  5. 日志跟踪:在日志系统中,通过 ThreadLocal 可以轻松地将每条日志的相关信息(如请求 ID、用户 ID)与当前线程关联起来,使得日志记录更加准确和可追踪。

总的来说,ThreadLocal 在需要实现线程隔离、线程上下文传递或线程状态保存的场景下非常有用。它提供了一种方便的机制,使得每个线程都可以独立地操作自己的对象副本,而不会受其他线程的干扰。

说一说对于 synchronized 关键字的了解?

synchronized 是 Java 中的关键字,用于实现线程的同步和互斥。它可以用于修饰方法、代码块和静态方法,用于控制对共享资源的访问。

synchronized 关键字的特性包括:

  • 互斥性:synchronized 保证了同一时间只有一个线程可以获得锁并执行同步代码块或方法,防止多线程并发访问共享资源导致的数据不一致性和冲突问题。
  • 可见性:synchronized 释放锁时会将修改的共享变量的值刷新到主内存,使得其他线程可以看到最新的值,保证了多线程间的数据一致性。
  • 内置锁:synchronized 使用的是内置锁(也称为监视器锁或互斥锁),每个对象都有一个与之关联的内置锁。当线程进入同步代码块时,它会尝试获取对象的内置锁,如果锁被其他线程持有,则线程进入阻塞状态,直到锁可用。

如何在项目中使用 synchronized 的?

同步代码块

同步方法

静态同步方法

说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?

  1. 锁升级

    锁升级是指在多线程并发访问中,锁的状态从低级别的锁形式逐渐升级到高级别的锁形式,以提高并发性能和减少开销。下面介绍Java中的锁升级过程:

    1. 无锁状态(01):初始状态下,对象没有被线程持有锁,任何线程都可以访问。
    2. 偏向锁(101):当一个线程获得锁时,JVM会将锁标记置为偏向锁,并将线程ID记录在锁对象的对象头中。此时,其他线程可以继续访问该对象,无需进行同步操作(修改锁对象对象头中的线程id)。只有当线程竞争锁时(一个线程在同步代码外要进去,一个线程在同步代码里面),偏向锁会被撤销,锁状态升级为轻量级锁。
    3. 轻量级锁(00):当偏向锁撤销后,JVM将锁状态升级为轻量级锁。轻量级锁使用CAS(Compare and Swap)与自旋实现,当自旋一定次数后获取不到锁,锁状态将进一步升级。
    4. 重量级锁(10):当轻量级锁的CAS操作失败,JVM将锁状态升级为重量级锁。重量级锁使用操作系统提供的互斥量来实现线程之间的同步,线程在获取锁时会进入阻塞状态。重量级锁保证了多个线程之间的互斥访问,但会增加线程上下文切换的开销。

    image.png

  2. 锁粗化

    将多个连续的细粒度锁操作合并成一个更大的粗粒度锁操作,从而减少锁的获取和释放频率,提高性能.

    优化前代码

Object lock = new Object();
synchronized (lock){
    // 操作1
}
synchronized (lock){
    // 操作2
}
synchronized (lock){
    // 操作3
}

优化后代码

Object lock = new Object();
synchronized (lock){
    // 操作1
    // 操作2
    // 操作3
}
  1. 锁消除

    编译器级别消除不必要的锁操作,从而提高程序性能。

    优化前

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中用于实现线程同步的机制,它们有以下区别:

  1. 锁的获取方式:synchronized是Java语言层面的关键字,它在获取锁时会隐式地获取和释放锁。而ReentrantLock是Java提供的一个类,需要显式地调用lock()方法获取锁,以及调用unlock()方法释放锁。

  2. 可中断性:ReentrantLock提供了一种可中断的获取锁的方式。在获取锁时,线程可以选择等待一段时间,并且可以通过调用lockInterruptibly()方法实现在等待期间响应中断。而synchronized关键字的锁获取过程是不可中断的,一旦获取不到锁,线程将一直处于阻塞状态,直到获取到锁或者抛出异常。

  3. 公平性:ReentrantLock可以是公平锁或非公平锁,而synchronized关键字默认情况下是非公平锁。公平锁会按照线程请求的顺序来获取锁,而非公平锁则允许插队获取锁。在高并发情况下,公平锁可能会导致线程饥饿,但可以避免线程的不公平竞争。

  4. 锁的可重入性:ReentrantLock和synchronized都支持锁的可重入性(也称为递归锁)。同一个线程可以多次获取同一个锁而不会发生死锁。在ReentrantLock中,线程必须显式地调用unlock()方法相同次数的释放锁。而synchronized关键字则会自动进行锁的释放。

  5. 扩展功能:ReentrantLock相对于synchronized提供了一些额外的功能,如可定时的、可轮询的、可中断的锁获取方式,以及可选择公平性。此外,ReentrantLock还支持多个条件变量(Condition),可以通过多个条件变量来控制线程的等待和唤醒。

需要注意的是,synchronized是Java语言内置的关键字,使用简单方便,适用于大多数的同步场景。而ReentrantLock是一个类,提供了更多灵活性和扩展功能,适用于需要更精细控制的同步场景。在选择使用synchronized还是ReentrantLock时,可以根据具体需求和场景来进行选择。

synchronized 和 volatile 的区别是什么?

synchronized和volatile是Java中用于实现线程安全的关键字,它们有以下区别:

  1. 作用范围:synchronized修饰方法或代码块。volatile用于修饰变量。
  2. 同步性质:synchronized保证可见性、原子性(这里的原子性指的是.同一时刻只能有一个线程操作,中间不能被其他线程操作,没有要么同时成功要么同时失败的语义)和一定程度的禁重排(synchronized内部的和外部的不能重排,但是内部是可以重排的,证据:双重检查为什么要加volatile修饰)。volatile只保证可见性和禁止指令重排(内存屏障),不保证原子性(多线程对volatile修饰的变量的i++操作).
  3. synchronized进入后会从主内存读,出来后会写会主内存,保证了可见性.volatile写立马刷会主内存,并让其他线程中对应的volatile修饰的变量失效,以此来保证内存的可见性.
  4. 应用场景:synchronized适用于多线程对共享资源进行并发访问和修改的情况,它提供了互斥访问的能力。volatile确保多个线程之间对变量的修改可见(没有复合操作可以用volatile),但不提供互斥访问的能力。

需要注意的是,synchronized提供了更强的线程安全性和数据一致性,但在使用时需要考虑锁的开销和可能的死锁情况。volatile关键字的使用更轻量,适用于简单的变量读写操作,但无法解决复合操作的原子性问题。在选择使用synchronized还是volatile时,需要根据具体的场景和需求来进行选择。

谈一下你对 volatile 关键字的理解?

当多个线程访问共享变量时,为了保证可见性和避免指令重排序带来的问题,可以使用volatile关键字来修饰变量。

volatile关键字具有以下特性:

  1. 可见性(Visibility):对于被volatile修饰的变量,在一个线程中对其进行修改后,其他线程能够立即看到最新的值。这是因为volatile关键字会告知编译器和处理器不要对该变量进行缓存,每次读取时都要从主内存中获取最新值。

  2. 禁止指令重排序(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();
        }
    }
}

说下对悲观锁和乐观锁的理解?

  1. 悲观锁:假设容易发生冲突,因此会加锁操作,确保只有一个线程能够访问资源,其他线程需要等待。悲观锁适用于对数据修改频繁的场景。

  2. 乐观锁:假设不容易发生冲突,因此不需要加锁,而是在更新数据之前先进行验证,如果没有发现冲突,就进行更新,否则进行相应的冲突处理,例如重试或放弃更新。适用于读多写少的场景

乐观锁常见的两种实现方式是什么

乐观锁常见的两种实现方式是版本号(Versioning)和比较并交换(Compare and Swap,CAS)。

  1. 版本号(Versioning):
    版本号是乐观锁的一种实现方式。在这种方式中,每个数据项都会有一个与之关联的版本号(或时间戳)。当一个线程要更新数据时,它首先读取数据的当前版本号,并在更新之前将版本号加一。然后,它尝试将更新后的数据写回,并比较写回操作前后的版本号是否一致。如果一致,则表示没有其他线程修改了数据,更新成功;如果不一致,则表示发生了冲突,需要进行相应的冲突处理。

版本号的实现可以是一个整数字段,例如使用 Java 中的 AtomicInteger,或者是一个时间戳字段,记录数据的修改时间。这样,每次更新操作都会更新版本号,从而标识数据的变化。

  1. 比较并交换(Compare and Swap,CAS):
    比较并交换是另一种常见的乐观锁实现方式,它是一种原子操作。CAS 操作包括三个操作数:内存地址(或引用)、预期值和新值。它会比较内存地址处的值与预期值是否相等,如果相等,则将新值写入内存地址;否则,表示数据已被其他线程修改,CAS 操作失败。

CAS 操作可以通过硬件的原子指令来实现,是一种非阻塞的操作。在多线程环境下,多个线程可以同时尝试执行 CAS 操作来更新数据,但只有一个线程的 CAS 操作会成功,其他线程需要重试或进行相应的处理。

Java 提供了 java.util.concurrent.atomic 包下的原子类,例如 AtomicIntegerAtomicLong 等,它们使用 CAS 操作来实现线程安全的原子操作,可以作为乐观锁的一种实现方式。

这两种乐观锁的实现方式都是基于无锁的思想,避免了线程阻塞和上下文切换的开销,适用于读多写少的场景。选择哪种方式取决于具体的应用需求和环境。

乐观锁的缺点有哪些?

乐观锁虽然在某些场景下可以提供高性能和并发量,但也存在一些缺点:

  1. 冲突处理:乐观锁需要处理可能发生的冲突。如果在验证阶段发现数据已经被其他线程修改,当前线程必须进行相应的冲突处理,例如重试操作或放弃更新。这种处理过程可能会引入额外的开销,特别是在高并发环境下,如果冲突频繁发生,重试的开销可能会增加系统的负载。

  2. 无法保证一致性:乐观锁无法保证数据的强一致性。由于它在读取和更新之间不加锁,其他线程可能会修改数据,导致最终数据的不一致。乐观锁适用于那些可以容忍一定程度的数据不一致性的场景,例如并发读取的数据不要求绝对实时的一致性。

  3. 自旋开销:在乐观锁的实现中,当发生冲突时,通常会采用自旋的方式进行重试,即反复尝试更新数据,直到成功或达到一定的重试次数。自旋过程会占用 CPU 时间,可能导致额外的开销。如果冲突频繁发生,自旋次数过多会浪费大量的 CPU 资源。

  4. 难以应对长事务:乐观锁对于长事务的处理相对困难。在长时间的事务中,其他并发事务可能修改了事务操作的数据,导致更新失败。为了避免这种情况,需要进行额外的控制和冲突处理,增加了实现的复杂性。

综上所述,乐观锁的主要缺点是需要处理冲突、无法保证一致性、自旋开销和难以应对长事务。在选择使用乐观锁时,需要综合考虑应用场景、并发访问模式和数据一致性要求,权衡其优点和缺点。

CAS 和 synchronized 的使用场景?

cas适用于冲突少的场景

synchronized 适用于冲突多的场景

简单说下对 Java 中的原子类的理解?

Java中的原子类是一组线程安全的类,它们提供了原子操作的功能,可以在多线程环境下进行线程安全的操作。这些原子类位于java.util.concurrent.atomic包中,常见的原子类包括AtomicIntegerAtomicLongAtomicBoolean等。

原子类的主要特点如下:

  1. 原子操作:原子类提供了一系列的原子操作方法,这些方法在执行过程中不会被其他线程中断,保证了操作的原子性。
  2. 无锁的实现:原子类使用了底层的硬件支持或CAS(Compare and Swap)操作来实现无锁的线程安全操作。
  3. 原子类的操作是基于变量的:原子类主要针对基本数据类型(如整型、长整型、布尔型)进行原子操作。对于复杂的数据结构,需要使用原子类的组合操作或其他线程安全的机制来实现。
  4. 线程安全:原子类的操作都是线程安全的,多个线程可以并发地访问和修改原子变量,而不需要额外的同步控制。原子类使用了底层的原子操作指令或CAS操作来保证线程安全。

原子类在多线程编程中非常有用,特别适用于高并发环境下对共享变量进行原子操作的场景。它们提供了一种高效、简洁和线程安全的方式来操作共享变量,避免了使用传统的锁机制所带来的线程阻塞和上下文切换的开销。通过使用原子类,可以编写出更高效、可靠和可维护的多线程代码。

atomic 的原理是什么?

说下对同步器 AQS 的理解?

AQS 的原理是什么?

AQS 对资源的共享模式有哪些?

AQS 底层使用了模板方法模式,你能说出几个需要重写的方法吗?

java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquire

说下对信号量 Semaphore 的理解?

CountDownLatch 和 CyclicBarrier 有什么区别?

说下对线程池的理解?为什么要使用线程池?

创建线程池的参数有哪些?

线程池的常见拒绝策略

  1. AbortPolicy拒绝执行,并抛出异常RejectedExecutionException.为默认拒绝策略
  2. CallerRunsPolicy让调用者自己执行
  3. DiscardPolicy丢弃
  4. 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);

线程池中的的线程数一般怎么设置?需要考虑哪些问题?

  1. 经验值

    cup密集型N+1

    IO密集型2N

  2. 计算公式

    最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

    很显然线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

    虽说最佳线程数目算法更准确,但是线程等待时间和线程CPU时间不好测量,实际情况使用得比较少,一般用经验值就差不多了。再配合系统压测,基本可以确定最适合的线程数。

执行 execute() 方法和 submit() 方法的区别是什么呢?

  1. 返回值:execute()没有返回值,submit() 有返回值

  2. 异常处理: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)
    
  3. 任务的包装: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);
    
  4. Future 对象:submit() 方法返回一个 Future 对象,可以通过该对象来管理和获取任务的执行状态和结果。

execute() 方法用于提交不需要返回结果的任务,而 submit() 方法用于提交需要返回结果的任务,并提供更多的控制和异常处理机制。如果你关心任务的返回结果或需要更精细的异常处理,那么使用 submit() 方法会更加灵活和方便。如果你只需要提交简单的任务而不关心结果,execute() 方法也是一个简单的选择。

说下对 Fork 和 Join 并行计算框架的理解?

JDK 中提供了哪些并发容器?

  1. ConcurrentHashMap:这是一个线程安全的哈希表,它支持高并发的读和一定程度上的并发写。它通过将数据分割为多个段(Segment)来实现并发操作,并使用锁粒度更小的方式来提高并发性能。
  2. CopyOnWriteArrayList:这是一个线程安全的动态数组,它通过在写操作时创建一个底层数组的副本来实现安全性。这意味着写操作不会影响正在进行的读操作,从而提供了较好的读多写少场景的性能。
  3. ConcurrentLinkedQueue:这是一个线程安全的无界非阻塞队列,基于链表实现。它提供高效的并发插入和删除操作,并且不需要显式的同步。适用于多生产者多消费者的场景。
  4. ConcurrentSkipListMap和ConcurrentSkipListSet:这是基于跳表(Skip List)数据结构实现的并发容器,提供了有序的键值映射和有序的集合操作。它们支持高并发读写操作,并且具有较好的查找性能。
  5. BlockingQueue接口的实现:JDK提供了多个BlockingQueue接口的实现,如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等。这些队列可以用于线程间的数据交换,支持阻塞的插入和删除操作。
  6. ConcurrentLinkedDeque:这是一个线程安全的双端队列,基于链表实现。它提供高效的并发插入和删除操作,可以在队列的两端进行操作。

谈谈对 CopyOnWriteArrayList 的理解?

谈谈对 BlockingQueue 的理解?分别有哪些实现类?

谈谈对 ConcurrentSkipListMap 的理解?

Java高频面试之总纲篇

Java高频面试之集合篇

Java高频面试之异常篇

Java高频面试之并发篇

Java高频面试之SSM篇

Java高频面试之Mysql篇

Java高频面试之Redis篇

Java高频面试之消息队列与分布式篇

50道SQL面试题

奇奇怪怪的面试题

五花八门的内存溢出

  • 10
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值