[java八股文][Java并发编程面试篇]并发安全
juc包下你常用的类?
线程池相关:
ThreadPoolExecutor
:最核心的线程池类,用于创建和管理线程池。通过它可以灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足不同的并发处理需求。Executors
:线程池工厂类,提供了一系列静态方法来创建不同类型的线程池,如newFixedThreadPool
(创建固定线程数的线程池)、newCachedThreadPool
(创建可缓存线程池)、newSingleThreadExecutor
(创建单线程线程池)等,方便开发者快速创建线程池。
并发集合类:
ConcurrentHashMap
:线程安全的哈希映射表,用于在多线程环境下高效地存储和访问键值对。它采用了分段锁等技术,允许多个线程同时访问不同的段,提高了并发性能,在高并发场景下比传统的Hashtable
性能更好。CopyOnWriteArrayList
:线程安全的列表,在对列表进行修改操作时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然可以在旧数组上进行,从而实现了读写分离,提高了并发读的性能,适用于读多写少的场景。
同步工具类:
CountDownLatch
:允许一个或多个线程等待其他一组线程完成操作后再继续执行。它通过一个计数器来实现,计数器初始化为线程的数量,每个线程完成任务后调用countDown
方法将计数器减一,当计数器为零时,等待的线程可以继续执行。常用于多个线程完成各自任务后,再进行汇总或下一步操作的场景。CyclicBarrier
:让一组线程互相等待,直到所有线程都到达某个屏障点后,再一起继续执行。与CountDownLatch
不同的是,CyclicBarrier
可以重复使用,当所有线程都通过屏障后,计数器会重置,可以再次用于下一轮的等待。适用于多个线程需要协同工作,在某个阶段完成后再一起进入下一个阶段的场景。Semaphore
:信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可计数器,线程在访问资源前需要获取许可,如果有可用许可,则获取成功并将许可计数器减一,否则线程需要等待,直到有其他线程释放许可。常用于控制对有限资源的访问,如数据库连接池、线程池中的线程数量等。
原子类:
AtomicInteger
:原子整数类,提供了对整数类型的原子操作,如自增、自减、比较并交换等。通过硬件级别的原子指令来保证操作的原子性和线程安全性,避免了使用锁带来的性能开销,在多线程环境下对整数进行计数、状态标记等操作非常方便。AtomicReference
:原子引用类,用于对对象引用进行原子操作。可以保证在多线程环境下,对对象的更新操作是原子性的,即要么全部成功,要么全部失败,不会出现数据不一致的情况。常用于实现无锁数据结构或需要对对象进行原子更新的场景。
#怎么保证多线程安全?
- synchronized关键字:可以使用
synchronized
关键字来同步代码块或方法,确保同一时刻只有一个线程可以访问这些代码。对象锁是通过synchronized
关键字锁定对象的监视器(monitor)来实现的。
public synchronized void someMethod() { /* ... */ }public void anotherMethod() {synchronized (someObject) {/* ... */}
}
- volatile关键字:
volatile
关键字用于变量,确保所有线程看到的是该变量的最新值,而不是可能存储在本地寄存器中的副本。
public volatile int sharedVariable;
- Lock接口和ReentrantLock类:
java.util.concurrent.locks.Lock
接口提供了比synchronized
更强大的锁定机制,ReentrantLock
是一个实现该接口的例子,提供了更灵活的锁管理和更高的性能。
private final ReentrantLock lock = new ReentrantLock();public void someMethod() {lock.lock();try {/* ... */} finally {lock.unlock();}
}
- 原子类:Java并发库(
java.util.concurrent.atomic
)提供了原子类,如AtomicInteger
、AtomicLong
等,这些类提供了原子操作,可以用于更新基本类型的变量而无需额外的同步。
示例:
AtomicInteger counter = new AtomicInteger(0);int newValue = counter.incrementAndGet();
- 线程局部变量:
ThreadLocal
类可以为每个线程提供独立的变量副本,这样每个线程都拥有自己的变量,消除了竞争条件。
ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();threadLocalVar.set(10);
int value = threadLocalVar.get();
- 并发集合:使用
java.util.concurrent
包中的线程安全集合,如ConcurrentHashMap
、ConcurrentLinkedQueue
等,这些集合内部已经实现了线程安全的逻辑。 - JUC工具类: 使用
java.util.concurrent
包中的一些工具类可以用于控制线程间的同步和协作。例如:Semaphore
和CyclicBarrier
等。
#Java中有哪些常用的锁,在什么场景下使用?
Java中的锁是用于管理多线程并发访问共享资源的关键机制。锁可以确保在任意给定时间内只有一个线程可以访问特定的资源,从而避免数据竞争和不一致性。Java提供了多种锁机制,可以分为以下几类:
- 内置锁(synchronized):Java中的
synchronized
关键字是内置锁机制的基础,可以用于方法或代码块。当一个线程进入synchronized
代码块或方法时,它会获取关联对象的锁;当线程离开该代码块或方法时,锁会被释放。如果其他线程尝试获取同一个对象的锁,它们将被阻塞,直到锁被释放。其中,syncronized加锁时有无锁、偏向锁、轻量级锁和重量级锁几个级别。偏向锁用于当一个线程进入同步块时,如果没有任何其他线程竞争,就会使用偏向锁,以减少锁的开销。轻量级锁使用线程栈上的数据结构,避免了操作系统级别的锁。重量级锁则涉及操作系统级的互斥锁。 - ReentrantLock:
java.util.concurrent.locks.ReentrantLock
是一个显式的锁类,提供了比synchronized
更高级的功能,如可中断的锁等待、定时锁等待、公平锁选项等。ReentrantLock
使用lock()
和unlock()
方法来获取和释放锁。其中,公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。非公平锁不保证锁分配的顺序,可以减少锁的竞争,提高性能,但可能造成某些线程的饥饿。 - 读写锁(ReadWriteLock):
java.util.concurrent.locks.ReadWriteLock
接口定义了一种锁,允许多个读取者同时访问共享资源,但只允许一个写入者。读写锁通常用于读取远多于写入的情况,以提高并发性。 - 乐观锁和悲观锁:悲观锁(Pessimistic Locking)通常指在访问数据前就锁定资源,假设最坏的情况,即数据很可能被其他线程修改。
synchronized
和ReentrantLock
都是悲观锁的例子。乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用版本号或时间戳来实现。 - 自旋锁:自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞。通常可以使用CAS来实现。这在锁等待时间很短的情况下可以提高性能,但过度自旋会浪费CPU资源。
#怎么在实践中用锁的?
Java提供了多种锁的实现,包括synchronized
关键字、java.util.concurrent.locks
包下的Lock
接口及其具体实现如ReentrantLock
、ReadWriteLock
等。下面我们来看看这些锁的使用方式。
synchronized
synchronized
关键字可以用于方法或代码块,它是Java中最早的锁实现,使用起来非常简单。
示例:synchronized方法
public class Counter {private int count = 0;public synchronized void increment() {count++;}public synchronized int getCount() {return count;}
}
示例:synchronized代码块
public class Counter {private Object lock = new Object();private int count = 0;public void increment() {synchronized (lock) {count++;}}
}
- 使用
Lock
接口
Lock
接口提供了比synchronized
更灵活的锁操作,包括尝试锁、可中断锁、定时锁等。ReentrantLock
是Lock
接口的一个实现。
示例:使用ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Counter {private Lock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}
}
- 使用
ReadWriteLock
ReadWriteLock
接口提供了一种读写锁的实现,允许多个读操作同时进行,但写操作是独占的。
示例:使用ReadWriteLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class Cache {private ReadWriteLock lock = new ReentrantReadWriteLock();private Lock readLock = lock.readLock();private Lock writeLock = lock.writeLock();private Object data;public Object readData() {readLock.lock();try {return data;} finally {readLock.unlock();}}public void writeData(Object newData) {writeLock.lock();try {data = newData;} finally {writeLock.unlock();}}
}
#Java 并发工具你知道哪些?
Java 中一些常用的并发工具,它们位于 java.util.concurrent
包中,常见的有:
- CountDownLatch:CountDownLatch 是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。它使用一个计数器进行初始化,调用
countDown()
方法会使计数器减一,当计数器的值减为 0 时,等待的线程会被唤醒。可以把它想象成一个倒计时器,当倒计时结束(计数器为 0)时,等待的事件就会发生。示例代码:
import java.util.concurrent.CountDownLatch;public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {int numberOfThreads = 3;CountDownLatch latch = new CountDownLatch(numberOfThreads);// 创建并启动三个工作线程for (int i = 0; i < numberOfThreads; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 正在工作");try {Thread.sleep(1000); // 模拟工作时间} catch (InterruptedException e) {e.printStackTrace();}latch.countDown(); // 完成工作,计数器减一System.out.println(Thread.currentThread().getName() + " 完成工作");}).start();}System.out.println("主线程等待工作线程完成");latch.await(); // 主线程等待,直到计数器为 0System.out.println("所有工作线程已完成,主线程继续执行");}
}
- CyclicBarrier:CyclicBarrier 允许一组线程互相等待,直到到达一个公共的屏障点。当所有线程都到达这个屏障点后,它们可以继续执行后续操作,并且这个屏障可以被重置循环使用。与
CountDownLatch
不同,CyclicBarrier
侧重于线程间的相互等待,而不是等待某些操作完成。示例代码:
import java.util.concurrent.CyclicBarrier;public class CyclicBarrierExample {public static void main(String[] args) {int numberOfThreads = 3;CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> {System.out.println("所有线程都到达了屏障,继续执行后续操作");});for (int i = 0; i < numberOfThreads; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + " 正在运行");Thread.sleep(1000); // 模拟运行时间barrier.await(); // 等待其他线程System.out.println(Thread.currentThread().getName() + " 已经通过屏障");} catch (Exception e) {e.printStackTrace();}}).start();}}
}
- Semaphore:Semaphore 是一个计数信号量,用于控制同时访问某个共享资源的线程数量。通过
acquire()
方法获取许可,使用release()
方法释放许可。如果没有许可可用,线程将被阻塞,直到有许可被释放。可以用来限制对某些资源(如数据库连接池、文件操作等)的并发访问量。代码如下:
import java.util.concurrent.Semaphore;public class SemaphoreExample {public static void main(String[] args) {Semaphore semaphore = new Semaphore(2); // 允许 2 个线程同时访问for (int i = 0; i < 5; i++) {new Thread(() -> {try {semaphore.acquire(); // 获取许可System.out.println(Thread.currentThread().getName() + " 获得了许可");Thread.sleep(2000); // 模拟资源使用System.out.println(Thread.currentThread().getName() + " 释放了许可");semaphore.release(); // 释放许可} catch (InterruptedException e) {e.printStackTrace();}}).start();}}
}
- Future 和 Callable:Callable 是一个类似于
Runnable
的接口,但它可以返回结果,并且可以抛出异常。Future 用于表示一个异步计算的结果,可以通过它来获取Callable
任务的执行结果或取消任务。代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;public class FutureCallableExample {public static void main(String[] args) throws Exception {ExecutorService executorService = Executors.newSingleThreadExecutor();Callable<Integer> callable = () -> {System.out.println(Thread.currentThread().getName() + " 开始执行 Callable 任务");Thread.sleep(2000); // 模拟耗时操作return 42; // 返回结果};Future<Integer> future = executorService.submit(callable);System.out.println("主线程继续执行其他任务");try {Integer result = future.get(); // 等待 Callable 任务完成并获取结果System.out.println("Callable 任务的结果: " + result);} catch (Exception e) {e.printStackTrace();}executorService.shutdown();}
}
- ConcurrentHashMap:ConcurrentHashMap 是一个线程安全的哈希表,它允许多个线程同时进行读操作,在一定程度上支持并发的修改操作,避免了
HashMap
在多线程环境下需要使用synchronized
或Collections.synchronizedMap()
进行同步的性能问题。代码如下:
import java.util.concurrent.ConcurrentHashMap;public class ConcurrentHashMapExample {public static void main(String[] args) {ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();map.put("key1", 1);map.put("key2", 2);// 并发读操作map.forEach((key, value) -> System.out.println(key + ": " + value));// 并发写操作map.computeIfAbsent("key3", k -> 3);}
}
#CountDownLatch 是做什么的讲一讲?
CountDownLatch 是 Java 并发包(java.util.concurrent
)中的一个同步工具类,用于让一个或多个线程等待其他线程完成操作后再继续执行。
其核心是通过一个计数器(Counter)实现线程间的协调,常用于多线程任务的分阶段控制或主线程等待多个子线程就绪的场景,核心原理:
- 初始化计数器:创建
CountDownLatch
时指定一个初始计数值(如N
)。 - 等待线程阻塞:调用
await()
的线程会被阻塞,直到计数器变为 0。 - 任务完成通知:其他线程完成任务后调用
countDown()
,使计数器减 1。 - 唤醒等待线程:当计数器减到 0 时,所有等待的线程会被唤醒。
主线程等待所有子线程就绪后启动,代码例子如下:
// 主线程启动多个子线程执行任务,等待全部完成后统计结果
public class MainThreadWaitExample {public static void main(String[] args) throws InterruptedException {int threadCount = 3;CountDownLatch latch = new CountDownLatch(threadCount);for (int i = 0; i < threadCount; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + " 执行任务");Thread.sleep(1000);latch.countDown(); // 任务完成,计数器-1} catch (InterruptedException e) {e.printStackTrace();}}, "Worker-" + i).start();}latch.await(); // 主线程等待所有子线程完成任务System.out.println("所有任务已完成");}
}
#synchronized和reentrantlock及其应用场景?
synchronized 工作原理
synchronized是Java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,
使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。
执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。
synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。
从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。
实际上大部分时候我认为说到monitorenter就行了,但是为了更清楚的描述,还是再具体一点。
如果再深入到源码来说,synchronized实际上有两个队列waitSet和entryList。
- 当多个线程进入同步代码块时,首先进入entryList
- 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
- 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
- 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null
reentrantlock工作原理
ReentrantLock 的底层实现主要依赖于 AbstractQueuedSynchronizer(AQS)这个抽象类。AQS 是一个提供了基本同步机制的框架,其中包括了队列、状态值等。
ReentrantLock 在 AQS 的基础上通过内部类 Sync 来实现具体的锁操作。不同的 Sync 子类实现了公平锁和非公平锁的不同逻辑:
- 可中断性: ReentrantLock 实现了可中断性,这意味着线程在等待锁的过程中,可以被其他线程中断而提前结束等待。在底层,ReentrantLock 使用了与 LockSupport.park() 和 LockSupport.unpark() 相关的机制来实现可中断性。
- 设置超时时间: ReentrantLock 支持在尝试获取锁时设置超时时间,即等待一定时间后如果还未获得锁,则放弃锁的获取。这是通过内部的 tryAcquireNanos 方法来实现的。
- 公平锁和非公平锁: 在直接创建 ReentrantLock 对象时,默认情况下是非公平锁。公平锁是按照线程等待的顺序来获取锁,而非公平锁则允许多个线程在同一时刻竞争锁,不考虑它们申请锁的顺序。公平锁可以通过在创建 ReentrantLock 时传入 true 来设置,例如:
ReentrantLock fairLock = new ReentrantLock(true);
- 多个条件变量: ReentrantLock 支持多个条件变量,每个条件变量可以与一个 ReentrantLock 关联。这使得线程可以更灵活地进行等待和唤醒操作,而不仅仅是基于对象监视器的 wait() 和 notify()。多个条件变量的实现依赖于 Condition 接口,例如:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 使用下面方法进行等待和唤醒
condition.await();
condition.signal();
- 可重入性: ReentrantLock 支持可重入性,即同一个线程可以多次获得同一把锁,而不会造成死锁。这是通过内部的 holdCount 计数来实现的。当一个线程多次获取锁时,holdCount 递增,释放锁时递减,只有当 holdCount 为零时,其他线程才有机会获取锁。
应用场景的区别
synchronized:
- 简单同步需求: 当你需要对代码块或方法进行简单的同步控制时,
synchronized
是一个很好的选择。它使用起来简单,不需要额外的资源管理,因为锁会在方法退出或代码块执行完毕后自动释放。 - 代码块同步: 如果你想对特定代码段进行同步,而不是整个方法,可以使用
synchronized
代码块。这可以让你更精细地控制同步的范围,从而减少锁的持有时间,提高并发性能。 - 内置锁的使用:
synchronized
关键字使用对象的内置锁(也称为监视器锁),这在需要使用对象作为锁对象的情况下很有用,尤其是在对象状态与锁保护的代码紧密相关时。
ReentrantLock:
- 高级锁功能需求:
ReentrantLock
提供了synchronized
所不具备的高级功能,如公平锁、响应中断、定时锁尝试、以及多个条件变量。当你需要这些功能时,ReentrantLock
是更好的选择。 - 性能优化: 在高度竞争的环境中,
ReentrantLock
可以提供比synchronized
更好的性能,因为它提供了更细粒度的控制,如尝试锁定和定时锁定,可以减少线程阻塞的可能性。 - 复杂同步结构: 当你需要更复杂的同步结构,如需要多个条件变量来协调线程之间的通信时,
ReentrantLock
及其配套的Condition
对象可以提供更灵活的解决方案。
综上,synchronized
适用于简单同步需求和不需要额外锁功能的场景,而ReentrantLock
适用于需要更高级锁功能、性能优化或复杂同步逻辑的情况。选择哪种同步机制取决于具体的应用需求和性能考虑。
#除了用synchronized,还有什么方法可以实现线程同步?
- 使用
ReentrantLock
类:ReentrantLock
是一个可重入的互斥锁,相比synchronized
提供了更灵活的锁定和解锁操作。它还支持公平锁和非公平锁,以及可以响应中断的锁获取操作。 - 使用
volatile
关键字:虽然volatile
不是一种锁机制,但它可以确保变量的可见性。当一个变量被声明为volatile
后,线程将直接从主内存中读取该变量的值,这样就能保证线程间变量的可见性。但它不具备原子性。 - 使用
Atomic
类:Java提供了一系列的原子类,例如AtomicInteger
、AtomicLong
、AtomicReference
等,用于实现对单个变量的原子操作,这些类在实现细节上利用了CAS(Compare-And-Swap)算法,可以用来实现无锁的线程安全。
#synchronized锁静态方法和普通方法区别?
锁的对象不同:
- 普通方法:锁的是当前对象实例(
this
)。同一对象实例的synchronized
普通方法,同一时间只能被一个线程访问;不同对象实例间互不影响,可被不同线程同时访问各自的同步普通方法。 - 静态方法:锁的是当前类的
Class
对象。由于类的Class
对象全局唯一,无论多少个对象实例,该静态同步方法同一时间只能被一个线程访问。
作用范围不同:
- 普通方法:仅对同一对象实例的同步方法调用互斥,不同对象实例的同步普通方法可并行执行。
- 静态方法:对整个类的所有实例的该静态方法调用都互斥,一个线程进入静态同步方法,其他线程无法进入同一类任何实例的该方法。
多实例场景影响不同:
- 普通方法:多线程访问不同对象实例的同步普通方法时,可同时执行。
- 静态方法:不管有多少对象实例,同一时间仅一个线程能执行该静态同步方法。
#synchronized和reentrantlock区别?
synchronized 和 ReentrantLock 都是 Java 中提供的可重入锁:
- 用法不同:synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块上。
- 获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁。而 ReentrantLock 需要手动加锁和释放锁
- 锁类型不同:synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
- 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
- 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
#怎么理解可重入锁?
可重入锁是指同一个线程在获取了锁之后,可以再次重复获取该锁而不会造成死锁或其他问题。当一个线程持有锁时,如果再次尝试获取该锁,就会成功获取而不会被阻塞。
ReentrantLock实现可重入锁的机制是基于线程持有锁的计数器。
- 当一个线程第一次获取锁时,计数器会加1,表示该线程持有了锁。在此之后,如果同一个线程再次获取锁,计数器会再次加1。每次线程成功获取锁时,都会将计数器加1。
- 当线程释放锁时,计数器会相应地减1。只有当计数器减到0时,锁才会完全释放,其他线程才有机会获取锁。
这种计数器的设计使得同一个线程可以多次获取同一个锁,而不会造成死锁或其他问题。每次获取锁时,计数器加1;每次释放锁时,计数器减1。只有当计数器减到0时,锁才会完全释放。
ReentrantLock通过这种计数器的方式,实现了可重入锁的机制。它允许同一个线程多次获取同一个锁,并且能够正确地处理锁的获取和释放,避免了死锁和其他并发问题。
#synchronized 支持重入吗?如何实现的?
synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
synchronized底层是利用计算机系统mutex Lock实现的。每一个可重入锁都会关联一个线程ID和一个锁状态status。
当一个线程请求方法时,会去检查锁状态。
- 如果锁状态是0,代表该锁没有被占用,使用CAS操作获取锁,将线程ID替换成自己的线程ID。
- 如果锁状态不是0,代表有线程在访问该方法。此时,如果线程ID是自己的线程ID,如果是可重入锁,会将status自增1,然后获取到该锁,进而执行相应的方法;如果是非重入锁,就会进入阻塞队列等待。
在释放锁时,
- 如果是可重入锁的,每一次退出方法,就会将status减1,直至status的值为0,最后释放该锁。
- 如果非可重入锁的,线程退出方法,直接就会释放该锁。
#syncronized锁升级的过程讲一下
具体的锁升级的过程是:无锁->偏向锁->轻量级锁->重量级锁。
- 无锁:这是没有开启偏向锁的时候的状态,在JDK1.6之后偏向锁的默认开启的,但是有一个偏向延迟,需要在JVM启动之后的多少秒之后才能开启,这个可以通过JVM参数进行设置,同时是否开启偏向锁也可以通过JVM参数设置。
- 偏向锁:这个是在偏向锁开启之后的锁的状态,如果还没有一个线程拿到这个锁的话,这个状态叫做匿名偏向,当一个线程拿到偏向锁的时候,下次想要竞争锁只需要拿线程ID跟MarkWord当中存储的线程ID进行比较,如果线程ID相同则直接获取锁(相当于锁偏向于这个线程),不需要进行CAS操作和将线程挂起的操作。
- 轻量级锁:在这个状态下线程主要是通过CAS操作实现的。将对象的MarkWord存储到线程的虚拟机栈上,然后通过CAS将对象的MarkWord的内容设置为指向Displaced Mark Word的指针,如果设置成功则获取锁。在线程出临界区的时候,也需要使用CAS,如果使用CAS替换成功则同步成功,如果失败表示有其他线程在获取锁,那么就需要在释放锁之后将被挂起的线程唤醒。
- 重量级锁:当有两个以上的线程获取锁的时候轻量级锁就会升级为重量级锁,因为CAS如果没有成功的话始终都在自旋,进行while循环操作,这是非常消耗CPU的,但是在升级为重量级锁之后,线程会被操作系统调度然后挂起,这可以节约CPU资源。
了解完 4 种锁状态之后,我们就可以整体的来看一下锁升级的过程了。
线程A进入 synchronized 开始抢锁,JVM 会判断当前是否是偏向锁的状态,如果是就会根据 Mark Word 中存储的线程 ID 来判断,当前线程A是否就是持有偏向锁的线程。如果是,则忽略 check,线程A直接执行临界区内的代码。
但如果 Mark Word 里的线程不是线程 A,就会通过自旋尝试获取锁,如果获取到了,就将 Mark Word 中的线程 ID 改为自己的;如果竞争失败,就会立马撤销偏向锁,膨胀为轻量级锁。
后续的竞争线程都会通过自旋来尝试获取锁,如果自旋成功那么锁的状态仍然是轻量级锁。然而如果竞争失败,锁会膨胀为重量级锁,后续等待的竞争的线程都会被阻塞。
#JVM对Synchornized的优化?
synchronized 核心优化方案主要包含以下 4 个:
- 锁膨胀:synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级。JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态,而转换的效率是比较低的。但有了锁膨胀机制之后,synchronized 的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了 synchronized 的性能。
- 锁消除:指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。
- 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
- 自适应自旋锁:指通过自身循环,尝试获取锁的一种方式,优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。
#介绍一下AQS
AQS全称为AbstractQueuedSynchronizer,是Java中的一个抽象类。 AQS是一个用于构建锁、同步器、协作工具类的工具类(框架)。
AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
主要原理图如下:
AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。
AQS广泛用于控制并发流程的类,如下图:
其中Sync
是这些类中都有的内部类,其结构如下:
可以看到:Sync
是AQS
的实现。 AQS
主要完成的任务:
- 同步状态(比如说计数器)的原子性管理;
- 线程的阻塞和解除阻塞;
- 队列的管理。
AQS原理
AQS最核心的就是三大部分:
- 状态:state;
- 控制线程抢锁和配合的FIFO队列(双向链表);
- 期望协作工具类去实现的获取/释放等重要方法(重写)。
状态state
- 这里state的具体含义,会根据具体实现类的不同而不同:比如在Semapore里,他表示剩余许可证的数量;在CountDownLatch里,它表示还需要倒数的数量;在ReentrantLock中,state用来表示“锁”的占有情况,包括可重入计数,当state的值为0的时候,标识该Lock不被任何线程所占有。
- state是volatile修饰的,并被并发修改,所以修改state的方法都需要保证线程安全,比如getState、setState以及compareAndSetState操作来读取和更新这个状态。这些方法都依赖于unsafe类。
FIFO队列
- 这个队列用来存放“等待的线程,AQS就是“排队管理器”,当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁。
- AQS会维护一个等待的线程队列,把线程都放到这个队列里,这个队列是双向链表形式。
实现获取/释放等方法
- 这里的获取和释放方法,是利用AQS的协作工具类里最重要的方法,是由协作类自己去实现的,并且含义各不相同;
- 获取方法:获取操作会以来state变量,经常会阻塞(比如获取不到锁的时候)。在Semaphore中,获取就是acquire方法,作用是获取一个许可证; 而在CountDownLatch里面,获取就是await方法,作用是等待,直到倒数结束;
- 释放方法:在Semaphore中,释放就是release方法,作用是释放一个许可证; 在CountDownLatch里面,获取就是countDown方法,作用是将倒数的数减一;
- 需要每个实现类重写tryAcquire和tryRelease等方法。
#CAS 和 AQS 有什么关系?
CAS 和 AQS 两者的区别:
- CAS 是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期值(A)和新值(B)。CAS 操作的逻辑是,如果内存位置 V 的值等于预期值 A,则将其更新为新值 B,否则不做任何操作。整个过程是原子性的,通常由硬件指令支持,如在现代处理器上,
cmpxchg
指令可以实现 CAS 操作。 - AQS 是一个用于构建锁和同步器的框架,许多同步器如
ReentrantLock
、Semaphore
、CountDownLatch
等都是基于 AQS 构建的。AQS 使用一个volatile
的整数变量state
来表示同步状态,通过内置的FIFO
队列来管理等待线程。它提供了一些基本的操作,如acquire
(获取资源)和release
(释放资源),这些操作会修改state
的值,并根据state
的值来判断线程是否可以获取或释放资源。AQS 的acquire
操作通常会先尝试获取资源,如果失败,线程将被添加到等待队列中,并阻塞等待。release
操作会释放资源,并唤醒等待队列中的线程。
CAS 和 AQS 两者的联系:
- CAS 为 AQS 提供原子操作支持:AQS 内部使用 CAS 操作来更新
state
变量,以实现线程安全的状态修改。在acquire
操作中,当线程尝试获取资源时,会使用 CAS 操作尝试将state
从一个值更新为另一个值,如果更新失败,说明资源已被占用,线程会进入等待队列。在release
操作中,当线程释放资源时,也会使用 CAS 操作将state
恢复到相应的值,以保证状态更新的原子性。
#如何用 AQS 实现一个可重入的公平锁?
AQS 实现一个可重入的公平锁的详细步骤:
- 继承 AbstractQueuedSynchronizer:创建一个内部类继承自
AbstractQueuedSynchronizer
,重写tryAcquire
、tryRelease
、isHeldExclusively
等方法,这些方法将用于实现锁的获取、释放和判断锁是否被当前线程持有。 - 实现可重入逻辑:在
tryAcquire
方法中,检查当前线程是否已经持有锁,如果是,则增加锁的持有次数(通过state
变量);如果不是,尝试使用 CAS操作来获取锁。 - 实现公平性:在
tryAcquire
方法中,按照队列顺序来获取锁,即先检查等待队列中是否有线程在等待,如果有,当前线程必须进入队列等待,而不是直接竞争锁。 - 创建锁的外部类:创建一个外部类,内部持有
AbstractQueuedSynchronizer
的子类对象,并提供lock
和unlock
方法,这些方法将调用AbstractQueuedSynchronizer
子类中的方法。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;public class FairReentrantLock {private static class Sync extends AbstractQueuedSynchronizer {// 判断锁是否被当前线程持有protected boolean isHeldExclusively() {return getExclusiveOwnerThread() == Thread.currentThread();}// 尝试获取锁protected boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 公平性检查:检查队列中是否有前驱节点,如果有,则当前线程不能获取锁if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) {// 可重入逻辑:如果是当前线程持有锁,则增加持有次数int nextc = c + acquires;if (nextc < 0) {throw new Error("Maximum lock count exceeded");}setState(nextc);return true;}return false;}// 尝试释放锁protected boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread()!= getExclusiveOwnerThread()) {throw new IllegalMonitorStateException();}boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}// 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现ConditionObject newCondition() {return new ConditionObject();}}private final Sync sync = new Sync();// 加锁方法public void lock() {sync.acquire(1);}// 解锁方法public void unlock() {sync.release(1);}// 判断当前线程是否持有锁public boolean isLocked() {return sync.isHeldExclusively();}// 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现public Condition newCondition() {return sync.newCondition();}
}
代码解释:
内部类 Sync:
-
isHeldExclusively
:使用getExclusiveOwnerThread
方法检查当前锁是否被当前线程持有。 -
tryAcquire
:-
首先获取当前锁的状态
c
。 -
如果
c
为 0,表示锁未被持有,此时进行公平性检查,通过hasQueuedPredecessors
检查是否有前驱节点在等待队列中。如果没有,使用compareAndSetState
尝试将状态设置为acquires
(通常为 1),并设置当前线程为锁的持有线程。 -
如果
c
不为 0,说明锁已被持有,检查是否为当前线程持有。如果是,增加锁的持有次数(可重入),但要防止溢出。
-
-
tryRelease
:-
先将状态减
releases
(通常为 1)。 -
检查当前线程是否为锁的持有线程,如果不是,抛出异常。
-
如果状态减为 0,说明锁被完全释放,将持有线程设为
null
。
-
-
newCondition
:创建一个ConditionObject
用于更复杂的同步操作,如等待 / 通知机制。
外部类 FairReentrantLock:
lock
方法:调用sync.acquire(1)
尝试获取锁。unlock
方法:调用sync.release(1)
释放锁。isLocked
方法:调用sync.isHeldExclusively
判断锁是否被当前线程持有。newCondition
方法:调用sync.newCondition
提供条件变量。
#Threadlocal作用,原理,具体里面存的key value是啥,会有什么问题,如何解决?
ThreadLocal
是Java中用于解决线程安全问题的一种机制,它允许创建线程局部变量,即每个线程都有自己独立的变量副本,从而避免了线程间的资源共享和同步问题。
从内存结构图,我们可以看到:
- Thread类中,有个ThreadLocal.ThreadLocalMap 的成员变量。
- ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。
ThreadLocal的作用
- 线程隔离:
ThreadLocal
为每个线程提供了独立的变量副本,这意味着线程之间不会相互影响,可以安全地在多线程环境中使用这些变量而不必担心数据竞争或同步问题。 - 降低耦合度:在同一个线程内的多个函数或组件之间,使用
ThreadLocal
可以减少参数的传递,降低代码之间的耦合度,使代码更加清晰和模块化。 - 性能优势:由于
ThreadLocal
避免了线程间的同步开销,所以在大量线程并发执行时,相比传统的锁机制,它可以提供更好的性能。
ThreadLocal的原理
ThreadLocal
的实现依赖于Thread
类中的一个ThreadLocalMap
字段,这是一个存储ThreadLocal
变量本身和对应值的映射。每个线程都有自己的ThreadLocalMap
实例,用于存储该线程所持有的所有ThreadLocal
变量的值。
当你创建一个ThreadLocal
变量时,它实际上就是一个ThreadLocal
对象的实例。每个ThreadLocal
对象都可以存储任意类型的值,这个值对每个线程来说是独立的。
-
当调用
ThreadLocal
的get()
方法时,ThreadLocal
会检查当前线程的ThreadLocalMap
中是否有与之关联的值。 -
如果有,返回该值;
-
如果没有,会调用
initialValue()
方法(如果重写了的话)来初始化该值,然后将其放入ThreadLocalMap
中并返回。 -
当调用
set()
方法时,ThreadLocal
会将给定的值与当前线程关联起来,即在当前线程的ThreadLocalMap
中存储一个键值对,键是ThreadLocal
对象自身,值是传入的值。 -
当调用
remove()
方法时,会从当前线程的ThreadLocalMap
中移除与该ThreadLocal
对象关联的条目。
可能存在的问题
当一个线程结束时,其ThreadLocalMap
也会随之销毁,但是ThreadLocal
对象本身不会立即被垃圾回收,直到没有其他引用指向它为止。
因此,在使用ThreadLocal
时需要注意,如果不显式调用remove()
方法,或者线程结束时未正确清理ThreadLocal
变量,可能会导致内存泄漏,因为ThreadLocalMap
会持续持有ThreadLocal
变量的引用,即使这些变量不再被其他地方引用。
因此,实际应用中需要在使用完ThreadLocal
变量后调用remove()
方法释放资源。
#悲观锁和乐观锁的区别?
- 乐观锁: 就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总 是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
- 悲观锁: 还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总 是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,不管三七二十一,直接上了锁就操作资源了。
#Java中想实现一个乐观锁,都有哪些方式?
- CAS(Compare and Swap)操作: CAS 是乐观锁的基础。Java 提供了 java.util.concurrent.atomic 包,包含各种原子变量类(如 AtomicInteger、AtomicLong),这些类使用 CAS 操作实现了线程安全的原子操作,可以用来实现乐观锁。
- 版本号控制:增加一个版本号字段记录数据更新时候的版本,每次更新时递增版本号。在更新数据时,同时比较版本号,若当前版本号和更新前获取的版本号一致,则更新成功,否则失败。
- 时间戳:使用时间戳记录数据的更新时间,在更新数据时,在比较时间戳。如果当前时间戳大于数据的时间戳,则说明数据已经被其他线程更新,更新失败。
#CAS 有什么缺点?
CAS的缺点主要有3点:
- ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
- 循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
- 只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。
#为什么不能所有的锁都用CAS?
CAS操作是基于循环重试的机制,如果CAS操作一直未能成功,线程会一直自旋重试,占用CPU资源。在高并发情况下,大量线程自旋会导致CPU资源浪费。
#CAS 有什么问题,Java是怎么解决的?
会有 ABA 的问题,变量值在操作过程中先被其他线程从 A 修改为 B,又被改回 A,CAS 无法感知中途变化,导致操作误判为“未变更”。比如:
- 线程1读取变量为
A
,准备改为C
。 - 此时线程2将变量
A
→B
→A
。 - 线程1的CAS执行时发现仍是
A
,但状态已丢失中间变化。
Java 提供的工具类会在 CAS 操作中增加版本号(Stamp)或标记,每次修改都更新版本号,使得即使值相同也能识别变更历史。比如,可以用 AtomicStampedReference 来解决 ABA 问题,通过比对值和版本号识别ABA问题。
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);// 尝试修改值并更新版本号
boolean success = ref.compareAndSet(100, 200, 0, 1);
// 前提:当前值=100 且 版本号=0,才会更新为(200,1)
#voliatle关键字有什么作用?
volatite作用有 2 个:
-
保证变量对所有线程的可见性。当一个变量被声明为volatile时,它会保证对这个变量的写操作会立即刷新到主存中,而对这个变量的读操作会直接从主存中读取,从而确保了多线程环境下对该变量访问的可见性。这意味着一个线程修改了volatile变量的值,其他线程能够立刻看到这个修改,不会受到各自线程工作内存的影响。
-
禁止指令重排序优化。volatile关键字在Java中主要通过内存屏障来禁止特定类型的指令重排序。
-
1)写-写(Write-Write)屏障:在对volatile变量执行写操作之前,会插入一个写屏障。这确保了在该变量写操作之前的所有普通写操作都已完成,防止了这些写操作被移到volatile写操作之后。
-
2)读-写(Read-Write)屏障:在对volatile变量执行读操作之后,会插入一个读屏障。它确保了对volatile变量的读操作之后的所有普通读操作都不会被提前到volatile读之前执行,保证了读取到的数据是最新的。
-
3)写-读(Write-Read)屏障:这是最重要的一个屏障,它发生在volatile写之后和volatile读之前。这个屏障确保了volatile写操作之前的所有内存操作(包括写操作)都不会被重排序到volatile读之后,同时也确保了volatile读操作之后的所有内存操作(包括读操作)都不会被重排序到volatile写之前。
-
#指令重排序的原理是什么?
在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,但是重排序要满足下面 2 个条件才能进行:
- 在单线程环境下不能改变程序运行的结果
- 存在数据依赖关系的不允许重排序。
所以重排序不会对单线程有影响,只会破坏多线程的执行语义。
我们看这个例子,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面,如果C排到A和B的前面,那么程序的结果将会被改变。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
#volatile可以保证线程安全吗?
volatile关键字可以保证可见性,但不能保证原子性,因此不能完全保证线程安全。volatile关键字用于修饰变量,当一个线程修改了volatile修饰的变量的值,其他线程能够立即看到最新的值,从而避免了线程之间的数据不一致。
但是,volatile并不能解决多线程并发下的复合操作问题,比如i++这种操作不是原子操作,如果多个线程同时对i进行自增操作,volatile不能保证线程安全。对于复合操作,需要使用synchronized关键字或者Lock来保证原子性和线程安全。
#volatile和sychronized比较?
Synchronized解决了多线程访问共享资源时可能出现的竞态条件和数据不一致的问题,保证了线程安全性。Volatile解决了变量在多线程环境下的可见性和有序性问题,确保了变量的修改对其他线程是可见的。
- Synchronized: Synchronized是一种排他性的同步机制,保证了多个线程访问共享资源时的互斥性,即同一时刻只允许一个线程访问共享资源。通过对代码块或方法添加Synchronized关键字来实现同步。
- Volatile: Volatile是一种轻量级的同步机制,用来保证变量的可见性和禁止指令重排序。当一个变量被声明为Volatile时,线程在读取该变量时会直接从内存中读取,而不会使用缓存,同时对该变量的写操作会立即刷回主内存,而不是缓存在本地内存中。
#什么是公平锁和非公平锁?
- 公平锁: 指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点在于各个线程公平平等,每个线程等待一段时间后,都有执行的机会,而它的缺点就在于整体执行速度更慢,吞吐量更小。
- 非公平锁: 多个线程加锁时直接尝试获取锁,能抢到锁到直接占有锁,抢不到才会到等待队列的队尾等待。非公平锁的优势就在于整体执行速度更快,吞吐量更大,但同时也可能产生线程饥饿问题,也就是说如果一直有线程插队,那么在等待队列中的线程可能长时间得不到运行。
#非公平锁吞吐量为什么比公平锁大?
- 公平锁执行流程:获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
- 非公平锁执行流程:当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。
#Synchronized是公平锁吗?
Synchronized不属于公平锁,ReentrantLock是公平锁。
#ReentrantLock是怎么实现公平锁的?
我们来看一下公平锁与非公平锁的加锁方法的源码。公平锁的锁获取源码如下:
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() && //这里判断了 hasQueuedPredecessors()compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) {throw new Error("Maximum lock count exceeded");}setState(nextc);return true;}return false;
}
非公平锁的锁获取源码如下:
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) { //这里没有判断 hasQueuedPredecessors()setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
通过对比,我们可以明显的看出公平锁与非公平锁的 lock() 方法唯一的区别就在于公平锁在获取锁时多了一个限制条件:hasQueuedPredecessors() 为 false,这个方法就是判断在等待队列中是否已经有线程在排队了。
这也就是公平锁和非公平锁的核心区别,如果是公平锁,那么一旦已经有线程在排队了,当前线程就不再尝试获取锁;对于非公平锁而言,无论是否已经有线程在排队,都会尝试获取一下锁,获取不到的话,再去排队。这里有一个特例需要我们注意,针对 tryLock() 方法,它不遵守设定的公平原则。
例如,当有线程执行 tryLock() 方法的时候,一旦有线程释放了锁,那么这个正在 tryLock 的线程就能获取到锁,即使设置的是公平锁模式,即使在它之前已经有其他正在等待队列中等待的线程,简单地说就是 tryLock 可以插队。
看它的源码就会发现:
public boolean tryLock() {return sync.nonfairTryAcquire(1);}
这里调用的就是 nonfairTryAcquire(),表明了是不公平的,和锁本身是否是公平锁无关。综上所述,公平锁就是会按照多个线程申请锁的顺序来获取锁,从而实现公平的特性。
非公平锁加锁时不考虑排队等待情况,直接尝试获取锁,所以存在后申请却先获得锁的情况,但由此也提高了整体的效率。
#什么情况会产生死锁问题?如何解决?
死锁只有同时满足以下四个条件才会发生:
- 互斥条件:互斥条件是指多个线程不能同时使用同一个资源。
- 持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
- 不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
- 环路等待条件:环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。
例如,线程 A 持有资源 R1 并试图获取资源 R2,而线程 B 持有资源 R2 并试图获取资源 R1,此时两个线程相互等待对方释放资源,从而导致死锁。
public class DeadlockExample {private static final Object resource1 = new Object();private static final Object resource2 = new Object();public static void main(String[] args) {Thread threadA = new Thread(() -> {synchronized (resource1) {System.out.println("Thread A acquired resource1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (resource2) {System.out.println("Thread A acquired resource2");}}});Thread threadB = new Thread(() -> {synchronized (resource2) {System.out.println("Thread B acquired resource2");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (resource1) {System.out.println("Thread B acquired resource1");}}});threadA.start();threadB.start();}
}
避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。
那什么是资源有序分配法呢?线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
相关文章:
[java八股文][Java并发编程面试篇]并发安全
juc包下你常用的类? 线程池相关: ThreadPoolExecutor:最核心的线程池类,用于创建和管理线程池。通过它可以灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足不同的并发处理需求。Exe…...
【东枫科技】代理英伟达产品:智能网卡
文章目录 对比详细:NVIDIA ConnectX-7 适配器详细:NVIDIA ConnectX-6 Lx 以太网智能网卡详细:NVIDIA ConnectX-6 Dx 以太网智能网卡详细:NVIDIA ConnectX-6 InfiniBand 适配器 对比 详细:NVIDIA ConnectX-7 适配器 为最…...
eNSP中路由器OSPF协议配置完整实验和命令解释
本实验使用三台华为路由器(R1、R2和R3)相连,配置OSPF协议实现网络互通。拓扑结构如下: 实验IP规划 R1: GE0/0/0: 192.168.12.1/24 (Area 0)Loopback0: 1.1.1.1/32 (Area 0) R2: GE0/0/0: 192.168.12.2/24 (Area 0)GE0/0/1: 192.…...
解锁健康生活:全新养身指南
健康养身不是遥不可及的目标,而是由一个个小习惯编织成的生活方式。当我们将这些健康理念融入日常,就能为身体注入源源不断的活力。 从 “吃” 开始守护健康。尝试制作 “营养碗”,底层铺满羽衣甘蓝、生菜等绿叶蔬菜,中间搭配水…...
win11 怎样把D盘空间分给C盘一点
如下所示,我的C盘甚至已经爆红了,打算D盘清理一些空间给C盘。 首先附上链接,这是我在b站看的教程,虽然跟着视频没成功,但是结合评论区大神们的建议,尝试了好几种方法,最终自己摸索成功了。 【怎…...
Apache Doris与StarRocks对比
## 历史背景 Apache Doris源自百度的Palo项目,于2017年开源,2018年贡献给Apache基金会,并于2022年从Apache孵化器毕业成为顶级项目。StarRocks则是由原Apache Doris团队的一部分成员在2020年分支出来成立的独立项目,最初称为DorisDB,后更名为StarRocks。这两个项目虽然有…...
OSCP - Proving Grounds - NoName
主要知识点 linux命令注入SUID find提权 具体步骤 从nmap开始搜集信息,只开放了一个80端口 Nmap scan report for 192.168.171.15 Host is up (0.40s latency). Not shown: 65534 closed tcp ports (reset) PORT STATE SERVICE VERSION 80/tcp open http …...
2025年OpenAI重大架构调整:资本与使命的再平衡
目录 前言 一、调整核心:三重架构的重构 1.1 控制权的重新锚定 1.2 营利部门的角色转型 1.3 资金池的重新配置 二、调整动因:三重矛盾的破解 2.1 资金需求与融资限制的冲突 2.2 商业竞争与使命纯度的博弈 2.3 内部治理与外部监管的张力 三、产…...
【quantity】0 README.md文件
PhysUnits 物理单位库 Type-safe physical quantities with dimensional analysis 带量纲分析的类型安全物理量库 A Rust library for safe unit operations / Rust实现的类型安全单位计算库 Core Design / 核心设计 1. Dimension / 量纲 /// Base SI dimensions / 国际单…...
[python] str
一、移除字符串中所有非字母数字字符 使用正则表达式 import re string_value "alphanumeric123__" cleaned_string re.sub(r[\W_], , string_value) # 或 r[^a-zA-Z0-9] print(cleaned_string) # 输出: alphanumeric123使用**str.isalnum()**方法 string_v…...
iOS与HTTPS抓包调试小结
最近在做一个多端 SDK 网络请求兼容性的测试,期间遇到一些 HTTPS 请求抓不到、iOS 抓包失效等问题,趁机整理一下我平时抓包时用到的几个工具和技巧,也顺便记录一下对比体验。 一、传统工具的局限 最早用的是 Charles 和 Fiddler,…...
AI基础知识(02):机器学习的任务类型、学习方式、工作流程
03 机器学习(Machine Learning)的任务类型与学习方式 广义的机器学习主要是一个研究如何让计算机通过数据学习规律,并利用这些规律进行预测和决策的过程。这里的Machine并非物理意义上的机器,可以理解为计算机软硬件组织;Learning可以理解为一个系统或平台经历了某些过程…...
2025年大风灾害预警升级!疾风气象大模型如何筑起安全防线?
近年来,全球极端天气事件频发,大风灾害正成为威胁城市安全、交通运输和公共设施的重要隐患。据气象部门预测,2025年我国大风天气将更加频繁,局部地区可能出现超强阵风,对高空作业、电力设施、交通运输等领域构成严峻挑战。面对这一趋势,传统的气象预警方式已难以满足精准…...
Docker手动重构Nginx镜像,融入Lua、Redis功能
核心内容:Docker重构Nginx镜像,融入Lua、Redis功能 文章目录 前言一、准备工作1、说明2、下载模块3、Nginx配置文件3、Dockerfile配置文件3、准备工作全部结束 二、构建镜像三、基于镜像创建容器三、lua脚本的redis功能使用总结 前言 …...
Spring Boot Starter简介-笔记
1. Starter简介 Spring Boot Starter 是 Spring Boot 框架的核心组件之一,它通过预定义的依赖集合和自动化配置机制,极大简化了 Spring 应用的开发和部署。 Spring Boot Starter 的核心功能 自动化配置(Auto-Configuration) Spr…...
关系型数据库与非关系型数据库深度对比:从设计哲学到应用场景的全解析
关系型数据库与非关系型数据库深度对比:从设计哲学到应用场景的全解析 引言 在数字化浪潮中,数据库技术始终扮演着基础核心角色。本文将通过技术架构、应用场景等维度,深入剖析关系型数据库(RDBMS)与非关系型数据库(NoSQL)的本质差异。我们将以MySQL、MongoDB、Redis等…...
论文速读:《CoM:从多模态人类视频中学习机器人操作,助力视觉语言模型推理与执行》
论文链接:https://arxiv.org/pdf/2504.13351 项目链接:https://chain-of-modality.github.io/ 0. 简介 现代机器人教学的一个重要方向是让机器人通过观看人类的视频演示,自动学习并执行复杂的物理操作任务,比如拧瓶盖、插插头、打…...
系统思考:选择大于努力
在今年的伯克希尔哈撒韦股东大会上,94岁高龄的股神巴菲特再次以他的智慧和幽默,给年轻人留下了三句关于人生的黄金建议。让我印象最深刻的是:“选择和谁一起走,比怎么走更重要。” 这一句话让我反思了许多——人生的轨迹不单单是…...
【HTML5】显示-隐藏法 实现网页轮播图效果
【HTML5】显示-隐藏法 实现网页轮播图效果 实现思路:先将所有图片在页面中设置好,然后给放置图片的元素li添加display:none属性将其隐藏,然后通过js获取到放置图片的元素li,再一个一个的给li元素添加displayÿ…...
Jenkins 改完端口号启动不起来了
让我们将 Jenkins 恢复到默认的 8080 端口,确保它能正常启动: 1. 修改 Jenkins 的配置文件: sudo nano /etc/default/jenkins 将内容修改为: HTTP_PORT8080 JENKINS_ARGS"--webroot/var/cache/jenkins/war --httpPort8080…...
招标专家随机抽选——设计讲解—未来之窗智能编程——仙盟创梦IDE
招标专家系统 专家评标系统是服务于各类招标评标活动的数字化平台。它依托先进信息技术,集专家库管理、随机抽取专家、在线评标等功能于一体。系统依据项目需求设定筛选条件,从庞大专家库中精准抽取合适专家。评标时,专家可在线查阅投标文件…...
Os 库报错指南 路径处理常见陷阱
平台分隔符差异 Windows用\,Unix用/ → 使用os.path.join()自动处理 路径解析错误 os.path.abspath()解析相对路径时依赖当前工作目录 路径规范化缺失 ../等符号链接需用os.path.normpath()规范化 # 不推荐 path dir\\file.txt # Windows风格 path dir/file.…...
GD32/STM32 ADC/DMA使用指南
首先我们对ADC及DMA的基础知识作一下简单介绍。 一、 GD32/STM32 ADC模块的核心要点 一)、ADC基础特性 12位逐次逼近型 GD32/STM32 ADC为12位分辨率,最大量化值为4095(对应3.3V参考电压),支持0-3.3V模拟输入范…...
CSS Border 三角形阴影与多重边框的制作
CSS Border 三角形阴影与多重边框的制作 在现代网页设计中,CSS的强大功能让设计师和开发者能够创造出丰富多彩的视觉效果。本文将深入探讨如何利用CSS的border属性制作三角形阴影以及多重边框效果。这些技巧不仅能提升页面的美观度,还能增强用户体验。…...
ES6/ES11知识点 续五
迭代器【Iterator】 ES6 中的**迭代器(Iterator)**是 JavaScript 的一种协议,它定义了对象如何被逐个访问。迭代器与 for…of、扩展运算符、解构赋值等语法密切相关。 📘 迭代器工作原理 ES6 迭代器的工作原理基于两个核心机制…...
如何选择 边缘计算服务器
边缘计算服务器选型指南(2025年更新版) 一、明确应用场景需求 场景细分 工业控制、自动驾驶等需毫秒级响应的场景,优先选择集成多核处理器(如Xeon D系列)和实时算法加速模块的机型,确保延迟≤50ms&…...
VMware如何安装?Ubuntu详细步骤
VMware如何安装?Ubuntu详细步骤如下: 在VMware中安装Ubuntu是一个常见的操作,适用于开发、测试或学习Linux的场景。以下是详细的实战步骤和注意事项,帮助你顺利完成安装。 准备工作 软件下载: VMware Workstation/Play…...
【Bootstrap V4系列】学习入门教程之 组件-卡片(Card)高级用法
【Bootstrap V4系列】学习入门教程之 组件-卡片(Card)高级用法 一、Sizing 尺寸1.1 Using grid markup 使用网格标记1.2 Using utilities 使用实用程序1.3 Using custom CSS 使用自定义CSS 二、Text alignment 文本对齐方式三、Navigation 导航 一、Sizi…...
WiFi那些事儿(八)——802.11n
目录 802.11n 技术简介与测试项 一、802.11n 技术简介 (一)标准概述 (二)关键技术特性 1. MIMO(多输入多输出)技术 2. 信道绑定(Channel Bonding) 3. 帧聚合(Fram…...
Transformer数学推导——Q56 推导动态残差门控(Dynamic Residual Gating)的权重更新公式
该问题归类到Transformer架构问题集——残差与归一化——残差连接。请参考LLM数学推导——Transformer架构问题集。 1. 引言 在深度学习的演进历程中,网络结构的创新始终围绕着如何更高效地处理信息、提升模型性能展开。动态残差门控(Dynamic Residual…...
Kaggle——House Prices(房屋价格预测)简单实现
题目: 从Kaggle的“House Prices - Advanced Regression Techniques”数据集使用Pandas读取数据,并查看数据的基本信息。选择一些你认为对房屋价格有重要影响的特征,并进行数据预处理(如缺失值处理、异常值处理等)。…...
Vue项目Git提交流程集成
Vue项目Git提交流程集成 本教程将指导你如何在Vue项目中集成一个规范化的Git提交流程,包括代码规范检查、提交信息规范和自动化工具配置。 前置条件 Node.js 14.0 和 npm/yarn/pnpmVue项目(Vue 2或Vue 3均可)Git已初始化的仓库 一、规范化…...
使用 OpenSSL 吊销 Kubernetes(k8s)的 kubeconfig 里的用户证书
一.用 OpenSSL 依据已有的自签名 CA 注销签发的证书的步骤 1. 准备工作 你得有自签名 CA 的私钥(通常是 .key 文件)、CA 证书(通常是 .crt 文件)以及证书吊销列表(CRL)文件。若还没有 CRL 文件,…...
kubeadm部署k8s
我在阿里云上部署的k8s master 4c/8g/40g rocky linux8.9 node1/node2 2c/4g/40g rocky linux8.9 安装docker (我安装的是v1.19.1版本,是旧版本,可以装新版本,docker的版本和kubeadm,kubectl,kubelet版本相同) 1.所有…...
FPGA实战项目1——坦克大战
FPGA实战项目1——坦克大战 根据模块化思想,可将此任务简单的进行模块拆分: 系统原理,模块划分,硬件架构,算法支持,Verilog实现框架 一,系统总体原理 1. 核心设计思想 硬件并行处理&#x…...
LeetCode 1781. 所有子字符串美丽值之和 题解
示例 输入:s "aabcb" 输出:5 解释:美丽值不为零的字符串包括 ["aab","aabc","aabcb","abcb","bcb"] ,每一个字符串的美丽值都为 1这题光用文字解说还是无法达到讲…...
Spring Web MVC————入门(1)
今天开始正式带大家学习Spring部分的内容了,大家尝试去弄个专业版嗷,学习起来爽一点 在idea中下载这个插件就行了 我们之后开始创建Spring项目, 蓝色 部分自己起名,type选Maven,其他的默认就好了,之后nex…...
关于 js:1. 基础语法与核心概念
js 全称 JavaScript(简称 JS),是 一种运行在浏览器和服务器端的脚本语言。 用途: 浏览器端交互(如:点击按钮出现弹窗) 网页动态内容渲染(如:淘宝、京东页面更新…...
云境天合水陆安全漏电监测仪—迅速确定是否存在漏电现象
云境天合水陆安全漏电监测仪是一种专为水下及潮湿环境设计的电气安全检测设备,通过高灵敏度电磁传感器探测漏电电流产生的交变磁场,基于法拉第电磁感应定律,自动区分高灵敏度信号和低灵敏度信号,精准定位泄漏电源的具体位置。一旦…...
二、Hadoop狭义和广义的理解
作者:IvanCodes 日期:2025年5月6日🫠 专栏:Hadoop教程 Hadoop 的双重身份:核心框架与生态系统 在大数据领域,Hadoop 是一个广为人知的概念,但它并非单指某一个软件,而是涵盖了两个层…...
DTU_DTU厂家_5G/4G DTU终端_DTU模块_厦门计讯物联科技有限公司
在物联网蓬勃发展的当下,数据的高效、稳定、可靠的传输成为关键。厦门计讯物联科技有限公司(以下简称“计讯物联”)作为国内工业物联网领域的核心厂商,专注于5G/4G DTU终端、DTU模块及无线数传设备的研发与生产,致力于为智慧城市、能源电力、…...
学习alpha,第2个alpha
alphas (-1 * ts_corr(rank(ts_delta(log(volume), 2)), rank(((close - open) / open)), 6)) 先分析操作符从左到右 ts_corr: Pearson 相关度量两个变量之间的线性关系。当变量呈正态分布且关系呈线性时,它最有效。 ts_corr(vwap, close, 20)是一个计算时间序列相…...
如何用爬虫获得按关键字搜索淘宝商品
在电商领域,获取淘宝商品的详细信息对于市场分析、选品上架、库存管理和价格策略制定等方面至关重要。淘宝作为国内知名的电商平台,提供了丰富的商品资源。通过 Python 爬虫技术,我们可以高效地获取淘宝商品的详细信息,包括商品名…...
Android SDK 开发中的 AAR 与 JAR 区别详解
在 Android SDK 开发中,构建项目时我们常常会看到生成两个不同的文件:一个是 build/outputs/aar/*.aar,另一个是 build/intermediates/aar_main_jar/debug/syncDebugLibJars/classes.jar。很多初学者会疑惑:它们之间有什么区别&am…...
Python cv2滤波与模糊处理:从原理到实战
在图像处理领域,滤波与模糊是预处理阶段的两大核心操作,既能消除噪声干扰,又能实现艺术化效果。本文将结合OpenCV的cv2库,系统讲解滤波与模糊的原理及Python实现,带你从理论到实战全面掌握这项技术。 一、滤波与模糊的…...
【SpringBoot3】idea找不到log符号
解决idea找不到log符号,Slf4j注解不起作用 如图 解决办法 pom.xml文件里要手动添加版本号 插件也要添加对应的版本号 <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30&l…...
Android学习总结之Java和kotlin区别
一、空安全机制 真题 1:Kotlin 如何解决 Java 的 NullPointerException?对比两者在空安全上的设计差异 解析: 核心考点:Kotlin 可空类型系统(?)、安全操作符(?./?:)、非空断言&…...
C++笔记-二叉搜索树(包括key,key/value搜索场景等)
1.二叉搜索树的概念 二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树: 1.若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值若它的右子树不为空,则2.右子树上所有结点的值都大于等于根结点…...
【从零开始学习RabbitMQ | 第二篇】生成交换机到MQ的可靠性保障
目录 编辑前言 交换机 Direct交换机与Fanout交换机的差异 Topic交换机 Topic交换机相比Direct交换机的差异 生成我们的交换机,队列,以及绑定关系 基于代码去生成交换机和队列 基于注解去声明队列和交换机 消息转换器 消息队列的高可靠性 发送…...
在 Sheel 中运行 Spark:开启高效数据处理之旅
在大数据处理领域,Apache Spark 凭借其强大的分布式计算能力,成为了众多开发者和企业处理海量数据的首选工具之一。而 Sheel 作为一种便捷的运行环境,在其中运行 Spark 可以充分发挥两者优势,实现高效的数据处理与分析。本文将详细…...