synchronized

synchronized

  1. 并发编程的三个问题:并发性,原子性,有序性

  2. 多线程并发时候可能出现可见性问题:就是在多线程中,在线程1中修改了变量a,在线程2中看不到同一个变量a的修改

  3. Synchonized可见性:Synchonized会把工作内存的变量清空,让我们去使用主存,因为Synchonized中会使用lock,而对一个变量使用lock操作,将会清空工作内存中变量的值(当前线程工作内存变量值清空了,下次就只能去主存中去数据了),而对于一个变量执行unlock操作之前,必须把此变量同步到主存中

  4. Synchonized锁的原理(指令级别):Synchonized会在自己包裹的代码执行前加monitorenter,在执行结束后使用monitorexit释放锁
    Synchonized锁的原理.png
  1. Synchonized原子性:加了Synchonized并不能防止指令重排,只是保证在同一个方法里面的操作是原子性的

  2. 为什么要重排序呢:这是cpu做的优化,目的是提高程序执行的效率,我们是控制不了的

  3. Synchonized有序性之不允许重排序的情况:

    1. 写后读:int a =1;int b = a这种情况是不允许重排序的,因为b是依赖于a的,如果重排先执行int b =a 再执行int a = 1,那么就是错误的,a都还没有值,又怎么赋值给b呢,所以这种情况是禁止重排序的

    2. 写后写:int a = 1;int a = 2这种情况也是不允许重排的,因为换了位置后,结果就不一样了

    3. 读后写:int a = 1;int b = a ;int a = 2这种情况也不能重排,因为调换位置后结果就不是我们想要的了

  4. Synchonized有序性之允许指令重排的情况:比如int a =1;int b = 2; int c = a+ b;那么此时int a = 1和int b = 2是可以重排的,因为不影响c的结果

  5. Synchonized保证有序性的意思是,不管编译器和cpu如何重排序,必须保证在单线程情况下,程序的结果是正确的

  • Synchonized可重入性

    1. 可重入:一个线程可以多次执行Synchonized重复获取同一把锁

    2. Synchonized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁

    3. 多个线程时,如果Runnable资源被Synchonized锁住了,那么当一个线程去执行的时候判断Synchonized的计数器值,只有计数器为0才能进入,证明抢到了这把锁,否则证明该锁已经被别人抢到了,我只能等待了;

    4. Synchonized可重入性的好处:
      死锁问题.png
  • 其他

    1. Synchonized是不可中断的,就是一个线程a获得了锁,其他线程必须等a执行完后才能执行自己的操作

    2. Synchonized修饰方法时,同步方法在反汇编后,会增加ACC_SYNCHRONIZED 修饰。会隐式调用monitorenter和 monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。

    3. Synchonized和lock的区别:

      1. Synchonized是关键字,lock是接口

      2. Synchonized会自动释放锁,lock要手动释放

      3. Synchonized是不可中断的,lock可以中断也可以不中断

      4. Synchonized不知道线程是否拿到锁,lock可以知道线程是否获得了锁

      5. Synchonized可以锁住方法和代码块,lock只能锁代码块

      6. Synchonized是非公平锁,ReentrantLock可以控制是否是公平锁,ReentrantLock默认是非公平的,创建的时候传入true就是公平锁了

    4. monitor是重量级锁:执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所synchronized是Java语 言中是一个重量(Heavyweight)的操作。

    5. 为什么monitor中操作系统用户态和内核态的转换会消耗大量的资源呢:因为用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器 值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在synchronized未优化之前,效率低的原因。

    6. CAS:Compare And Swap(比较相同再交换),CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中。
      CAS原理.png
  1. CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下,因为单核的话,多线程是cpu切换实现的,那其他线程被放在就绪队列里面的,根本没有执行,所以用不到CAS。

    1. 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。

    2. 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。

  2. 所以CAS就是一种乐观锁,而synchronized是一种悲观锁

  3. 对象的布局:对象头,实例数据,对齐填充

    1. 对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指针,及对象指向它的类元数据的指针。当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?是存在锁对象的对象头中的。

    2. Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(经过15次的GC后就会进入老年代)、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。

synchronized锁升级过程:

  • 偏向锁(当只有一个线程获取锁时):这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

    1. 当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:虚拟机将会把对象头中的标志位设为“01”,即偏向模式。

    2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何 同步操作,偏向锁的效率高。

    3. 偏向锁的撤销(当有线程竞争时,就不能使用偏向锁了,就要撤销偏向锁):1.偏向锁的撤销动作必须等待全局安全点(一个暂停的时刻,非常短);2.暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态3.撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态

    4. synchronized默认是开启的轻量级锁,延时4秒后才会打开偏向锁,为什么会延时呢?因为在synchronized代码块执行前,还会进行其他的操作,可能会用到synchronized,可能是并发的,所以我们不能一开始就设置它为偏向锁,---注意: 可以手动关闭偏向锁延时

    5. 偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。

    6. 如果默认就开启偏向锁的话,那么即使没执行到synchronized代码块,对象也会有偏向锁,此时为匿名偏向锁,对象头中是没有线程id的,当真正执行到synchronized代码块时,才把线程id写进去

  • 轻量级锁:当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁

    1. 引入轻量级锁的目的:引入轻量级锁的目的是在多线程交替执行(交替执行,而不是并发)同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

    2. 轻量级锁升级为重量级锁之前会默认使用自旋锁自旋10次,如果10次内能拿到锁,就不升级为重量级锁了,否则升级为重量级锁,后面又有了自适应的自旋锁,此时自旋的次数,和单次的时间就由虚拟机决定了,而不是固定的

  • 锁相关

    1. 锁消除:是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

    2. 锁粗化:JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。

    3. 优化:1.减少synchronized的代码块的作用范围,代码越少,执行的速度越快,那么发生竞争的概率就越小;2.降低synchronized锁的颗粒度,比如hashtable和concurrentHashMap,前者锁整个表,后者只锁链表和红黑树的首节点,肯定后者的效率更高;读写分离:读取时不加锁,写入和删除时才加锁

  • 用户态和内核态:
  1. 由 synchronized锁优化联想到用户态与内核态区别_未知的风fly-CSDN博客

  2. 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

你可能感兴趣的:(synchronized)