java锁总结

1.公平锁与非公平锁

公平锁:指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来依次获得锁。

优点:等待锁的线程不会饿死。缺点:整体效率相对较低。

非公平锁:可以抢占,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,则该线程会直接抢占,而这时阻塞在等待队列的线程不会被唤醒。

默认实现的是非公平锁,因为可能会出现线程连续获取锁的情况,因此非公平锁可能造饥饿,但由于线程切换很少,保证其吞吐量大。

2.自旋锁

使用自旋锁的原因:java的线程是映射到操作系统的原生线程之上的,阻塞或者唤醒一个线程会导致操作系统在用户态和核心态的切换,状态转换可能耗时较长。因此考虑让后面请求锁的线程等待一下,并不放弃处理器的执行时间,看持有锁的线程是否会很快释放锁。为了让线程等待,让线程执行一个忙循环(自旋)

需要注意的是:自旋虽然避免了线程间切换,但要占用cpu时间,因此自选的时间应存在一定的限度。自适应自旋锁:自旋的时间不在固定,而是由前一次在同一个锁上的自选时间及锁的拥有者的状态来决定。

注:自旋是在轻量级锁中使用的,重量级锁中,线程不使用自旋。

3.锁消除

  虚拟机在运行时,对一些要求同步的代码,但检测不可能存在共享数据竞争的锁进行消除,依据来源于逃逸分析的数据支持。

  注:逃逸分析可分为方法逃逸和线程逃逸。方法逃逸:当一个对象在方法中被定义后,因为可能被外部方法引用,比如作为调用参数被传递到其他的方法里。线程逃逸:当一个对象在方法中被定义后,可能被外部线程访问到,比如给类变量或者在其他线程中访问的实例变量。

4.锁粗化

  原则上,同步块的作用范围限制应尽量的小。但若存在一系列的操作都对同一对象反复进行加锁解锁,那么虚拟机会把加锁同步的范围扩展到整个操作序列的外部。

5.可重入锁(递归锁)

  同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码。在java环境下,ReentrantLock和synchronized都是可重入锁,作用:避免死锁。

6.偏向锁、轻量级锁和重量级锁

  synchronized的偏向锁、轻量级锁以及重量级锁是通过Java对象头实现的。

  Java对象的内存布局分为:对象头、实例数据和对其填充,而对象头又可以分为”Mark Word”和类型指针class。“Mark Word”是关键,默认情况下,其存储对象的HashCode、分代年龄和锁标记位。

  

  1) 偏向锁:

  目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。

  偏向锁会偏向与第一个获得它的线程,如果在之后的执行过程中,该锁没有被其他线程所获取,则持有偏向锁的线程将永远不需要同步

  当锁对象第一次被线程获取的时候,线程使用CAS操作把这个锁的线程ID记录再对象Mark Word之中,同时置偏向标志位1。以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。

  偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他前程城市竞争偏向锁时,只有偏向锁的线程才会释放锁。锁的撤销,需要等到全局安全点(在这个时间点上没有正在执行的字节码)。首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果前程不处于活动状态,则将对象头设置为无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的所记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

  如果线程使用CAS操作时失败则表示该锁对象上存在竞争并且这个时候另外一个线程获得偏向锁的所有权。当到达全局安全点(safepoint,这个时间点上没有正在执行的字节码)时获得偏向锁的线程被挂起,膨胀为轻量级锁(涉及Monitor Record,Lock Record相关操作,这里不展开),同时被撤销偏向锁的线程继续往下执行同步代码。当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。

  线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录(Lock Record)的空间,并将对象头中的Mard Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果自旋失败则锁会膨胀成重量级锁。如果自旋成功则依然处于轻量级锁的状态。

  2)轻量级锁

  轻量级锁的解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中赋值的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了,如果替换失败,就说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。

  轻量级锁提升程序同步性能的依据是:对于绝大部分的锁,在整个同步周期内都是不存在竞争的(区别于偏向锁)。这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。

 锁的优缺点对比:

  1. 偏向锁:加锁和解锁的过程不需要额外的消耗,和执行非同步方法相比进存在纳秒的差别。但如果线程间存在锁竞争,会带来额外的锁撤销的消耗。因此适用于只有一个线程访问同步块的场景。
  2. 轻量级锁:竞争的线程不会阻塞,提高了程序的响应速度。但如果始终得不到锁竞争的线程,使用自旋会消耗CPU。所以适用于追求响应时间,同步块执行速度非常快的场景。
  3. 重量级锁:线程竞争不适用自旋,不会消耗cpu,但线程阻塞,响应时间缓慢。所以适用于追求吞吐量,同步执行速度较长。

如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那么偏向锁就是在无竞争的情况下把整个同步消除掉。

7. 整个synchronized锁流程如下:

  1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁;
  2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1;
  3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁;
  4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁;
  5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁;
  6. 如果自旋成功则依然处于轻量级状态;
  7. 如果自旋失败,则升级为重量级锁;

8. 悲观锁与乐观锁

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

乐观锁:假定不会发生并发冲突,只在提交操作时检测是否违反数据完整性。(使用版本号或者时间戳来配合实现)

9. 共享锁和排他锁

共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁,获取共享锁的事务只能读数据,不能修改数据。

排他锁:如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的锁。获得排他锁的事务既能读数据又能修改数据。

10. 读写锁

  一个资源能够被多个读进程访问,或者被一个写进程访问但不能同时存在读进程。

 

 

 

1、有volatile变量修饰的共享变量进行写操作的时候会多出行汇编代码。该指令会引发两件事情:1)将当前处理器缓存行的数据写回到系统内存;2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

2、锁一共有四种状态,由低到高分别为:无锁,偏向锁,轻量级锁,重量级锁。锁可以升级但是不可以降级。

3、当读一个volatile变量时,JMM(Java内存模型)会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享内存。

4、当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

5、公平锁和非公平锁的内存语义:

  • 公平锁和非公平锁释放时,最后都要写一个volatile变量state
  • 公平锁获取时,首先会去读volatile变量
  • 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

6、许多声明抛出InterruptedException的方法,在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标志位清除,然后再抛异常,此时调用isinterrupted()方法将会返回false。

7、waitThread首先获取了对象的锁,然后调用对象的wait方法,从而放弃了锁并进入了对象的等待队列waitQueue中,进入等待状态。由于waitThread释放了对象的锁,notifyThread随后获取了对象的锁,并调用对象的notify方法,将waitThread从waitQueue移到SynchronizedQueue中,此时waitThread的状态变为阻塞状态,notifyThread释放了锁之后,waitThread再次获取到锁并从wait方法返回继续执行。

  Monitor.Enter:监视器进入,获取锁

  Monitor.Exit:监视器退出,释放锁。

  任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。

8、Lock接口提供的synchronized关键字不具备的主要特性:

  • 尝试非阻塞的获取锁:当前线程尝试获取锁,若这一时刻锁没有被其他线程获取到,则成功获取并持有锁
  • 能被中断的获取锁:与s不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常就会被抛出,同时锁会被释放
  • 超时获取锁:在指定的截止时间之前获取锁,如果截止时间到了仍然无法获取锁,则返回。

  Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。

9、队列同步器是用来构建锁或其他同步组件的基本框架,他使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

同步器的设计是基于模板模式的,因此,使用者需要集成同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

 

 

  同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置称为尾节点之后,当前线程才能从该方法返回,否则当前线程不断地尝试设置。

  节点进入同步队列之后,就进入了一个自旋地过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点地线程)。移出队列(或停止自旋)的条件是前去节点为头节点且成功获取了同步状态。

 

独占式同步状态获取流程:

 

 

10、读写锁:

  分为读锁和写锁,写锁被获取到时,后续(其他线程)的读写操作都会被阻塞,写锁释放后,所有操作继续执行。

  锁降级:遵循获取写锁、获取读锁、再释放写锁的次序,写锁能够降级为读锁。(把持住(当前拥有的)写锁,在获取到读锁,随后释放(当前拥有的)写锁的过程)

  读写锁同样依赖自定义同步器来实现同步状态,在一个整形变量上维护读写状态,高16位表示读,低16位表示写。

 

posted on 2019-06-30 19:55  Toria  阅读(171)  评论(0编辑  收藏  举报

导航