1.java对象头
java对象头包含两个部分,一个指向对象类型的指针,一个MarkWord存放(对象的hashcode,分代年龄,以及锁相关的信息如:锁状态,锁标志位,是否是偏向锁等),如果该对象是数组,还会有一个数组长度
2.Synchronized(锁升级的过程)
1.三种使用方式
- 修饰方法 锁是当前实例对象
- 修饰静态方法 锁是当前类的class对象
- 修饰代码块 锁是synchronized括号里面的对象
2.无状态锁,偏向锁,轻量级锁,重量级锁
- 偏向锁
HotSpot作者发现,在大部分的时候,锁不仅不存在被多个对象竞争,反而被同一个线程所一直持有,如果同一个线程一直获得锁和释放锁就造成了(上下文切换)资源的浪费。所以引入了偏向锁,当锁是偏向锁状态时,对象中markword中有一个线程ID,该线程id指向持有该偏向锁的线程,每次获得该锁之前,只需判断该线程ID是不是请求的线程,如果是的,则证明当前线程持有锁则不需要重新获取锁。
偏向锁初始化,当对象想获取偏向锁时,会使用CAS操作将MarkWrod中的线程id指向栈帧中锁记录得线程id,如果CAS失败则证明还有其他线程竞争资源,则可能转变成轻量级锁。
偏向锁的释放,不是在代码块执行完之后释放,而是如果有其他线程过来竞争,才会释放,这个时候由safepoint 判断当前线程是否处于不活动状态,如果处于不活动状态则释放锁。
偏向锁的关闭需要手动设置一个参数-XX:BiasedLockingStartupDelay=0
而是在偏向锁的获取过程中, 发现了竞争时, 直接将一个被偏向的对象“升级到” 被加了轻量级锁的状态。 这个操作的具体完成方式如下:
首先通过 MarkWord 中已经存在的 Thread Id 找到成功获取了偏向锁的那个线程, 暂停拥有偏向锁的线程,然后在该线程的栈帧中补充上轻量级加锁时, 会保存的锁记录(Lock Record), 然后将被获取了偏向锁对象的 MarkWord 更新为指向这条锁记录的指针。最后唤醒暂停线程
- 轻量级锁
线程在执行代码同步快时,会在线程的栈帧中创建用于存储锁记录的空间,并将对象的markWord 复制过去,称为Replace markWord ,然后采用CAS的方法,讲对象头下·中的markWord替换成指向锁记录的指针,如果替换成功则持有锁,如果替换失败,则采用自旋的方式获得锁,如果自旋(一定次数)获得锁失败,则会发生锁膨胀,升级为重量级锁,并进入阻塞状态,等到之前持有锁的线程释放锁则会唤醒阻塞的进程重新竞争锁。
-重量级锁
重量级锁依赖于操作系统的互斥量(mutex) 实现。
3.java使用什么机制保证原子性操作呢?
1.使用CAS+自旋锁方式
但是存在三个问题
- ABA问题:如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有变化,但是实际上却发生了变化。解决思路是,使用版本号,在变量前追加版本号,每次变量更新把版本号加1,如A-B-A ==> 1A-2B-3A
- 循环时间开销太大
解决思路:jvm允许处理器使用pause指令
- 只能保证一个共享变量的原子操作
解决思路:JDK提供了AtomicReference类来保证引用对象之间的原子操作,就可以把多个变量放在一个对象里进行CAS操作了(还有一种将两个变量合并,如i=2,j=a ==> ij=2a)
2.使用锁机制实现原子操作
如偏向锁,轻量级锁,自旋锁,互斥锁
4.上下文切换
我们在获取锁和释放锁时都会造成上下文切换,如果频繁的上下文切换则会浪费资源
如何减少上下文切换呢?
- 使用CAS算法
-使用最少线程
-使用协程
5.并发工具类
1. CountDownLatch ---等待多线程完成
我们可以看到这里的CountDownLatch底层是实现了一个同步队列器的.
这里有一个构造函数,当我们new CountDownLatch的时候实际上是在向同步队列器中设置了一个状态值,然后重写了tryAcquireShared()方法,这个地方就是获取同步状态的方法,可以看到这里是判断state==0?即只有state==0的时候才有机会获取到同步状态,否则其他情况下就都会加入到同步队列器中进行自旋等待。
额,如果想释放它,则必须让state==0所以重写了tryReleaseShared方法,这个方法用CAS保证了每次对state的减1操作,当state==0时就释放同步状态唤醒后继节点。
2.同步屏障CyclicBarrier
让一组线程达到一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会被打开。
提供两种构造函数:
1.)new CyclicBarrier(int count)//count 表示屏障拦截的线程数量,每个线程调用await()方法告诉CyclicBarrier直接达到屏障点。
可以看到一直等到线程2到达同步点才释放同步状态。
2.)new CyclicBarrier(int count,Runnable Action);这个构造函数表示,当线程到达屏障时会优先处理第二个参数中的run方法。
可以看到,CylicBarrier构造函数中的第二个参数被先执行了。一般可用于最后进行统计,比如最后汇总一年所消费的前,比如用一个线程计算一个月,最后用这个第二个参数进行统计所有的钱。
CylicBarrier底层的await()方法是用ReentrantLock 和该重入锁的Condition实现的,当我们调用初始化方法的时候,会初始一个parties和指向Runable 方法的参数。当我们调用await方法时,首先加锁操作,然后进行parties--操作,判断该index是否等于0,如果不等于0则调用Condition.await()被阻塞。一直等到其他线程调用await()方法将index减等于0时,则判断第二个参数是否存在,如果存在执行该方法,然后唤醒所有的等待线程,去竞争同步状态。依次执行后面的操作。
CylicBarrier 和 CountDownLatch的区别:
1.CylicBarrier 可以重复使用,而CountDownLach只能使用一次
2.CountDownLach底层使用的时同步队列器,而CylicBarrier 用的时Reentrantlock 和Condition
3.控制线程数的Semaphore
一般用来做流量控制,特别时公有资源有限的应用场景,通过acquire来获取许可证,release()方法来释放许可证。
4.线程间交换数据的Exchanger
6.AQS 队列同步器
1.)队列同步器的底层结构
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用,等待状态,以及前驱和后继节点,节点属性类型与名称。
AQS 底层是维护着一个双链表的这样的形式来形成一个FIFO的队列,当一个线程获取到同步状态(锁)会被放入到同步器中,反之如果获取失败则会用CAS方式插入到尾部。
同步器包含两个节点的类型引用,一个指向头节点,一个指向尾节点,当一个线程成功的获取到同步状态时,其他线程将无法获取到同步状态,转而被构造成为节点加入到同步队列,而这个加入队列的过程必须保证线程的安全,因此同步器提供一个基于CAS的设置尾部节点的方法,compareAndSetTail(Node expect,Node update)。
AQS结构
首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。节点的值就是对线程的引用
2.)独占式同步状态获取与释放
- acquire(独占式获取)
独占式获取
这是同步器提供的模板方法,该方法独占式获取同步状态,如果说获取到同步状态则由该方法返回,否则会进入到同步队列中等待。
-tryAcquire 这个方法用于独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态,以下为简单实现。如果获取同步状态失败,则证明该线程没有持有锁。举个例子,比如1表示,准备获取锁,调用tryAcquire如果将state从0改成1了则证明该线程独占此锁,后面第二个进程进来,也想将state从0改成1则会返回false,证明该线程没有持有锁则进入了后后面的acquirQueued方法中了
-acquirQueued(addWaiter(Node.EXCLUSIVE),arg)
首先我们得先看 里面的addWaiter方法
-addWaiter(Node mode)
首先新建一个Node标记为独占状态,通过CAS快速尝试将改节点插入到尾部,如果插入成功则返回该节点,否则调用enq方法
该方法采用自旋的方式,死循环替换尾节点,替换之前先判断当前尾节点是否为空,如果为空的话CAS将当前节点设置成头节点以及尾节点。最后返回的节点,去进入到acquirQueued方法中
- acquirQueued
这个方法中我们可以看到,如果该节点想重新获取同步状态的话,必须该节点的前驱节点是头节点,
否则一直自旋等待。或则还有一种方式,如果interrupted被标记为true也会去调用cancelAcquire方法
为什么这里 必须是前驱节点是头节点才获取同步状态呢,这里有两个原因:
1.头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需检查自己的前驱节点是否是头节点。
2.维护同步队列的FIFO原则。同时避免被过早通知(指的是前驱节点不是头节点的线程由于中断而被唤醒)。
节点自旋同步状态
-relase(同步器提供的独占式release)
通过锁自定义的tryRelease(arg)来判断是否需要释放,比如前面的Mutex锁,当state=0时tryRelease()为true即释放,释放将头节点的后继节点唤醒。
总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋,移出队列(或停止自旋)的条件是前驱节点为头节点并且成功获取到了同步状态。在释放同步状态时,同步器调用tryRelase(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
3.)共享式同步状态获取与释放
-acquireShared()
要想实现共享锁得话,首先自己得实现自己锁得tryAcquireShared方法
我们可以在以上方法看到,共享式同步器获取自旋过程中,也是首先判断前驱节点是否为头节点,且当前得TryAcquireShared 是否大于0,如果大于0前前驱节点为头节点,则表示该节点获得了同步状态。
- releaseShared