Java锁常见知识点

synchronized实现原理

  •  synchronized同步代码块:synchronized关键字经过编译之后,会在同步代码块前后分别形成monitorerter和monitorexit字节码指令,在执行monitorenter指令的时候,首先尝试获取对象的锁,如果这么锁没有被锁定或者当前线程已经拥有了那个对象的锁,锁的计数器就加1,在执行monitorexit指令时会将锁的计数器减1,当减为0的时候就释放锁。如果获取锁对象一直失败,那当前线程就阻塞等待,直到对象锁被另一个线程释放为止。
  • synchronized同步方法:方法级的同步是隐式的,无需通过字节码指令来控制,JVM可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令会检查方法中的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先持有monitor对象,然后才能执行方法,最后当方法执行完(无论正常完成还是非正常完成)时释放monitor对象。在方法执行期间,执行线程持有了管程,其他线程都无法再次获取同一个管程。

☞ Synchronized优化

Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁、轻量级锁和自旋锁等概念。

ReentrantLock是如何实现可重入性的?

内部自定了同步器Sync,加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下当前维护的那个线程ID和当前请求的线程ID是否一样,一样就可重入了。

ReentrantLock如何避免死锁? 

  • 响应中断lockInterruptibly()
  • 可轮询锁tryLock()
  • 定时锁tryLock(long time)

tryLock, lock, lockInterruptibly的区别

  1. tryLock能获得锁就返回true,不能就立即返回false
  2. tryLock(long timeout, TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁就返回false
  3. lock能获得锁就返回true,不能的话就一直等待获得锁
  4. lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock不会抛出异常,而lockInterruptibly会抛出异常。

CountDownLatch和CyclicBarrier的区别是什么

 CountDownLatch是等待其他线程执行到某一个点的时候,再继续执行逻辑(子线程不会被阻塞,会继续执行),只能使用一次。最常见的是join形式,主线程等待子线程执行完任务,在用主线程去获取结果的方式,内部是用计数器相减实现的,AQS的state承担了计数器的作用,初始化的时候,使用AQS赋值,主线程调用await(),被加入共享线程等待队列里面,子线程调用countDown的时候,使用自选的方式,减1,直到为0时触发唤醒。

CyclicBarrier回环屏障,主要是等待一组线程到同一个状态的时候放闸同时启动。CyclicBarrier还可以传递一个Runnable对象,可以到放闸的时候执行这个任务,CyclicBarrier是可循环的,当调用await的时候如果count变成了0则会充值状态。CyclicBarrier新增了一个字段parties,用来保存初始值,当count变为0的时候就重新赋值。还有一个不同点,CyclicBarrier不是基于AQS的,而是基于ReentrantLock实现的,存放的等待队列使用了条件变量的方式。

synchronized与ReentrantLock的区别

  • 都是可重入锁:ReentrantLock是显式获取和释放锁,synchronized是隐式的
  • ReentrantLock可以直到有没有成功获取锁,可以定义读写锁,是api级别,synchronized是JVM级别
  • ReentrantLock可以定义公平锁,Lock是接口,synchronized是Java关键字

什么是信号量Semaphore

信号量是一种固定资源的限制的一种并发工具包,基于AQS实现,在构造的时候会设置一个值,代表着资源数量。信号量主要是应用于多个共享资源的互斥使用,以及用于并发线程数的控制。信号量也分公平和非公平的情况,基本方式和ReentrantLock差不多,在请求资源调用task时,会用自旋的方式减1,如果成功,则获取成功了,如果失败,导致资源数变为了0,就会加入队列里面等待;调用release的时候会加1,补充资源,并唤醒等待队列。

Samaphore应用

  • acquire(), release()可用于对象池、资源池的构建,比如静态全局对象池,数据库连接池
  • 可创建计数为1的Samaphore,作为互斥锁(二元信号量)

可重入锁概念

  1. 可重入锁是指同一个线程可以多次获取同一把锁,不会因为之前已经获取过还没释放而阻塞
  2. ReentrantLock和synchronized都是可重入锁
  3. 可重入锁的一个优点是可以一定程度避免死锁 

ReentranLock原理

☞ CAS+AQS队列来实现

  1. 先通过CAS尝试获取锁,如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起
  2. 当锁被释放之后,排在队首的线程会被唤醒CAS再次尝试获取锁
  3. 如果是非公平锁,同时还有一个线程进来尝试获取可能会让这个线程抢到锁
  4. 如果是公平锁,新加的线程会排到队尾,由队首的线程获取到锁

AQS原理

Node内部类构成一个双向链表结构的同步队列,通过控制state (volatile的int类型)状态来判断锁的状态.(1)对于非可重入锁状态不是0则去阻塞;(2)对于可重入锁如果是0则执行,非0则判断当前线程是否是获取到这个锁的线程,是的话把state状态+1,比如重入5次,那么state=5,而在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格获得锁。

AQS两种资源共享方式

  •  Exclusive:独占,只有一个线程能执行,如ReentrantLock
  • Share:共享,多个线程可以同时执行,如Semaphore, CountDownLotch, ReadWriteLock, CyclicBarrier

CAS原理、缺点及应用

☞ 原理

内存值V,旧的期望值A,要修改的新值B,当A=V时,将内存值修改为B,否则什么都不做。

☞ 缺点

  1. ABA问题
  2. 如果CAS失败,自旋会给CPU带来压力
  3. 只能保证对一个变量的原子性操作,i++这种是不能保证的

☞ 应用

Atomic系列

公平锁和非公平锁

  1. 公平锁在分配锁前检查是否有线程在排队等待获取该锁,优先分配排队时间最常的线程,非公平锁直接尝试获取锁
  2. 公平锁需要多维护一个线程队列,效率低,默认非公平

独占锁和共享锁

  1. ReentrantLock为独占锁(悲观加锁策略)
  2. ReentrantReadWriterLock中读锁为共享锁

4种锁状态

  1. 无锁
  2. 偏向锁 - 会偏向第一个访问锁的线程,当一个线程访问同步块代码获得锁时,会在对象头和栈帧记录里存储锁偏向的线程ID,当这个线程再次进入同步代码块时,就不需要CAS操作来加锁了,只要测试一下对象头里面是否存储指向当前线程的偏向锁,如果偏向锁未启动,new出的对象是普通对象(即无锁,有稍微竞争会造成轻量级锁),如果启动,new出的对象是匿名偏向(偏向锁)。对象头主要包括两部分数据:Mark Work(标记字段,存储对象自身的运行时数据)、class pointer(类型指针,对象指向它的类元数据的指针)
  3. 轻量级锁(自旋锁)
    1. 在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这是就无需再进行阻塞操作,避免了用户态到内核态的切换(自适应自选时间为一个线程上下文切换时间)
    2. 在用自旋锁时有可能会造成死锁,当递归调用时有可能造成死锁
    3. 自旋锁底层是通过指向线程栈中Lock Record的指针来实现
  4. 重量级锁

轻量级锁与偏向锁的区别

  1. 轻量级锁是通过CAS来避免进入开销较大的互斥操作
  2. 偏向锁是在无竞争场景下完全消除同步,连CAS也不执行

自旋锁升级到重量级锁条件

  1. 某线程自旋次数超过10次
  2. 等待的自旋线程超过了系统core数的一半

读写锁及其实现方式

常用的读写锁ReentrantReadWriteLock,这个其实和ReentrantLock相似,也是基于AQS的,但是这个是基于共享资源的,不是互斥,关键在于state的处理,读写锁把高16位记为读状态,低16位记为写状态,就分开了,读读情况是读锁重入,读写/写读/写写都是互斥的,只要判断低16位就好了。

zookeeper实现分布式锁

  1.  利用节点名称唯一性来实现,加锁时所有客户端一起创建节点,只有一个创建成功者获得锁,解锁时删除节点。
  2. 利用临时顺序节点来实现,加锁时所有客户端都创建临时顺序节点,创建节点序号最小的获得锁,否则监视比机子序号次小的节点进行等待
  3. 方法2相比方案1的好处时当zookeeper宕机后,临时顺序节点会自动删除释放锁,不会噪声锁等待
  4. 方案1会产生惊群效应(当有很多进程在等待锁的时候,在释放锁的时候会有很多进程就过来争夺锁)
  5. 由于需要频繁创建和删除节点,性能上不如redis锁

volatile变量

  1. 变量可见性
  2. 防止指令重排序
  3. 保障变量单次读写的原子性,但不能保证i++这种操作的原子性,因为本质是读,写两次操作

volatile如何保证线程间可见和避免指令重排

volatile之可见性是由原子性保证的,在jmm中定义了8类原子性指令,比如write, store, read, load。而volatile就要求write-store, load-read成为一个原子性操作,这样子可以确保在读取的时候都是从主内存读入,写入的时候会同步到主内存中。

指令重排则是由内存屏障来保证的,有两个内存屏障:

  1. 一个是编译器屏障:阻止编译器重排,保证编译程序时在优化屏障之后的指令不会在优化屏障之后执行
  2. 二是cput屏障:sfence保证写入,lfence保证读取,lock类似于锁的方式。

 

你可能感兴趣的:(Java)