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的区别
- tryLock能获得锁就返回true,不能就立即返回false
- tryLock(long timeout, TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁就返回false
- lock能获得锁就返回true,不能的话就一直等待获得锁
- 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,作为互斥锁(二元信号量)
可重入锁概念
- 可重入锁是指同一个线程可以多次获取同一把锁,不会因为之前已经获取过还没释放而阻塞
- ReentrantLock和synchronized都是可重入锁
- 可重入锁的一个优点是可以一定程度避免死锁
ReentranLock原理
☞ CAS+AQS队列来实现
- 先通过CAS尝试获取锁,如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起
- 当锁被释放之后,排在队首的线程会被唤醒CAS再次尝试获取锁
- 如果是非公平锁,同时还有一个线程进来尝试获取可能会让这个线程抢到锁
- 如果是公平锁,新加的线程会排到队尾,由队首的线程获取到锁
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,否则什么都不做。
☞ 缺点
- ABA问题
- 如果CAS失败,自旋会给CPU带来压力
- 只能保证对一个变量的原子性操作,i++这种是不能保证的
☞ 应用
Atomic系列
公平锁和非公平锁
- 公平锁在分配锁前检查是否有线程在排队等待获取该锁,优先分配排队时间最常的线程,非公平锁直接尝试获取锁
- 公平锁需要多维护一个线程队列,效率低,默认非公平
独占锁和共享锁
- ReentrantLock为独占锁(悲观加锁策略)
- ReentrantReadWriterLock中读锁为共享锁
4种锁状态
- 无锁
- 偏向锁 - 会偏向第一个访问锁的线程,当一个线程访问同步块代码获得锁时,会在对象头和栈帧记录里存储锁偏向的线程ID,当这个线程再次进入同步代码块时,就不需要CAS操作来加锁了,只要测试一下对象头里面是否存储指向当前线程的偏向锁,如果偏向锁未启动,new出的对象是普通对象(即无锁,有稍微竞争会造成轻量级锁),如果启动,new出的对象是匿名偏向(偏向锁)。对象头主要包括两部分数据:Mark Work(标记字段,存储对象自身的运行时数据)、class pointer(类型指针,对象指向它的类元数据的指针)
- 轻量级锁(自旋锁)
- 在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这是就无需再进行阻塞操作,避免了用户态到内核态的切换(自适应自选时间为一个线程上下文切换时间)
- 在用自旋锁时有可能会造成死锁,当递归调用时有可能造成死锁
- 自旋锁底层是通过指向线程栈中Lock Record的指针来实现
- 重量级锁
轻量级锁与偏向锁的区别
- 轻量级锁是通过CAS来避免进入开销较大的互斥操作
- 偏向锁是在无竞争场景下完全消除同步,连CAS也不执行
自旋锁升级到重量级锁条件
- 某线程自旋次数超过10次
- 等待的自旋线程超过了系统core数的一半
读写锁及其实现方式
常用的读写锁ReentrantReadWriteLock,这个其实和ReentrantLock相似,也是基于AQS的,但是这个是基于共享资源的,不是互斥,关键在于state的处理,读写锁把高16位记为读状态,低16位记为写状态,就分开了,读读情况是读锁重入,读写/写读/写写都是互斥的,只要判断低16位就好了。
zookeeper实现分布式锁
- 利用节点名称唯一性来实现,加锁时所有客户端一起创建节点,只有一个创建成功者获得锁,解锁时删除节点。
- 利用临时顺序节点来实现,加锁时所有客户端都创建临时顺序节点,创建节点序号最小的获得锁,否则监视比机子序号次小的节点进行等待
- 方法2相比方案1的好处时当zookeeper宕机后,临时顺序节点会自动删除释放锁,不会噪声锁等待
- 方案1会产生惊群效应(当有很多进程在等待锁的时候,在释放锁的时候会有很多进程就过来争夺锁)
- 由于需要频繁创建和删除节点,性能上不如redis锁
volatile变量
- 变量可见性
- 防止指令重排序
- 保障变量单次读写的原子性,但不能保证i++这种操作的原子性,因为本质是读,写两次操作
volatile如何保证线程间可见和避免指令重排
volatile之可见性是由原子性保证的,在jmm中定义了8类原子性指令,比如write, store, read, load。而volatile就要求write-store, load-read成为一个原子性操作,这样子可以确保在读取的时候都是从主内存读入,写入的时候会同步到主内存中。
指令重排则是由内存屏障来保证的,有两个内存屏障:
- 一个是编译器屏障:阻止编译器重排,保证编译程序时在优化屏障之后的指令不会在优化屏障之后执行
- 二是cput屏障:sfence保证写入,lfence保证读取,lock类似于锁的方式。