Java并发常见面试题总结(中)

1、什么Volatile 关键字?

在 Java 中,"volatile" 关键字用于修饰变量,表示该变量可能会被多个线程同时访问,并且不保证线程安全。使用 volatile 关键字修饰的变量,在读取和写入时会强制刷新缓存,并且保证线程间的可见性和有序性,即一个线程修改了该变量的值,其他线程立即可见。

具体来说,使用 volatile 修饰的变量保证了以下三点:

  1. 可见性:一个线程修改了该变量的值,其他线程能够立即看到修改后的值。

  2. 原子性:对于单个 volatile 变量的读取和写入是原子操作,但是对于复合操作,例如自增和自减等则不具备原子性。

  3. 有序性:保证 volatile 变量的读写顺序与程序代码中的执行顺序一致。

需要注意的是,虽然 volatile 可以保证可见性和有序性,但是并不能保证线程安全。如果需要保证线程安全,需要采用其他手段,例如使用 synchronized 关键字或者使用并发容器等。

2、volatile 可以保证原子性么?

volatile 可以保证单个 volatile 变量的读取和写入是原子性的,但是对于复合操作,例如自增和自减等,则不具备原子性。因此,volatile 不能保证所有操作的原子性,只能保证单个 volatile 变量的原子性。

要想保证多个操作的原子性,可以使用 Java 的 atomic 包中提供的原子类,例如 AtomicInteger、AtomicLong 等。这些原子类提供了一些原子操作方法,例如 incrementAndGet、decrementAndGet 等,能够保证多个操作的原子性,避免了多线程并发操作时的竞态条件问题。

3、什么是悲观锁?使用场景是什么?

悲观锁是一种传统的并发控制技术,它的核心思想是:在对共享资源进行操作时,悲观地认为其他线程可能同时访问这个资源,并且会对它造成修改,因此在访问资源之前,需要先获取锁来保证资源的独占性。

悲观锁在 Java 中的实现通常是通过 synchronized 关键字来实现,或者是通过 ReentrantLock、ReadWriteLock 等锁机制来实现。当一个线程获得了锁后,其他线程就无法访问这个资源,只能等待该线程释放锁之后才能访问。

悲观锁的使用场景通常是在多线程并发访问共享资源时,比如数据库的并发控制、线程间的数据同步等。悲观锁的优点是能够保证共享资源的数据完整性和一致性,缺点是可能会引起死锁、线程等待等问题,降低系统的并发性能。因此,在使用悲观锁时需要权衡其优缺点,选择适当的场景和实现方式。

4、什么是乐观锁?使用场景是什么?

乐观锁是一种乐观地认为在对共享资源进行操作时,其他线程不会对它造成修改的并发控制技术。乐观锁不会像悲观锁一样在操作前先获取锁,而是在操作时通过一定的方式来判断资源是否被修改,如果未被修改则继续执行,如果被修改则进行相应的处理,例如重试操作、回滚操作等。

乐观锁在 Java 中的实现通常是通过版本号或者时间戳来实现,例如数据库中的乐观锁就是通过版本号实现的。每次对数据进行修改时,都需要记录当前的版本号或者时间戳,并将其保存在数据中。当其他线程要修改这个数据时,首先需要读取当前的版本号或者时间戳,然后将要修改的数据和读取到的版本号或者时间戳进行比较,如果一致则进行修改,否则认为数据已经被其他线程修改了,需要进行相应的处理。

乐观锁的使用场景通常是在并发度较高、竞争不激烈的情况下,例如多线程并发访问缓存、读多写少的场景等。乐观锁的优点是不需要加锁,避免了死锁和线程等待等问题,提高了系统的并发性能,缺点是需要处理冲突、重试等问题,增加了代码的复杂性。

总之,悲观锁和乐观锁都是并发控制技术,根据不同的场景选择合适的锁机制可以提高系统的并发性能和数据一致性。

5、如何实现乐观锁?

可以通过版本号或者时间戳来实现乐观锁。具体实现步骤如下:

  1. 在数据表中增加一个版本号或者时间戳字段,用于记录数据的版本信息。

  2. 当对数据进行修改时,需要将当前的版本号或者时间戳一并提交到数据库中。

  3. 在执行更新操作时,需要判断数据的版本号或者时间戳是否与提交的值一致,如果一致则进行更新操作,否则认为数据已经被其他线程修改了,需要进行相应的处理,例如重试操作、回滚操作等。

  4. 在执行更新操作后,需要更新数据的版本号或者时间戳,以便后续的操作可以使用新的版本信息。

下面是一个简单的 Java 代码示例,实现了基于版本号的乐观锁:


// 定义一个 User 类,包含 id、name、version 三个字段
public class User {
    private Long id;
    private String name;
    private Long version;
    
    // getter 和 setter 方法
    // ...
}

// 在更新用户信息时,使用乐观锁机制
public void updateUser(User user) {
    // 查询当前用户的版本号
    User oldUser = userDao.getById(user.getId());
    Long oldVersion = oldUser.getVersion();
    
    // 将当前用户的版本号更新为新的版本号
    user.setVersion(oldVersion + 1);
    
    // 更新用户信息
    int rows = userDao.update(user);
    
    // 如果更新失败,则说明数据已经被其他线程修改了,需要进行重试或者回滚操作
    if (rows == 0) {
        // ...
    }
}

在上面的代码中,首先查询当前用户的版本号,然后将当前用户的版本号更新为新的版本号,最后更新用户信息。如果更新失败,则说明数据已经被其他线程修改了,需要进行重试或者回滚操作。

6、乐观锁存在哪些问题?

  1. 冲突处理复杂:由于乐观锁不会加锁,所以在并发度较高的情况下,数据冲突的概率也会增加,需要处理冲突,例如重试操作、回滚操作等。这些处理逻辑会增加代码的复杂性,降低开发效率。

  2. 更新操作开销大:在乐观锁的实现中,每次更新操作都需要查询当前数据的版本信息,然后进行比较和更新,这会增加系统的开销,特别是在数据量较大、并发度较高的情况下,会对系统性能产生较大的影响。

  3. 可能存在ABA问题:在基于版本号的乐观锁实现中,如果一个线程读取了数据的版本号,然后另一个线程先修改了数据,然后又将数据修改为原来的值,再次提交,那么第一个线程就会认为数据没有被修改,从而导致数据不一致的问题。这种问题称为ABA问题。

  4. 不适用于竞争激烈的场景:乐观锁适用于并发度较高、竞争不激烈的场景,如果竞争激烈,可能会导致大量的重试和回滚操作,降低系统的性能。

7、进程之间如何通信?

  1. 管道(Pipe):管道是一种半双工的通信方式,只能在具有父子关系的进程之间使用。它是一种先进先出(FIFO)的缓冲区,其中一个进程将数据写入管道,而另一个进程则从管道中读取数据。在 Unix/Linux 系统中,管道使用 "|" 符号来表示。

  2. 命名管道(Named Pipe):命名管道也是一种半双工的通信方式,但它可以在不具有父子关系的进程之间使用。命名管道实际上是一种特殊的文件,其中一个进程将数据写入文件,而另一个进程则从文件中读取数据。

  3. 消息队列(Message Queue):消息队列是一种可在不同进程之间传递数据的机制。消息队列维护了一个消息队列池,其中每个消息都有一个类型和一个数据部分。发送进程将消息写入队列,而接收进程则从队列中读取消息。

  4. 共享内存(Shared Memory):共享内存是一种允许多个进程访问同一段内存的机制。多个进程可以将同一段内存映射到它们自己的地址空间中,并通过对该内存区域的读写来进行通信。

  5. 信号量(Semaphore):信号量是一种用于进程间同步和互斥的机制。它可以控制多个进程对共享资源的访问,从而避免资源竞争和数据不一致的问题。

  6. 套接字(Socket):套接字是一种可在不同计算机之间传递数据的机制。它可以使用 TCP 或 UDP 协议在网络上进行通信,可以用于进程之间的通信,也可以用于不同计算机之间的通信。

8、如何保证多线程下 i++ 结果正确?有几种实现方式?

在多线程环境下,i++ 操作不是原子操作,即它包含读取变量的值、对其进行加1、然后将加1后的值写回变量中这三个步骤,如果多个线程同时进行这个操作,就会导致数据错误。

为了保证多线程下 i++ 的正确性,需要使用同步机制来协调不同线程的访问。常见的实现方式包括:

  1. synchronized 关键字:可以使用 synchronized 关键字对 i++ 操作进行同步,这样在同一时刻只有一个线程能够访问 i 变量。示例代码如下:


private static int i = 0;

public synchronized void increase() {
    i++;
}
  1. Lock 接口:也可以使用 Lock 接口的实现类 ReentrantLock 来保证 i++ 的正确性。与 synchronized 不同的是,ReentrantLock 可以控制线程的等待和唤醒,具有更高的灵活性。示例代码如下:


private static int i = 0;
private static Lock lock = new ReentrantLock();

public void increase() {
    lock.lock();
    try {
        i++;
    } finally {
        lock.unlock();
    }
}

  1. 原子类(Atomic Class):Java 中提供了原子类 AtomicInteger,可以保证对 i 变量的操作是原子的。示例代码如下:


private static AtomicInteger i = new AtomicInteger(0);

public void increase() {
    i.incrementAndGet();
}

以上三种方式都可以保证多线程下 i++ 的正确性,需要根据具体情况选择合适的实现方式。

9、synchronized 是什么?

synchronized 是 Java 中的一个关键字,用于实现同步机制,保证多个线程对共享资源的互斥访问,避免并发访问导致的数据不一致或竞争条件问题。

10、如何使用synchronized?

synchronized 关键字可以用于方法和代码块上,用于实现同步机制,保证多个线程对共享资源的互斥访问。具体使用方式如下:

  1. 同步方法

在方法前加上 synchronized 关键字,这样就可以保证同一时刻只有一个线程能够进入该方法。

  1. 同步代码块

在代码块中使用 synchronized 关键字,指定要锁定的对象,保证同一时刻只有一个线程能够进入同步代码块。

需要注意的是,在使用 synchronized 关键字时,要保证锁定的对象是唯一的,否则会出现并发问题。另外,同步代码块和同步方法的锁定粒度不同,同步代码块可以指定锁定的对象,而同步方法锁定的是当前对象。

使用 synchronized 关键字时需要注意,同步代码块或同步方法只有在持有锁的情况下才能执行,如果某个线程已经持有了锁,则其他线程必须等待其释放锁才能继续执行。因此,如果同步代码块或同步方法执行时间过长,就会导致其他线程的等待时间过长,影响系统的并发性能。因此,需要根据具体情况合理使用 synchronized 关键字。

11、synchronized的底层实现原理?

synchronized 是一种基于锁的同步机制,用于保证多个线程对共享资源的互斥访问。在底层,synchronized 是通过使用对象监视器(monitor)实现的。

每个 Java 对象都会关联一个 monitor 对象,它用于实现对象的同步机制。当一个线程访问一个同步方法或同步代码块时,它会尝试获取该对象的 monitor,如果该 monitor 正在被其他线程持有,则当前线程就会进入阻塞状态,直到该 monitor 被释放。

12、JDK1.6之后,synchronized 底层做了哪些优化?

为了提高 synchronized 的性能,在 JDK1.6 中引入了偏向锁、轻量级锁和重量级锁三种机制,从而使得 synchronized 的性能得到了大幅度提升。具体优化如下:

  1. 偏向锁优化

当一个线程访问同步代码块时,如果该对象的 monitor 处于空闲状态,则该线程会将 monitor 的持有者标记为自己,这个过程称为偏向。在以后的访问中,如果发现该对象的 monitor 仍然处于偏向状态,并且持有者是自己,则该线程不需要再次竞争 monitor,从而避免了不必要的锁竞争,提高了程序性能。

  1. 轻量级锁优化

当一个线程访问同步代码块时,如果该对象的 monitor 已经被其他线程持有,则当前线程会通过 CAS 操作尝试获取 monitor,如果获取成功,则可以进入同步代码块。如果获取失败,则表示该对象已经进入了重量级锁状态,当前线程会使用自旋锁来等待 monitor 的释放。轻量级锁避免了线程上下文切换和线程阻塞的开销,因此效率较高。

  1. 重量级锁优化

当一个线程访问同步代码块时,如果该对象的 monitor 处于轻量级锁状态,但是自旋锁等待超时或者次数达到一定的阈值,或者该对象的 monitor 已经被多个线程持有,则当前线程会进入重量级锁状态。在重量级锁状态下,当前线程会阻塞等待 monitor 的释放,其他线程也需要等待该 monitor 的释放才能继续执行。虽然重量级锁效率较低,但是它可以确保多个线程之间的互斥访问,保证程序的正确性。

总体来说,偏向锁、轻量级锁和重量级锁三种机制可以根据实际情况选择最合适的锁机制,从而提高程序的并发性能。在 JDK1.6 以后,synchronized 在锁机制上的优化使得它的性能得到了大幅度提升,成为 Java 中最常用的同步机制之一。

13、synchronized 和 volatile 有什么区别

synchronized 和 volatile 都是 Java 中用于保证多线程程序正确性的关键字,它们的作用和使用场景有所不同,区别如下:

  1. 作用不同

synchronized 用于保证代码块或方法在多线程访问时的同步性,保证在同一时间只有一个线程可以执行 synchronized 代码块或方法。它可以保证互斥性和可见性,可以防止出现数据竞争、死锁等问题。

volatile 用于保证变量在多线程之间的可见性,即当一个线程修改了 volatile 变量的值,其他线程能够立即看到这个变量的最新值。它可以确保线程之间的数据同步,防止出现数据不一致的问题。

  1. 实现方式不同

synchronized 是通过获取对象的监视器锁来实现同步的,当一个线程获取到锁后,其他线程就不能再获取锁,只能等待当前线程释放锁。在 Java 5 之前,synchronized 是基于重量级锁实现的,效率较低,从 Java 5 开始,synchronized 还支持了偏向锁、轻量级锁和重量级锁等多种优化方式,性能得到了很大的提升。

volatile 是通过内存屏障和 CPU 缓存一致性协议来实现的。当一个线程修改了 volatile 变量的值时,会立即将修改后的值刷新到主内存中,并且通知其他线程刷新 CPU 缓存中的该变量的值。这样其他线程读取该变量时就能得到最新的值,而不是从自己的 CPU 缓存中读取旧值。

  1. 应用场景不同

synchronized 适用于多个线程之间需要协调同步的情况,比如共享资源的读写操作、临界区的访问等。它可以保证同一时间只有一个线程执行临界区,防止出现数据竞争、死锁等问题。

volatile 适用于一个线程修改了变量值,其他线程需要立即看到最新值的情况,比如状态标志位的修改、计数器的自增操作等。它可以确保线程之间的数据同步,防止出现数据不一致的问题。

总体来说,synchronized 和 volatile 都是用于保证多线程程序正确性的关键字,但是它们的作用和使用场景有所不同。synchronized 用于实现多个线程之间的同步和互斥,volatile 用于保证变量在多线程之间的可见性和一致性。在实际开发中,需要根据具体的场景选择最合适的关键字来保证程序的正确性。

14、ReentrantLock 是什么?

ReentrantLock 是 Java 提供的一种可重入锁,是一种替代 synchronized 关键字的工具,可以用于多线程同步操作。与 synchronized 不同的是,ReentrantLock 提供了更多的功能和灵活性,可以控制锁的获取和释放、可中断性、公平性等,还支持多个条件变量和超时等待等。

ReentrantLock 是一种独占锁,同一时间只有一个线程可以获取到锁,其他线程需要等待锁释放才能继续执行。它实现了 Lock 接口,提供了 lock()、unlock()、tryLock() 等方法来实现锁的获取和释放,也提供了与 synchronized 相似的功能,如等待/通知机制、可重入性、可见性等。

相比于 synchronized 关键字,ReentrantLock 在一些场景下具有更好的性能和更灵活的控制能力,但同时也需要程序员自己掌握更多的细节和使用技巧。在使用 ReentrantLock 时,需要注意避免死锁、饥饿等问题,合理使用锁的条件变量和超时等待等。

15、公平锁和非公平锁有什么区别?

公平锁和非公平锁是针对锁的获取顺序而言的。

公平锁指的是线程按照申请锁的顺序来获取锁,即先申请锁的线程先获取锁,保证锁的获取是公平的,不会出现线程饥饿的情况。在公平锁的实现中,当有线程申请锁时,如果锁已经被占用,则该线程会进入等待队列中等待,等待队列中的线程会按照先来先服务的原则依次获取锁。

非公平锁则是线程获取锁的顺序是不确定的,也就是说新申请的线程有可能会插队成功获取到锁,这样就可能会出现某些线程一直获取不到锁的情况,从而出现线程饥饿的情况。在非公平锁的实现中,当有线程申请锁时,如果锁已经被占用,则该线程会直接尝试获取锁,而不是进入等待队列中等待。

公平锁和非公平锁的选择取决于应用场景和要求。如果需要保证线程的公平性,避免线程饥饿的情况,则应该选择公平锁;如果对线程的获取顺序没有要求,需要提高锁的吞吐量和效率,则可以选择非公平锁。但需要注意的是,非公平锁可能会导致某些线程一直无法获取到锁,从而引发性能问题,应该根据具体的应用场景进行选择。

 

公众号:大雄说技术

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值