大话Synchronized及锁升级

前言

小伙伴你们好,我是jack xu,今天是清明假期,跟你们来聊一聊synchronized。本篇是并发编程中的第一篇,为何说是第一篇呢,由于并发编程涉及的东西太多太多,晦涩难懂,随便一个知识点拉出来均可以写一篇文章,如此算来写完并发编程一个系列最起码要十篇。我将知识点进行了总结概括,排类分类,用通俗易懂的方式来跟你们说清楚、讲明白。。java

为何要用Synchronized

这个问题很简单,首先咱们来看下面这个代码linux

开10000个线程,将变量count递增,结果是9998,很显然是出现了线程不安全。那为何会出现这样的结果呢,答案也很简单

这里稍微解释下为啥会得不到 100(知道的可直接跳过), i++ 这个操做,计算机须要分红三步来执行。
一、读取 i 的值。
二、把 i 加 1.
三、把 最终 i 的结果写入内存之中。
因此,(1)、假如线程 A 读取了 i 的值为 i = 0,(2)、这个时候线程 B 也读取了 i 的值 i = 0。
(3)、接着 A把 i 加 1,而后写入内存,此时 i = 1。(4)、紧接着,B也把 i 加 1,此时线程B中的 i = 1,
而后线程B 把 i 写入内存,此时内存中的 i = 1。也就是说,线程 A, B 都对 i 进行了自增,但最终的结果倒是1,不是 2.
复制代码

归根到底一句话就是这么多操做不是原子性,那怎么解决这个问题呢,加上Synchronized便可编程

三大特性

在上面例子演示的是原子性。synchronized 能够确保可见性,根据happens-before规定,在一个线程执行完 synchronized 代码后,全部代码中对变量值的变化都能当即被其它线程所看到。顺序性的话就是禁止指令重排,代码块中的代码从上往下依次执行,归根到底再一句话,并发问题中的三个特性synchronized都能保证,也就是synchronized是万金油,用他准没错!安全

使用方法

从语法上讲,Synchronized总共有三种用法:bash

  • 修饰实例方法
public synchronized void eat(){
	.......
  .......
}
复制代码
  • 修饰静态方法
public static synchronized void eat(){
	.......
  .......
}
复制代码
  • 修饰代码块
public void eat(){
   synchronized(this){
   	.......
 	.......
   }
}
复制代码
public void eat(){
   synchronized(Eat.class){
   	.......
 	.......
   }
}
复制代码

其中第一种和第三种对等,第二种和第四种对等,这个很简单,下面是使用 synchronized的总结:markdown

  • 选用一个锁对象,能够是任意对象;
  • 锁对象锁的是同步代码块,并非本身;
  • 不一样类型的多个 Thread 若是有代码要同步执行,锁对象要使用全部线程共同持有的同一个对象;
  • 须要同步的代码放到大括号中。须要同步的意思就是须要保证原子性、可见性、有序性中的任何一种或多种。不要放不须要同步的代码进来,影响代码效率。

锁升级

好,本文的高潮来了,你们仔细听,在JDK的早期,synchronized叫作重量级锁,由于申请锁资源必须经过kernel,系统调用,从用户态 -> 内核态的转换,效率比较低,JDK1.6 以后作了一些优化,为了减小得到锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。所以你们会发如今 synchronized 中,锁存在四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁;多线程

咱们知道synchronized锁的是对象,对象就是Object,Object在heap中的布局,以下图所示并发

前面8个字节就是markword,后面4个字节是class pointer就是这个对象属于哪一个类的,People就是People.class,Cat类就是Cat.class,在后面实例数据就是看你类里面字段的具体大小了,int age就是4个字节,string name就是英文1个字节, 中文2个字节(String的中文字节数要看用的编码集合,若是是utf-8类型的,那么中文占2到3个字节,若是是GBK类型的,那么中文占2个字节),最后前面三项加起来不能被8整除的,就是补齐到可以被8整除。下图就是markword(8*8=64位)的分布图,锁升级就是markdown里面标志位的变化。

网上因此的图都是32位的,我这里画的是64位的,你们发现一共有五种状态,用两位是不够的,因此01的时候在向前借一位。

偏向锁

hotspot虚拟机的做者通过调查发现,大部分状况下,加锁的代码不只仅不存在多线程竞争,并且老是由同一个线程屡次得到。因此基于这样一个几率,咱们一开始加锁上的是偏向锁,当一个线程访问加了同步锁的代码块时,首先会尝试经过CAS操做在对象头中存储当前线程的ID

(1)若是成功markword则存储当前线程ID,接着执行同步代码块app

(2)若是是同一个线程加锁的时候,不须要争用,只须要判断线程指针是否同一个,可直接执行同步代码块jvm

(3)若是有其余线程已经得到了偏向锁,这种状况说明当前锁存在竞争,须要撤销已得到偏向锁的线程,而且把它持有的锁升级为轻量级锁(这个操做须要等到全局安全点,也就是没有线程在执行字节码)才能执行

在咱们的应用开发中,绝大部分状况下必定会存在 2 个以上的线程竞争,那么若是开启偏向锁,反而会提高获取锁的资源消耗。因此能够经过jvm参数UseBiasedLocking 来设置开启或关闭偏向锁

轻量级锁

撤销偏向锁,升级轻量级锁,每一个线程在本身的线程栈生成LockRecord,用CAS操做将markword设置为指向本身这个线程的LR的指针,设置成功者获得锁。 轻量级锁在加锁过程当中,用到了自旋锁,自旋锁的使用,其实也是有必定条件的,若是一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。

(1)默认状况下自旋的次数是 10 次,能够经过-XX:PreBlockSpin来修改,或者自旋线程数超过CPU核数的一半

(2)在 JDK1.6 以后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。若是在同一个锁对象上,自旋等待刚刚成功得到过锁,而且持有锁的线程正在运行中,那么虚拟机就会认为此次自旋也是颇有可能再次成功,进而它将容许自旋等待持续相对更长的时间。若是对于某个锁,自旋不多成功得到过,那在之后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源

知足这两种状况之一后升级为重量级锁

重量级锁

这时候就惊动老佛爷了,向操做系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操做系统的调度,而后再映射回用户空间。

咱们随便写一段简单的带有 synchronized 关键字的代码。先将其编译为.class 文件,而后使用 javap -c xxx.class 进行反汇编。咱们就能够获得 java 代码对应的汇编指令。里面能够找到以下两行指令。

字节码层面就是关键的这两条指令,monitorenter,moniterexit  (注:代码块用的是ACC_SYNCHRONIZED,这是一个标志位,底层原理仍是这两条指令)

java中每一个对象都关联了一个监视器锁monitor,当monitor被占用时就会处于锁定状态。线程执行monitorenter 指令时尝试获取monitor的全部权,过程以下:

  • 若是monitor的进入数为 0,则该线程进入monitor,而后将进入数设置为1,该线程即为monitor 的全部者。
  • 若是线程已经占有该monitor,只是从新进入,则进入monitor的进入数加 1。
  • 若是其余线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为 0,再从新尝试获取monitor的全部权。

从上面过程能够看出两点,第一:monitor是可重入的,他有计数器,第二:monitor是非公平锁

monitor 依赖操做系统的mutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会致使系统在用户态与内核态之间来回切换,严重影响锁的性能

锁消除

咱们都知道 StringBuffer 是线程安全的,由于它的关键方法都是被 synchronized修饰过的,但咱们看上面这段代码,咱们会发现,sb 这个引用只会在 add方法中使用,不可能被其它线程引用(由于是局部变量,栈私有),所以 sb是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。

public void add(String str1,String str2){
         StringBuffer sb = new StringBuffer();
         sb.append(str1).append(str2);
}
复制代码

总结

好,本文对synchronized所涵盖的知识点已经讲解的很清楚了。synchronized是Java并发编程中最经常使用的用于保证线程安全的方式,其使用相对也比较简单。在synchronized优化之前,synchronized的性能是比ReentrantLock差不少的,可是自从synchronized引入了偏向锁,轻量级锁(自旋锁)后,二者的性能就差很少了。 在两种方法均可用的状况下,官方甚至建议使用synchronized,其实synchronized的优化我感受就借鉴了ReentrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

相关文章
相关标签/搜索