你知道有哪几种锁?分别有什么特点?
1.锁的7大分类
2.偏向锁/轻量级锁/重量级锁
偏向锁如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
轻量级锁JVM开发者发现在很多情况下,synchronized中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用CAS就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
重量级锁重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
3.可重入锁/非可重入锁
区别在于下次获取需不需要再次获取。
4.共享锁/独占锁
读写锁的读锁是共享锁,写锁是独占所。
5.公平锁/非公平锁
公平的含义在于如果线程现在拿不到这把锁那么线程就都会进入等待,开始排队在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。
非公平锁会在一定情况下忽略掉已经在排队的线程发生插队现象。
6.悲观锁/乐观锁
在获取资源之前必须先拿到锁,以便达到“独占”的状态当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我。
并不要求在获取资源前拿到锁也不会锁住资源相反,乐观锁利用CAS理念在不独占资源的情况下,完成了对资源的修改。
7.自旋锁/非自旋锁
如果线程现在拿不到锁,并不直接陷入阻塞或者释放CPU资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为自旋”,就像是线程在“自我旋转”自旋不会改变自己的状态。
没有自旋的过程如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。
8.可中断锁/不可中断锁????
ReentrantLock是一种典型的可中断锁例如使用lockInterruptibly方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情不需要一直傻等到获取到锁才离开。锁在执行时可被中断,也就是在执行时可以接收 interrupt 的通知,从而中断锁执行。
在Java中synchronized关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路,只能等到拿到锁以后才能进行其他的逻辑处理。
悲观锁和乐观锁的本质是什么?
1.悲观锁
会在每次获取并修改数据前,都把数据锁住让其他线程无法访问该数据,这样就可以确保数据内容万无一失
2.乐观锁
认为自己在操作资源的时候不会有其他线程来干扰所以并不会锁住被操作对象,不会不让别的线程来接触它同时为了确保数据正确性在更新前会去对比在我修改数据期间,数据有没有被其他线程修改过:如果没被修改过,就说明真的只有我自己在操作,那我就可以正常的修改数据如果发现数据和我一开始拿到的不一样了,说明其他线程在这段时间内修改过数据,那说明我迟了一步所以我会放弃这次修改,并选择报错、重试等策略。
3.典型案例
悲观锁:synchronized关键字和Lock接口
Java中悲观锁的实现包括synchronized关键字和Lock相关类等
例如Lock的实现类ReentrantLock,类中的lock)等方法就是执行加锁,而unlock)方法是执行解锁
处理资源之前必须要先加锁并拿到锁,等到处理完了之后再解开锁,这就是非常典型的悲观锁思想
·乐观锁:原子类
乐观锁的典型案例就是原子类
例如Atomiclnteger在更新数据时,就使用了乐观锁的思想,多个线程可以同时操作同一个原子变量
大喜大悲:数据库
数据库中同时拥有悲观锁和乐观锁的思想
例如,我们如果在MySQL选择select for update语句,那就是悲观锁,在提交之前不允许第三方来修改该数据,这当然会造成一定的性能损耗,在高并发的情况下是不可取的
4.“汝之蜜糖,彼之砒霜”
有一种说法认为悲观锁由于它的操作比较重量级,不能多个线程并行执行,而且还会有上下文切换等动作所以悲观锁的性能不如乐观锁好,!!应该尽量避免用悲观锁!!,这种说法是不正确的因为虽然悲观锁确实会让得不到锁的线程阻塞,但是这种开销是固定的悲观锁的原始开销确实要高于乐观锁,但是特点是一劳永逸,就算一直拿不到锁,也不会对开销造成额外的影响。
反观乐观锁虽然一开始的开销比悲观锁小但是如果一直拿不到锁,或者并发量大,竞争激烈,导致不停重试,那么消耗的资源也会越来越多,甚至开销会超过悲观锁所以,同样是悲观锁,在不同的场景下,效果可能完全不同,可能在今天的这种场景下是好的选择,在明天的另外的场景下就是坏的选择,这恰恰是“汝之蜜糖,彼之砒霜”。
5.两种锁各自的使用场景
悲观锁:适用于并发写入多临界区代码复杂竞争激烈等场景这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。
乐观锁:适用于大部分是读取少部分是修改的场景也适合虽然读写都很多但是并发并不激烈的场景在这些场景下,乐观锁不加锁的特点能让性能大幅提高。
4.3如何看到synchronized背后的“monitor锁”?
1.获取和释放monitor锁的时机
每个Java对象都可以用作一个实现同步的锁这个锁也被称为内置锁或monitor锁获得monitor锁的唯一途径就是进入由这个锁保护的同步代码块或同步方法线程在进入被synchronized保护的代码块之前,会自动获取锁并且无论是正常路径退出,还是通过抛出异常退出,在退出的时候都会自动释放锁。
2.用javap命令查看反汇编的结果
同步代码块
public class SynTest {
public void synBlock() {
synchronized (this) {
System.out.println("lagou");
}
}
}
首先用 cd 命令切换到 SynTest.java 类所在的路径,然后执行 javac SynTest.java,于是就会产生一个名为 SynTest.class 的字节码文件,
然后我们执行 javap -verbose SynTest.class,就可以看到对应的反汇编内容。
public void synBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String lagou
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
monitorenter 与monitorexit 就是获取锁与释放锁的动作,14:的monitorexit是同步代码块正常执行完毕。19:的monitorexit是执行同步代码块反生异常进行相关的动作。
- monitorenter
执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:
a. 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。
b. 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。
c. 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。
- monitorexit
monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。
同步方法
这个方法会有一个叫作 ACC_SYNCHRONIZED 的 flag 修饰符,来表明它是同步方法。
public synchronized void synMethod() {
}
public synchronized void synMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 16: 0
synchronized和Lock孰优孰劣,如何选择?
1.相同点
synchronized 和 Lock 都是用来保护资源线程安全
synchronized 和 ReentrantLock 都拥有可重入的特点
都可以保证可见性
2.不同点
- 用法区别
synchronized 关键字可以加在方法上,不需要指定锁对象(此时的锁对象为 this),也可以新建一个同步代码块并且自定义 monitor 锁对象;而 Lock 接口必须显示用 Lock 锁对象开始加锁 lock() 和解锁 unlock(),并且一般会在 finally 块中确保用 unlock() 来解锁,以防发生死锁。
与 Lock 显式的加锁和解锁不同的是 synchronized 的加解锁是隐式的,尤其是抛异常的时候也能保证释放锁,但是 Java 代码中并没有相关的体现。
- 加解锁顺序不同
lock1.lock();
lock2.lock();
...
lock1.unlock();
lock2.unlock();
synchronized(obj1){
synchronized(obj2){
...
}
}
synchronized 的嵌套顺序加解锁,不能自行控制
- synchronized 锁不够灵活
一旦 synchronized 锁已经被某个线程获得了,此时其他线程如果还想获得,那它只能被阻塞,直到持有锁的线程运行完毕或者发生异常从而释放这个锁。
相比之下,Lock 类在等锁的过程中,如果使用的是 lockInterruptibly 方法,那么如果觉得等待的时间太长了不想再继续等待,可以中断退出,也可以用 tryLock() 等方法尝试获取锁,如果获取不到锁也可以做别的事,更加灵活。
- synchronized 锁只能同时被一个线程拥有
例如在读写锁中的读锁,是可以同时被多个线程持有的。
- 原理区别
synchronized 是内置锁,由 JVM 实现获取锁和释放锁的原理,还分为偏向锁、轻量级锁、重量级锁。
Lock 根据实现不同,有不同的原理,例如 ReentrantLock 内部是通过 AQS 来获取和释放锁的。
- 是否可以设置公平/非公平
- 性能区别
在 Java 5 以及之前,synchronized 的性能比较低,但是到了 Java 6 以后,发生了变化,因为 JDK 对 synchronized 进行了很多优化,比如自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等,所以后期的 Java 版本里的 synchronized 的性能并不比 Lock 差。
3.如何选择
- 如果能不用最好既不使用 Lock 也不使用 synchronized。因为在许多情况下你可以使用 java.util.concurrent 包中的机制,它会为你处理所有的加锁和解锁操作,也就是推荐优先使用工具类来加解锁。
- 如果 synchronized 关键字适合你的程序, 那么请尽量使用它,这样可以减少编写代码的数量,减少出错的概率。因为一旦忘记在 finally 里 unlock,代码可能会出很大的问题,而使用 synchronized 更安全。
- 如果特别需要 Lock 的特殊功能,比如尝试获取锁、可中断、超时功能等,才使用 Lock。
Lock有哪几个常用方法?分别有什么用?
3.Iock()方法
lock 的加锁和释放锁都必须以代码的形式写出来,所以使用 lock() 时必须由我们自己主动去释放锁,因此最佳实践是执行 lock() 后,首先在 try{} 中操作同步资源,如果有必要就用 catch{} 块捕获异常,然后在 finally{} 中释放锁,以保证发生异常时锁一定被释放,示例代码如下所示。
Lock lock = ...;
lock.lock();
try{
//获取到了被本锁保护的资源,处理任务
//捕获异常
}finally{
lock.unlock(); //释放锁
}
4.tryLock()
因为该方法会立即返回,即便在拿不到锁时也不会一直等待,所以通常情况下,我们用 if 语句判断 tryLock() 的返回结果,根据是否获取到锁来执行不同的业务逻辑,典型使用方法如下
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则做其他事情
}
5.tryLock(long time,TimeUnit unit)
有一个超时时间,在拿不到锁时会等待一定的时间,如果在时间期限结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。
lockInterruptibly()
顾名思义,lockInterruptibly() 是可以响应中断的。相比于不能响应中断的 synchronized 锁,lockInterruptibly() 可以让程序更灵活,可以在获取锁的同时,保持对中断的响应。我们可以把这个方法理解为超时时间是无穷长的 tryLock(long time, TimeUnit unit),因为 tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 都能响应中断,只不过 lockInterruptibly() 永远不会超时。该方法会抛出异常然后抛出InterruptedException并清除当前线程的中断状态
7.unlock()
对于 ReentrantLock 而言,执行 unlock() 的时候,内部会把锁的“被持有计数器”减 1,直到减到 0 就代表当前这把锁已经完全释放了,如果减 1 后计数器不为 0,说明这把锁之前被“重入”了,那么锁并没有真正释放,仅仅是减少了持有的次数。
Lock有哪几个常用方法?分别有什么用?
1.什么是公平和非公平
/**
* 描述:演示公平锁,分别展示公平和不公平的情况,非公平锁会让现在持有锁的线程优先再次获取到锁。代码借鉴自Java并发编程实战手册2.7。
*/
public class FairAndUnfair {
public static void main(String args[]) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue), "Thread " + i);
}
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Job implements Runnable {
private PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
@Override
public void run() {
System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
printQueue.printJob(new Object());
System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
}
}
class PrintQueue {
private final Lock queueLock = new ReentrantLock(false);
public void printJob(Object document) {
queueLock.lock();
try {
Long duration = (long) (Math.random() * 10000);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",
Thread.currentThread().getName(), (duration / 1000));
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
queueLock.lock();
try {
Long duration = (long) (Math.random() * 10000);
System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",
Thread.currentThread().getName(), (duration / 1000));
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
查看ReentrantLock类的构造发现 传入的布尔值可控制是否为公平锁,下面有
FairSync 与 NonfairSync
FairSync 中比 NonfairSync 多了一行判断 hasQueuedPredecessors()
ReentrantReadWriteLock
同时存在 写锁与读锁竞争的时候 读锁会被禁用。
写锁,1.其他线程不占用该锁ReentrantReadWriteLock 时候可获取
2.在不释放锁的时候空重新获取锁
代码演示
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 描述: 演示读写锁用法
*/
public class ReadWriteLockDemo {
// 非公平策略创建
private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
false);
private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock
.readLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock
.writeLock();
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> read()).start();
new Thread(() -> read()).start();
new Thread(() -> write()).start();
new Thread(() -> write()).start();
}
}
ReentrantReadWriteLock的公平锁与非公平的的差异。
公平锁的读写都需要排
apparentlyFirstQueuedIsExclusive()显然,先排队是唯一的 先执行排队队列的写锁
什么是自旋锁?
对比自旋和非自旋的获取锁的流程
自旋锁用循环去不停地尝试获取锁让线程始终处于Runnable状态节省了线程状态切换带来的开销。
AtomicLongl的实现
AtomicLong.getAndIncrement()这里的 do-while 循环就是一个自旋操作,如果在修改过程中遇到了其他线程竞争导致没修改成功的情况,就会 while 循环里进行死循环,直到修改成功为止 自己实现一个可重入的自旋锁
package thread;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
/**
* 描述: 实现一个可重入的自旋锁
*/
public class ReentrantSpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
//重入次数
private int count = 0;
public void lock() {
// 获取当前线程的引用
Thread t = Thread.currentThread();
// 如果获取到了🔒 则返回
if (t == owner.get()) {
++count;
return;
}
//否则 利用CAS方法去尝试获得失败了就重试
while (!owner.compareAndSet(null, t)) {
System.out.println("自旋了");
}
}
public void unlock() {
Thread t = Thread.currentThread();
//只有持有锁的线程才能解锁
if (t == owner.get()) {
if (count > 0) {
--count;
} else {
//此处无需CAS操作,因为没有竞争,因为只有线程持有者才能解锁
owner.set(null);
}
}
}
public static void main(String[] args) {
ReentrantSpinLock spinLock = new ReentrantSpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
spinLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
缺点
1.在避免线程切换开销的同时也带来了新的开销
2.随着时间的增加,后期甚至会超过线程切换的开销
适用场景
并发度不是特别高的场景
临界区比较短小的情况
VM对锁进行了哪些优化
自适应的自旋锁
在JDK1.6中引入了自适应的自旋锁来解决长时间自旋的问题自适应意味着自旋的时间不再固定,而是会根据最近自旋尝试的成功率、失败率以及当前锁的拥有者的状态等多种因素来共同决定自旋的持续时间是变化的,自旋锁变“聪明”了 。
比如如果最近尝试自旋获取某一把锁成功了,那么下一次可能还会继续使用自旋,并且允许自旋更长的时间。但是如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便减少无用的自旋,提高效率。
偏向锁/轻量级锁/重量级锁
锁升级的路径
无锁→偏向锁→轻量级锁→重量级锁