小伙伴你们好,我是jack xu,今天是清明假期,跟你们来聊一聊synchronized。本篇是并发编程中的第一篇,为何说是第一篇呢,由于并发编程涉及的东西太多太多,晦涩难懂,随便一个知识点拉出来均可以写一篇文章,如此算来写完并发编程一个系列最起码要十篇。我将知识点进行了总结概括,排类分类,用通俗易懂的方式来跟你们说清楚、讲明白。。java
这个问题很简单,首先咱们来看下面这个代码linux
这里稍微解释下为啥会得不到 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
好,本文的高潮来了,你们仔细听,在JDK的早期,synchronized叫作重量级锁,由于申请锁资源必须经过kernel,系统调用,从用户态 -> 内核态的转换,效率比较低,JDK1.6 以后作了一些优化,为了减小得到锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。所以你们会发如今 synchronized 中,锁存在四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁;多线程
咱们知道synchronized锁的是对象,对象就是Object,Object在heap中的布局,以下图所示并发
(1)若是成功markword则存储当前线程ID,接着执行同步代码块app
(2)若是是同一个线程加锁的时候,不须要争用,只须要判断线程指针是否同一个,可直接执行同步代码块jvm
(3)若是有其余线程已经得到了偏向锁,这种状况说明当前锁存在竞争,须要撤销已得到偏向锁的线程,而且把它持有的锁升级为轻量级锁(这个操做须要等到全局安全点,也就是没有线程在执行字节码)才能执行
在咱们的应用开发中,绝大部分状况下必定会存在 2 个以上的线程竞争,那么若是开启偏向锁,反而会提高获取锁的资源消耗。因此能够经过jvm参数UseBiasedLocking 来设置开启或关闭偏向锁
(1)默认状况下自旋的次数是 10 次,能够经过-XX:PreBlockSpin来修改,或者自旋线程数超过CPU核数的一半
(2)在 JDK1.6 以后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。若是在同一个锁对象上,自旋等待刚刚成功得到过锁,而且持有锁的线程正在运行中,那么虚拟机就会认为此次自旋也是颇有可能再次成功,进而它将容许自旋等待持续相对更长的时间。若是对于某个锁,自旋不多成功得到过,那在之后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源
知足这两种状况之一后升级为重量级锁
这时候就惊动老佛爷了,向操做系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操做系统的调度,而后再映射回用户空间。
咱们随便写一段简单的带有 synchronized 关键字的代码。先将其编译为.class 文件,而后使用 javap -c xxx.class 进行反汇编。咱们就能够获得 java 代码对应的汇编指令。里面能够找到以下两行指令。
java中每一个对象都关联了一个监视器锁monitor,当monitor被占用时就会处于锁定状态。线程执行monitorenter 指令时尝试获取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技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。