目录
AQS(AbstractQueuedSynchronizer):
为什么会出现锁
锁的类型以及相关概念
讲到锁,就得先讲一下同步,每当我们要用同步是就会想到synchronized,
5.使用局域变量来实现线程同步,如果使用ThreadLocal来管理变量,那么每一个使用该变量的线程都会获得该变量的副本,副本之间是相互独立的,这样每个线程修改自己变量的时候不会影响其他线程。
synchronized我们用起来是同步的作用,其实内部是锁来实现的,具体来看看synchronized到底是什么。
synchronized:
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
当被synchronized修饰时,synchronized会锁定当前变量,只有当前线程能够访问该变量,其他线程会被阻塞。
synchronized的具体是怎么实现的呢?
Java对象头和monitor是实现synchronized的基础
Hotspot的对象头主要包括两部分,Mark Word(标记字段),Klass Pointer(类型指针)。
Klass Pointer:对象指向类元数据的指针,虚拟机通过这个来确定对象是那个类的实例
Mark Word:存储自身运行时数据,比如哈希码,GC分代年龄,锁标记位,线程持有的锁等
什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。
与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
现在回到synchornized代码实现上
当被synchronized关键字修饰的代码块在被编译成字节码的时候,会在该代码块开始和结束的地方加入 monitorenter 和 moniterexist 指令,任何对象都有一个monitor相关联,当一个monitor被持有后,他就处于锁定状态,当线程执行到monitorenter指令时,会获尝试获取对象对应的monitor的所有权,即获取对象的锁。
虚拟机在执行这两个命令的时候,会检查对象的锁状态是否为空或当前线程是否已经拥有对象的锁,
与synchornized类似的还有Volatile,Lock,ThreadLocal
Volatile:
volatile,final,synchronized都可以实现可见性
1.运算结果只保证一个线程修改变量的值或者不依赖于变量的当前值
Volatile禁止指令重排序,因为Volatile变量在赋值后会有一个lock add命令,这个命令相当于内存屏障,重排序时不能把屏障后的指令重排序到屏障之前。
Volatile保证可见性:add指令会使得其他工作线程的工作内存缓存的数据失效
synchronized与Volatile的区别:
volatile,final,synchronized都可以实现可见性
volatile的本质是告诉jvm这个变量是在寄存器中的值是不确定的,需要从主存中读取。
synchronized是锁定当前变量,只有当前线程能够访问该变量,其他线程被阻塞。
volatile仅能实现变量的修改可见性,不具备原子性,而synchronized都可以保证
volatile标记的变量不会被编译器优化,而synchronized可以
volatile使用时变量级别,而synchronized可以使用在变量和方法
volatile不会阻塞线程,而synchronized可能会
Condition:
condition将Object监视器方法(wait,notify和notifyAll),分解成了多个不同的对象,然后通过这些对象和任意Lock进行结合使用。
Lock就相当于替代了synchronized方法和语句的使用。
condition替代了Objective监视器的使用方法。
condition是java5以后出现的机制,一个对象可以有多个condition(对象监视器),线程可以注册在不同的condition,可以更灵活的调度线程,有选择的调度线程,而synchronized相当于只有一个condition,所有的线程都注册在他身上,线程调度得调度所有的注册线程。
Lock:
Lock的锁定是通过代码实现的,而synchronized是在JVM层面实现。
Lock提供的是一种显示的,可轮询的定时的以及可中断的锁获取操作。
synchronized与Lock的区别:
1.Lock是一个接口,而synchronized是关键字,是在jvm层面实现的
2.Lock发生异常时,如果没有unlock()释放锁,会造成死锁现象,需要在finally块中释放,synchronized发生异常时会自动释放线程占有的锁
3.Lock可以让等待锁的线程相应中断,他不行,他得一直等待下去,不能中断
Lock的锁定是通过代码实现的,而synchronized是在JVM层面实现。
如果竞争资源不激烈,两者性能差不多,当有大量线程同时竞争时,Lock的性能高于synchronized。
AQS(AbstractQueuedSynchronizer):
提供了一个基于FIFO队列,可以用于构建阻塞锁或者同步容器的基础框架,AQS基于FIFO队列实现,存在一个个节点,node就是一个节点。对于FIFO中队列的各种操作在AQS中已经实现,AQS的子类只需要重写 tryAcquire(int arg) 和 tryRelease(int arg) 方法,用int值表示状态,子类必须定义更改此状态的受保护方法,并定义那种状态表示被获取或者被释放。这些都实现后,此类的其他方法就可以实现所有排队和阻塞机制。
tryAcquire(int arg) 试图在独占模式下获取对象状态。
tryRelease(int arg) 试图设置状态来反映独占模式下的下一个释放。
tryAcquireShared(int arg)试图在共享模式下获取对象状态。
tryReleaseShared(int arg)试图设置状态反应共享模式下的一个释放
ReentrantLock:
ReentrantLock是java.utils.locks的一个可重入锁类,在高竞争的情况下有良好的性能,可以中断,支持重人性,即对公享资源可以反复加锁,当前线程再次获取该锁时,不会被堵塞,ReentrantLock是基于AQS实现,而AQS又是基于FIFO实现的,整个AQS其实是模板模式的经典应用,FIFO队列中所有的操作,AQS已经实现,AQS的子类只需要重写tryAcquire和tryRelese。
ReentrantLock中有一个抽象类syc继承与AbstractQuenedSynchronizer,syc的实现类FairSync,NoFairSync,即公平锁,非公平锁,他们都是ReentrantLock的静态内部类。ReentrantLock中用的比较多的是非公平锁。
非公平锁流程,线程1调用ReentrantLock的lock(),线程一变成独占,第一个线程做了两件事
1.设置AbstractQuenedSynchronizer的satae为1
2.设置AbstractQuenedSynchronizer的thread为当前线程
这样当第二个线程获取锁时,就执行else,调用acquire方法,进而调用acquire中的tryacquire。
公平锁:按照时间顺序,先等待的线程,先得到锁,公平锁不会产生饥饿锁,只要排队最终都能获得锁
ReentrantLock使用场景:
3.尝试等待执行,就是发现操作已经在执行,尝试一段时间后,等待超时就不执行
ThreadLocal:
ThreadLoacl为每一个线程创建了一个独立的变量副本,这样每个线程都可以独立改变自己的副本,操作变量时互不影响。这块与线程同步机制不同,线程同步机制是多个线程共享一个变量。制造变量副本是会消耗内存的,所以他们不同之处可以说ThreadLoacl实际上是用空间换取时间策略,而线程同步是时间换取空间策略。
ThreadLoaclMap是ThreadLoacl的一个静态内部类,它是实现线程隔离机制的关键
ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。
ThreadLocal使用场景:
比如在数据库连接池中,有多个DAO要获取连接,但这时需要完成事务,只能这些DAO获取同一个Connection,这样才能完成一个事务。从ThreadLoacl中获取Connetion的话,Dao就会被列入同一个Connection.
synchronized与ThreadLocal
可以说ThreadLoacl用于线程间数据隔离,而synchronized是线程间数据共享。
无锁 --> 偏向锁 --> 轻量级锁(利用CAS原理,避免重量级锁的消耗) --> 重量级锁
注意:锁可以升级,但是不能降级,为了减少获得锁和释放锁所带来的消耗
jdk1.6对锁的优化:
1.6中出现了轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化开启,这些操作都是为了线程之间更高效的共享数据,解决竞争问题,主要是优化synchronized中获取锁,释放锁的性能问题。
jdk1.8新特性:
1.允许为接口添加一个非抽象的方法实现,使用default方法,叫扩展方法
轻量级锁实现:
轻量级锁的引入为了提升在没有提升线程竞争的情况下,执行同步代码的效率。
虚拟机使用CAS操作将对象头中的Mark Word 更新为当前线程的Lock Word的指针
如果更新成功,表示已经持有这个锁,mark Word的标记位 00,为轻量级锁。
如果更新失败,虚拟机会检查对象中的Mark Word是否指向当前线程的栈帧,如果指向则直接进入同步代码直接执行,不是,说明线程竞争。如果有两条以上的线程抢占资源,轻量级锁膨胀为重量级锁,锁状态改为10,mark Word中存储指向重量级锁的指针,后面的锁进入阻塞状态。
轻量级性能提升的依据就是“对于大多数锁”在整个同步周期是不存在竞争的,没有竞争时轻量级锁利用CAS操作避免使用系统互斥量的开销。
偏向锁实现:
当只有一个线程执行同步块时,这种情况下,使用轻量级锁也会有多个CAS操作。所以开启偏向锁后,虚拟机检查当前线程是否处于无锁状态01,且标记为0没有偏向锁,如果成功,线程会用CAS操作把锁的线程ID放入对象的Mark Word中,以后有偏向锁的线程进入同步块时,虚拟机只看锁ID是否一样,如果是直接进入,不用CAS操作了。如果有其他线程获取该对象的锁,偏向模式就结束。根据锁的状态,撤销偏向锁后看恢复成无锁还是偏向锁。如同轻量级锁的介绍。
轮询锁:
不能同时获得所有锁时,可以使用轮询锁或者定时锁避免死锁。当一个线程获得多个锁时,已经获得一部分,另一部分没获得,此时返回失败,释放已经获得的锁,重新尝试获得所有的锁。
乐观锁与悲观锁:
每次拿数据时,都认为别人不会去修改,所以不上锁,更新时采取判断别人更改数据了没。
适用于写比较少的情况,冲突发生的很少,减少系统开销,增大系统的吞吐量。
乐观锁大多是基于数据版本记录机制实现的,读取数据时,将版本号,一同读出,之后更新版本号加一,版本数据在于数据库表的版本信息比对,如果提交的数据版本号大于数据库当前版本,则给予更新,否则认为是过期