sychronized
用来修饰方法、对象实例。属于独占锁、悲观锁、可重入锁、非公平锁,是Java中的一个关键字,修饰方法对象实例。
自动加锁与释放锁
JVM层面的锁
锁的是对象,锁信息保存在对象头中
底层有锁升级过程
Lock
是 Java 中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
vs synchronized
自动挡和手动挡的区别
- Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别
- Lock是一个接口,而 synchronized 是 Java 中的关键字, synchronized是内置的语言实现。
- synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
- Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- Lock 可以通过实现读写锁提高多个线程进行读操作的效率。
ReentrantLock
是JDK提供的一个类,继承了 Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
最主要特点:
需要手动加锁与释放锁
API层面的锁
int类型的state标识来标识锁的状态
没有锁升级过程
vs synchronized
相同点:
1.主要解决共享变量如何安全访问的问题
2.都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,
3.保证了线程安全的两大特性:可见性、原子性。
不同点:
1. ReentrantLock 就像手动汽车,需要显示的调用 lock 和 unlock 方法, synchronized 隐式获得释放锁。
2. ReentrantLock 可响应中断, synchronized 是不可以响应中断的,ReentrantLock 为处理锁的不可用性提供了更高的灵活性
3.ReentrantLock 是 API级别的, synchronized是JVM级别的
4. ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized是非公平锁,且不可更改。
5. ReentrantLock 通过 Condition 可以绑定多个条件
名词解释
CAS(比较并替换)
比较当前值(主内存中的值),与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,一样则更新,否则继续进行CAS操作。
乐观锁
乐观锁是一种乐观思想,假定当前环境是读多写少,遇到并发写的概率比较低,读数据时认为别的线程不会正在进行修改(所以没有上锁)。写数据时,判断当前 与期望值是否相同,如果相同则进行更新(更新期间加锁,保证是原子性的)。
悲观锁
只要操作就会上锁,类似饿汉思想
自旋锁
不改变内容,时刻判断自身的状态
自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
可重入锁
可重入锁是一种技术:任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。
可重入锁的原理:通过组合自定义同步器来实现锁的获取与释放。
● 再次获取锁:识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。获取锁后,进行计数自增
● 释放锁:释放锁时,进行计数自减。
Java 中的可重入锁:ReentrantLock、synchronized 修饰的方法或代码段。
可重入锁的作用:避免死锁。
面试题1: 可重入锁如果加了两把,但是只释放了一把会出现什么问题?
答:程序卡死,线程不能出来,也就是说我们申请了几把锁,就需要释放几把锁。
读写锁
读写锁是一种技术:通过 ReentrantReadWriteLock 类来实现。为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm 自己控制的。
写锁:只允许一个线程获取写锁,不允许同时访问同一个资源。
读锁:允许多个线程获取读锁,同时访问同一个资源。
共享锁
共享锁是一种思想: 可以有多个线程获取读锁,以共享的方式持有锁。和乐观锁、读写锁同义。
Java中用到的共享锁:ReentrantReadWriteLock
独占锁
独占锁是一种思想: 只能有一个线程获取锁,以独占的方式持有锁。和悲观锁、互斥锁同义。
Java 中用到的独占锁:synchronized, ReentrantLock
公平锁或非公平锁
公平锁和非公平锁的底层实现都会使用AQS来进行排队,它们的区别在于线程在使用lock()方法加锁时:
1.如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队
2.如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。
分段锁(Segmented Locking)
分段锁是一种并发控制机制,通常用于优化对共享资源的访问。它将共享资源划分为多个段(segment),每个段都有一个独立的锁来控制对该段的访问。这样做可以降低锁的粒度,减少了锁的竞争,从而提高了并发性能。分段锁常见于一些数据结构的实现中,比如 ConcurrentHashMap。
互斥锁(Mutex Lock)
互斥锁是一种最常见的锁机制,用于控制多个线程对共享资源的访问。当一个线程持有了互斥锁时,其他线程就无法同时持有相同的锁,只能等待该锁被释放。互斥锁可以确保在任意时刻只有一个线程能够访问共享资源,从而避免了竞争条件和数据不一致的问题。
同步锁(Synchronization Lock)
同步锁是一种用于实现线程同步的机制,通常指的是在 Java 中通过关键字 synchronized
来实现的锁。同步锁可以保证在同一时刻只有一个线程能够执行同步代码块或同步方法,从而保证了线程之间的协调和数据的一致性。
死锁(Deadlock)
死锁是指两个或多个线程相互等待对方持有的资源而无法继续执行的状态。简单来说,就是多个线程在互相等待对方释放资源,导致所有线程都无法继续执行。死锁是多线程编程中常见的问题之一,需要谨慎设计和管理锁的使用,以避免死锁的发生。常见的解决方法包括使用合适的锁顺序、加锁超时机制、死锁检测与恢复等。
锁升级
偏向锁(Biased Lock)
为了在无竞争的情况下提高性能而设计的一种锁机制。当一个线程获取了一个对象的锁并且在之后再次获取同一个锁时,偏向锁就会生效。这样,在之后这个线程再次获取锁时,就无需进行竞争,从而减少了性能开销。偏向锁的实现会在对象头中存储线程ID,用来标识持有锁的线程。如果有其他线程尝试获取同一个锁,偏向锁就会升级为轻量级锁或重量级锁。
在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了,也就是支持锁重入
轻量级锁
由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
重量级锁
如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
锁粗化(Lock Coarsening)
锁粗化是一种优化技术,用于减少锁操作的次数,从而提高性能。当虚拟机检测到一系列连续的操作都对同一个对象加锁和解锁时,它会将这些操作合并为一个范围更大的锁操作,从而减少了锁操作的次数。这样做可以减少由于频繁加锁和解锁而产生的性能开销,提高了程序的执行效率。锁粗化通常是由虚拟机自动进行的,而不需要用户手动介入。
锁消除(Lock Elimination)
锁消除是一种优化技术,用于在编译器级别去除不必要的锁操作,从而提高程序的性能。当编译器检测到一些锁操作在特定的情况下是不必要的时候,它会将这些锁操作消除掉,从而减少了锁的竞争和开销。例如,当编译器能够确定某个对象只会被单线程访问时,就可以安全地消除对该对象的锁操作。锁消除通常发生在即时编译(Just-In-Time Compilation,JIT)阶段,由编译器进行。