公平锁/非公平锁、自旋锁、synchronized和lock的区别、合适的线程数是多少?CPU核心数和线程数的关系?(java八股文面试题)

1、你知道“公平锁”吗?为什么会有“非公平锁”?

公平锁(Fair Lock): 公平锁是指多个线程按照它们发出请求的顺序来获取锁,即先来先得。当一个线程释放锁时,等待队列中的线程会按照它们进入队列的顺序逐个获得锁。这种方式可以确保所有线程都有机会获取锁,避免某些线程一直被饿死(永远无法获取锁)的情况。 

非公平锁(Unfair Lock): 非公平锁则不考虑线程请求锁的顺序,它允许新到来的线程抢占已经被持有的锁,即使有其他线程在等待。这可能会导致某些线程频繁获得锁,而其他线程一直无法获取锁,造成不公平的情况。但是由于不考虑线程的请求顺序,非公平锁在某些情况下可能会比公平锁更快。

在实际编程中,可以根据具体的需求来选择使用公平锁还是非公平锁。如果对线程的执行顺序要求较高,希望避免饥饿现象,那么可以选择公平锁。如果对性能要求较高,而且可以容忍一些线程的相对不公平,那么可以选择非公平锁。

 Java的ReentrantLock类可以作为公平锁或非公平锁进行配置,而synchronized关键字在某种程度上可以被视为一种非公平锁,因为它并不保证等待线程按照请求顺序获得锁。

补充知识

1.1线程的转态:
  1. 新建(New):当线程对象被创建但尚未启动时,线程处于新建状态。

  2. 可运行(Runnable):线程已经启动并且可以在CPU上执行,但它可能正在等待分配CPU时间片,也可能正在执行。

  3. 运行(Running):线程正在CPU上执行代码。

  4. 阻塞(Blocked):线程被阻塞,暂时停止执行,等待某些条件的满足。常见的阻塞情况包括等待获取锁、等待I/O操作完成等。

  5. 等待(Waiting):线程处于等待状态,等待某些特定条件的发生,进入等待状态的线程需要被其他线程唤醒。

  6. 计时等待(Timed Waiting):与等待状态类似,但是线程会在一定时间后自动苏醒,不需要被其他线程唤醒。常见的计时等待情况包括使用Thread.sleep()或等待锁的时候设置了超时时间。

  7. 终止(Terminated):线程完成了它的执行任务或因某些原因终止,处于终止状态。

在Java中,可以通过Thread类的方法以及相关的API来管理和监控线程的状态。例如:

  • Thread.start():启动线程,使其进入可运行状态。
  • Thread.sleep(long milliseconds):使线程进入计时等待状态。
  • Object.wait():使线程进入等待状态。
  • Thread.join():等待其他线程执行完毕。
  • Thread.interrupt():中断线程,将其从阻塞状态或等待状态唤醒。
  • Thread.getState():获取线程的当前状态。
1.2常见的线程锁
  1. 内置锁(Intrinsic Lock)/ 监视器锁(Monitor Lock): 在Java中,每个对象都有一个内置锁,也称为监视器锁。通过synchronized关键字,可以使用内置锁来实现线程同步。当线程进入synchronized块或方法时,它会尝试获取对象的内置锁。其他线程如果想要进入同一个对象的synchronized块或方法,必须等待内置锁的释放。

  2. 重入锁(Reentrant Lock): Java中的ReentrantLock类提供了显式的锁机制,也称为重入锁。与synchronized不同,重入锁允许同一个线程多次获得同一个锁,避免了死锁的情况。它还支持更灵活的锁获取和释放操作,并且可以选择公平锁或非公平锁。

  3. 读写锁(Read-Write Lock)ReadWriteLock接口定义了读写锁,其中包含了读锁和写锁。读锁允许多个线程同时访问共享资源,而写锁只允许一个线程访问资源并进行修改。这种锁适用于读多写少的场景,可以提高并发性能。

  4. 信号量(Semaphore): 信号量是一种更通用的同步机制,它允许多个线程同时访问一个资源,但限制了可以同时访问资源的线程数量。Java中的Semaphore类可以用来实现信号量。

  5. 倒计时门闩(CountDownLatch)CountDownLatch类用于等待一组线程执行完毕。它允许一个或多个线程等待其他线程完成任务后再继续执行。

  6. 循环栅栏(CyclicBarrier)CyclicBarrier类允许一组线程在某个点上等待彼此,然后一起继续执行。

  7. 阻塞队列(Blocking Queue): 阻塞队列是一种特殊的队列,可以用于在多线程环境中实现生产者-消费者模型。它提供了线程安全的入队和出队操作,可以用于有效地协调生产者和消费者线程。

2、你对自旋锁了解吗?优缺点分别是什么?

 自旋锁(Spin Lock):

自旋锁是一种基于忙等待(busy-waiting)的同步机制,线程在获取锁时会一直循环检测锁的状态,直到获得锁为止。自旋锁适用于以下情况:

  • 临界区代码执行时间较短,不会花费太多CPU时间。
  • 在竞争激烈时,锁的占用时间短,不会导致其他线程的等待时间过长。
  • 可以有效减少线程切换的开销,因为线程不会被挂起和唤醒。

优点:

  1. 低延迟: 自旋锁不涉及线程的挂起和唤醒,避免了线程切换的开销,因此在临界区执行时间很短的情况下,自旋锁可以带来较低的延迟。

  2. 避免上下文切换: 自旋锁不会让线程阻塞,因此避免了线程上下文切换的开销,特别是在多核处理器上。

缺点:

  1. CPU消耗: 自旋锁需要不断地在循环中检测锁的状态,这会消耗CPU资源。如果临界区执行时间较长,自旋锁可能会浪费大量的CPU时间。

  2. 竞争激烈时效率下降: 当多个线程竞争同一个锁时,如果自旋锁被频繁地抢占,会导致线程之间的竞争变得更加激烈,可能会影响性能。

  3. 不适用于长时间等待: 如果一个线程在自旋锁上等待的时间过长,会造成CPU资源的浪费。因此,自旋锁不适合用于长时间等待的情况。

  4. 可能引发活锁: 自旋锁的自旋过程可能会导致线程陷入活锁,即一直在自旋但始终无法获取锁。

非自旋锁(Non-Spin Lock):

非自旋锁是一种基于阻塞的同步机制,当线程无法获取锁时,会被挂起并进入等待状态,直到锁被其他线程释放并通知。非自旋锁适用于以下情况:

  • 临界区代码执行时间较长,自旋等待会浪费大量的CPU资源。
  • 线程竞争较激烈,自旋等待会增加竞争导致的开销。
  • 线程挂起和唤醒的开销相对较小,不会影响性能。

3、线程枷锁有哪些方式?synchronized和lock的区别?

 当涉及多线程编程时,我们需要确保多个线程在访问共享资源时不会产生问题。synchronizedLock 都是用来帮助我们实现这种同步的工具。

  • 使用 synchronized 想象你们进入一间书房,书房门上有一把锁。每次只能有一个人进入,而其他人必须在门外等待。当一个人进入书房(获取锁),他可以阅读书籍(访问共享资源)。其他人必须等待他出来(释放锁)后才能进去。这确保了在任何时候只有一个人在书房里,防止了人们同时翻阅书籍造成混乱。

  • 使用 Lock 现在,你们进入一个更大的房间,里面有许多不同的书房,每个书房都有一把锁。每个人可以选择一个书房,进去后锁上门,开始阅读。其他人看到门是锁着的,就知道里面有人在阅读。如果有人突然想要进入一个被锁的书房,他可以等待,或者干脆去其他的书房。这种情况下,每个人有更多的选择和控制权。

总之,synchronized 是一种更简单的方法,就像一个房间的通用钥匙,可以防止多个线程同时访问共享资源。而 Lock 是一种更强大的方法,允许你更精细地控制线程的访问,就像每个房间都有自己的锁,人们可以根据需要选择进入哪个房间。不过,需要注意的是,Lock 的使用可能更加复杂一些,需要手动管理锁的获取和释放。

因此,Lock 提供了更多的控制和灵活性,特别是在需要更高级的同步策略时。然而,它也更复杂一些,需要更多的编程工作。选择使用哪种同步机制取决于你的需求和情况。

Synchronized和Lock的区别:

  • Synchronized编码简单,锁机制后JVM维护,在竞争不激烈的情况下性能更好。Lock功能更强大更灵活,竞争激烈时性能更好。
  • 性能不一样:只要竞争激烈的情况下,lock性能会比synchronized好,竞争不激烈的情况下,synchronized比lock性能好,synchronized会根据锁的情况,从偏向锁-->轻量锁-->重量级锁升级,而且编程更简单。
  • 锁机制不一样:synchronized是在JVM层面实现的,系统会监控锁的释放与否。lock是JDK代码实现的,需要手动释放,在finally快中释放。可以采用非阻塞的方式获取锁。
  • synchronized的编程更简洁,lock的功能更多更灵活,缺点是一定要在finally里面unlock()资源才行。
  • 用法不一样:synchronized可以在方法上、代码快上,lock只可以在代码里,不能直接修改方法。

lock支持的功能:

  • 公平锁: Synchronized 是非公平锁,Lock 支持公平锁,默认非公平锁
  • 可中断锁: ReentrantLock 提供了 locklnterruptiblvy )的功能,可以中断争夺锁的操作,抢锁的时会 check 是否被中断、中断直接抛出异常,退出抢锁。而 Synchronized 只有抢锁的过程,不可干预,直到抢到锁以后,才可以编码控制锁的释放。
  • 快速反馈锁:ReentrantLock 提供了 trvlock )和 tvck trvTimes)的功能,不等待或者限定时间等待获取锁,更灵活。可以避免死锁的发生。
  • 读写锁: ReentrantReadWriteLock 类实现了读写锁的功能,类似于 Mysql,锁自身维护一个计数器,读锁可以并发的获取,写锁只能独占。而synchronized全是独占锁。
  • Condition: ReentrantLock 提供了比 Sync 更精准的线程调度工具,Condition,一个lck 可以有多个 Condition,比如在生产消费的业务下,一个锁通过控制生产 Condition 和消费 Condition 精准控制。

4、合适的线程数是多少?CPU核心数和线程数的关系?

合适的线程数是一个复杂的问题,因为它取决于多个因素,包括硬件特性、应用程序性质、任务类型以及操作系统的调度机制。

其实我们可以通过CPU密集型任务和I/O密集型任务这两种类型任务来考虑线程数问题。

CPU密集型任务:

CPU密集型任务是指任务的主要瓶颈在于CPU的计算能力,而不是在等待I/O操作完成。典型的CPU密集型任务包括数学运算、图像处理、加密解密等。

在CPU密集型任务中,合适的线程数通常与CPU的核心数相关。这是因为每个核心可以执行一个线程,因此使用与核心数相近的线程数可以充分利用CPU资源,避免了线程切换的开销。不过,超过核心数的线程数可能会引起不必要的上下文切换开销,从而降低性能。

建议: 在CPU密集型任务中,通常可以从与CPU核心数相等或略小于核心数的线程数开始,然后通过基准测试逐渐调整线程数,找到性能的最佳平衡点。

I/O密集型任务:

I/O密集型任务是指任务的主要瓶颈在于等待I/O操作完成,例如从磁盘读取数据、网络通信等。在这种任务中,线程在等待I/O时不会完全占用CPU,因此可以更充分地利用空闲的CPU时间。

在I/O密集型任务中,适当增加线程数可以使得在一个线程等待I/O的时候,另一个线程可以继续执行,从而充分利用CPU资源。然而,过多的线程可能会导致线程切换的开销增加,甚至超过CPU资源的利用。

建议: 在I/O密集型任务中,通常可以适当增加线程数,但不必追求与核心数一致。你可以通过实验和基准测试来找到最佳线程数,以确保在I/O等待时能够充分利用CPU,并避免过多的线程切换开销。

有一个硬核的公式,这个公式是java并发实战的一个作者给出的,如果我们不能确定创景中的线程数,我们可以直接使用这个公式,就可以直接计算出一个比较合理的线程数了,然后在进行性能的调试。

线程数=CPU核心数*(1+平均等待时间/平均工作时间)

  • CPU核心数: 这是你计算机中实际的CPU核心数量,每个核心可以执行一个线程。

  • 平均等待时间: 这是指在某个任务中,线程在执行时可能需要等待的时间,比如等待I/O操作完成。

  • 平均工作时间: 这是指在线程执行任务时实际进行工作的时间。

总之,选择合适的线程数取决于任务类型和应用场景。了解任务的特点,理解计算资源的使用情况以及进行实际测试是找到最佳线程数的关键。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值