锁的面试相关
重入锁
锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) 。这些已经写好提供的锁为我们开发提供了便利。
重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁
读写锁
相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注也就是说读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
锁的类型
在Java中,锁主要用于实现线程同步,以确保多线程环境下对共享资源的安全访问。Java中的锁可以分为以下几种类型
-
内置锁(Intrinsic Locks/Synchronized Locks)
- 同步代码块使用
synchronized
关键字修饰的代码块,它使用对象本身作为锁。 - 同步方法使用
synchronized
关键字修饰的方法,如果是实例方法,使用对象实例作为锁;如果是静态方法,使用类的Class对象作为锁。
- 同步代码块使用
-
重入锁(Reentrant Lock)
- 是
java.util.concurrent.locks
包中提供的一种显式锁,需要手动获取和释放锁。 - 提供了比内置锁更丰富的操作,如尝试非阻塞获取锁(
tryLock
)、可中断获取锁(lockInterruptibly
)和支持超时的获取锁(tryLock(long timeout, TimeUnit unit)
)等。
- 是
-
读写锁(Read-Write Lock)
- 也是
java.util.concurrent.locks
包中提供的锁,具体实现为ReentrantReadWriteLock
。 - 分为读锁(
ReadLock
)和写锁(WriteLock
),允许多个线程同时读取,但写入时需要独占访问。
- 也是
-
条件锁(Condition Locks)
- 与
ReentrantLock
配合使用,通过ReentrantLock
的newCondition
方法创建Condition
对象。 - 可以让线程在特定条件下等待(
await
)或者在条件成立时被唤醒(signal
或signalAll
)。
- 与
-
乐观锁/悲观锁
- 不是具体的锁类型,而是一种锁的使用策略。
- 乐观锁通常使用版本号或CAS(比较并交换)操作来实现,假设没有冲突直接进行操作,如果检测到冲突则重试。
- 悲观锁假设会发生冲突,通常通过上锁来保护数据。
-
自旋锁(Spin Lock)
- 不是JDK直接提供的锁类型,而是一种可以自行实现的锁。
- 线程反复检查锁是否可用,而不是在获取不到锁时立即挂起,适用于锁等待时间非常短的场景。
-
分段锁(Segmented Lock)
- 通常用于实现并发集合,如
ConcurrentHashMap
。 - 将数据分为若干段,每段有自己的锁,不同段的数据可以并发处理。
- 通常用于实现并发集合,如
-
偏向锁、轻量级锁和重量级锁
- 这些是JVM为了优化锁的竞争而提供的锁状态,并不是独立的锁类型。
- 偏向锁是假设锁只会被一个线程频繁访问,轻量级锁是在无竞争时的优化,重量级锁则是传统的互斥锁。
-
StampedLock
- 提供了一种乐观的读锁定机制,也包含悲观读锁和写锁。
- 与
ReentrantReadWriteLock
相比,StampedLock
支持更复杂的操作,并且通常具有更好的性能,但不支持重入。
以上是Java中常见的锁类型和机制。不同的锁类型适用于不同的场景,开发者应根据实际需求选择合适的锁类型来确保数据的一致性和线程的安全。
锁的策略
乐观锁
总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。
version方式一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
核心SQL语句
update table set x=x+1, version=version+1 where id=#{id} and
version=#{version};
CAS操作方式即compare and swap 或者 compare and
set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。
悲观锁
总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。
独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java
ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。
锁的线程获取策略
公平锁
非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。 对于Java ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
锁的状态
无锁
偏向锁
轻量级锁
重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java
5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。