并发编程-JMM&ReentrantLock锁以及原理
并发编程-JMM&ReentrantLock锁以及原理
JMM(Java Memory Model(Java内存模型)):我们都明白java是一个一次编译多处运行的语言,然而在不同的系统架构中拥有不同的内存模型,java是一个跨平台的虚拟系统,所以他有制定了自己的内存模型,内存模型描述了程序中各个变量之间的关系,以及这些变量如何存取的底层细节。因为是多线程,我们就不得不考虑有序性、原子性、和可见性的问题,那jJMM就和这些问题有关,所以我们来对jmm进行认知。
Lock:他的作用和synchronized的是一样的,都是解决程安全问题的一个手段,是JUC(java.util.concurrent)下的一个工具
JMM:
实际他定义了一个在java中线程访问内存的规范,他类似于一个策略模式,在不同的系统架构中,他生成不同的指令 从而达到不同的架构运行结果的相同:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本,这样做只是把对cpu的操作方法抽象到jvm中去实现而已。
jmm如何保障可见性:
前面我们说过cpu中提供了一个方法【内存屏障】,那在jvm中对应也封装了这样的指令对于不同系统架构的访问,这些实现主要是由OrderAccess类定义的一些列的读写屏障来实现的,在一系列OrderAccess类中对这下面这些指令有不同的实现方法
当我们增加一个volatile关键字后,在jvm底层的代码会进行判断,然后调用相关的指令,我们发现底层实际上是调用上面图中的storeload的指令的。
然而并不是所有的程序指令中都有指令重新排序和可见性的问题的,那就有什么时候不会有这种问题发生呢,那就引出了【happens-before】
happens-before(本质上描述的是可见性规则)
happens-beore是为了约束指令重排序制定的一系列规则
- 程序顺序性规则(as-if-series):不管指令如何重排序,单线程的程序执行结果不会发生任何变化
- 传递性规则:hb(A, B) , hb(B, C),那么hb(A, C)。这里的hb的意思就是发生之后执行(happens-before)
- volatile变量规则
![]()
- 监视器锁规则:想象一下两个线程抢占一个锁,线程一修改了变量a,然后线程二进入读取到的变量a一定是线程一修改过后的数据
- start规则:线程在start之间对一个变量进行修改,在线程start之后,读取的数据一定是修改后的数据
![]()
- join规则:我们知道join是保障线程顺序执行的,这个的意思就是,在我join前面的线程中修改的数据,在join后一定读取到的是join前面修改的数据
Lock:
上面讲到是他的作用和synchronized是一样的只是实现的方式不一样,在他的下面有很多实现方式,我们先从【ReentrantLock】开始,他是一种重入锁(是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞),换言之他是一种互斥锁,当我们使用synchronized的时候,可以使用他进行代替。
他的用法不同于synchronized,他需要手动释放锁
public class LockDemo { static Lock lock=new ReentrantLock(); static int count=0; private static void inc(){ lock.lock(); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } count++; } public static void main(String[] args) throws InterruptedException { for (int i = 0; i <1000 ; i++) { new Thread(LockDemo::inc).start(); } Thread.sleep(3000); System.out.print(""+count); } }
ReentrantLock的实现原理
本质上我们需要去满足锁的互斥,意味着同一个时间只允许一个线程进入被锁保护的代码中,以及在多线程环境下,需要满足顺序访问。
如果我们自己去设计,需要。。。
- 锁肯定会被抢占,那我们一定要设计一个标记,去标记当前代码是否已经被别的锁抢占,类似synchronized的对象头中的锁的标记markward
- 抢占到锁如何处理->不需要处理,直接执行代码即可
- 没有抢占到锁如何处理
锁抢占的公平性(是否允许插队,是否让队列中的线程逐一执行 )
- 需要等待(让处理排队状态的线程,直接先阻塞,这样可以释放cpu的资源)
- 如何让线程等待
- wait/notify(一种线程的通讯机制,但是没有办法指定唤醒的线程)
- LockSupport.park/unpark(阻塞一个指定的线程、唤醒一个指定的线程)
- condition
- 需要排队(运行有n个线程被阻塞,此时线程处理活跃状态)
- 通过一个数据结构把n个排队的线程存储起来
- 公平 (队列中的线程逐一执行)
- 非公平(可以提升性能,这样就不用排队)
- 抢占到锁的释放过程如何处理
- LockSupport.unpark(唤醒处于队列中的指定线程)
上面我们的猜想,实际上[AQS]AbstractQueuedSynchronizer已经帮我们做了一部分工作
【AQS java.util.concurrent.locks.AbstractQueuedSynchronizer】AQS定义了一套多线程访问共享资源的同步器框架,他提供了两种机制,
- 共享锁:同时可以有多个线程来获得锁
- 互斥锁:同一时刻只能有一个线程来获得锁
整体流程为
现在有线程A、B、 C来共同抢占一个方法
- 判断锁的状态
- 如果无锁,
- 那就修改aqs中state(标记锁的状态java.util.concurrent.locks.AbstractQueuedSynchronizer#state)的数值,改为1,
- 然后把当前抢占到锁的线程放入exclusiveOwnerThread(记录当前是谁获得的锁java.util.concurrent.locks.AbstractOwnableSynchronizer#exclusiveOwnerThread)中,假设是线程A,然而修改状态不是原子的,这里底层用到了CAS操作
- 有锁,
- 因为线程A已经获得了锁,修改了状态,然后假设现在是线程B,那这个时候线程B就需要去被放在队列中,这个队列是一个双向列表,线程C也会被放在这个双向列表中,他们被封装成一个个node,互相指向对方,
- 现在他们每个节点都开始自旋(不断的判断state的状态,然后去抢占锁,其实是一次自旋后,发现没有可用的锁,那本身就阻塞,调用 LockSupport.park(this))当线程A释放了锁之后,
- 他们就会被唤醒(LockSupport.unpark(头节点的下一个节点)),记住,这里只唤醒头节点的下一个节点,那就是线程B了,这个时候线程B会继续自旋,然后去抢占锁,这里就体现了公平性(是否允许插队),如果这个在线程A释放锁的时候,这个时候来个线程D那么刚好这个时候是没有锁的,那线程D就有可能抢到线程B的前面