最杂乱无章的一个知识点:锁

    • 请聊一聊你对锁的理解???
      • 杂乱无章分析之synchronized
      • synchronized优化升级
      • 锁之读写锁
      • 锁之乐观锁悲观锁
      • 杂乱无章分析之ReentrantLock锁
      • 重磅出击!!!AQS
      • 谈谈 synchronized和ReentrantLock 的区别

请聊一聊你对锁的理解???

这个问题是我在面试中遇到过的问题,这问题我第一次听起来感觉很扯,因此锁可以作用在很多方面,分为很多不同种类的锁。所以说面试到这个问题的时候我不知道从何说起,是从数据库?还是从线程?还是…?

杂乱无章分析之synchronized

在聊synchronized之前,我就先跟大家聊一下我们Java对象的构成:
在 JVM 中,对象在内存中分为三块区域:
1.对象头
Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
2.实例数据
这部分主要是存放类的数据信息,父类的信息
3.对其填充
由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐

重点来了:synchronized作用的范围不同,原理不同
作用在同步代码块上
最杂乱无章的一个知识点:锁_第1张图片
synchronized底层使用的monitor,我在最开始提到过对象头,他会关联到一个monitor对象。
当我们进入一个方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。
如果你已经是这个monitor的owner了,你再次进入,就会把进入数+1.
同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。
那么为什么会有两个monitorexit呢?
为了防止同步代码块中出现异常,锁可以被正确的释放。
作用在同步方法上
同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。
所以归根究底,还是monitor对象的争夺。
synchronized锁的是什么?
synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
synchronized 关键字加到实例方法上是给对象实例上锁。

synchronized优化升级

升级方向:
在这里插入图片描述
偏向锁
之前我提到过了,对象头是由Mark Word和Klass pointer 组成,锁争夺也就是对象头指向的Monitor对象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。这个过程是采用了CAS乐观锁操作的,每次同一线程进入,虚拟机就不进行任何同步的操作了,对标志位+1就好了,不同线程过来,CAS会失败,也就意味着获取锁失败。
轻量级锁
轻量级锁是采用自旋锁的方式来实现的,自旋锁分为固定次数自旋锁和自适应自旋锁。轻量级锁是针对竞争锁对象线程不多且线程持有锁时间不长的场景, 因为阻塞线程需要CPU从用户态转到内核态,代价很大,如果一个刚刚阻塞不久就被释放代价有大。具体实现和升级为重量级锁过程:线程A获取轻量级锁时会把对象头中的MarkWord复制一份到线程A的栈帧中创建用于存储锁记录的空间,然后使用CAS将对象头中的内容替换成线程A存储的空间地址。如果这时候出现线程B来获取锁,线程B也跟线程A同样复制对象头的MarkWord到自己的锁记录空间中,如果线程A锁还没释放,这时候那么线程B的CAS操作会失败,会继续自旋,当然不可能让线程B一直自旋下去,自旋到一定次数(固定次数/自适应)就会升级为重量级锁。
重量级锁
将自旋失败后的线程加入队列
扩展:锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
扩展:锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。例如你for循环来给一个变量+1,那么循环10次你需要加锁10次解锁10次,这种频繁的加锁解锁是非常消耗资源的,我们可以将整个for循环加锁。

锁之读写锁

读写锁从字面意思我们也可以知道,它由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。所以,读写锁适用于能明确区分读操作和写操作的场景
读写锁的工作原理是:
当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。但是,一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。知道了读写锁的工作原理后,我们可以发现,读写锁在读多写少的场景,能发挥出优势。ReentrantReadWriteLock就是经典的读写锁。
另外,根据实现的不同,读写锁可以分为**「读优先锁」「写优先锁」**。
读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取读锁。
写优先锁是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。

锁之乐观锁悲观锁

前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

杂乱无章分析之ReentrantLock锁

一句极其重要的话:AQS是ReentrantLock的基础,AQS是构建锁、同步器的框架
构造方法
最杂乱无章的一个知识点:锁_第2张图片
非公平lock方法
当获取锁失败后,会执行AQS中的acquire()方法
最杂乱无章的一个知识点:锁_第3张图片
acquire()方法中分三个方法实现
最杂乱无章的一个知识点:锁_第4张图片
tryacquire()方法就是重新去获取一次锁
最杂乱无章的一个知识点:锁_第5张图片
如果获取锁成功,那么作为非公平锁将毫不留情的去占用这把锁。但是锁的状态为公平锁时,会加以判断此时队列是否为空,如果不为空,该线程排队等候,让队列头部的线程去获取锁。
最杂乱无章的一个知识点:锁_第6张图片

重磅出击!!!AQS

AQS实现原理
AQS中 维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
这里volatile能够保证多线程下的可见性,当state=1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,比列会被UNSAFE.park()操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。另外state的操作都是通过CAS来保证其并发修改的安全性。
最杂乱无章的一个知识点:锁_第7张图片
场景分析
如果同时有三个线程并发抢占锁,此时线程一抢占锁成功,线程二和线程三抢占锁失败,具体执行流程如下:
最杂乱无章的一个知识点:锁_第8张图片
首先抢占锁失败的线程二会进入到acquire()方法中:
最杂乱无章的一个知识点:锁_第9张图片
首先执行tryAquire()方法,其实重新去CAS尝试占有锁,线程二通过CAS修改state的值不会成功。加锁失败。
线程二执行tryAcquire()后会返回false,接着执行addWaiter(Node.EXCLUSIVE)逻辑,将自己加入到一个FIFO等待队列中,其基本过程我口述一下吧,因为我实在不想看源码了。
第一步:先判断队列是否为空,如果为空new一个哨兵节点,这个节点有一个很重要的属性waitstatus = 0,(每一个节点上都有这个属性)。然后将线程B存入节点,并和哨兵节点连接(双向连接),哨兵节点.next = 节点B。
第二步:此时线程B只是入队了,还不是被真正的阻塞,入队以后执行acquireQueued()方法,首先判断头节点是否为哨兵节点,然后再次执行tryacquire(),获取失败后改变哨兵节点的值0 -> -1,返回值为false。当返回值为false时进行一次自旋,再次执行tryacquire(),这次和上次的不同是哨兵节点的waistatus = -1,则返回值为true,此时调用LockSupport.park(线程B),B就真真正正的入队了,处于正在排队等待中的状态。
最杂乱无章的一个知识点:锁_第10张图片
第三步:当A线程任务一直完毕后,释放锁并且修改state的值为0,代表锁已经被释放了。将哨兵节点的waitstatus由 -1 ->0。然后B再去执行tryacquire()方法尝试获取锁,此时state的状态为0,B上位成功 。

谈谈 synchronized和ReentrantLock 的区别

1. 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
3.ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:
①等待可中断 ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。

②可实现公平锁 ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。

③可实现选择性通知(锁可以绑定多个条件) synchronized关键字与**wait()和notify()/notifyAll()**方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

你可能感兴趣的:(多线程,面试)