synchronized原理

Posted 意犹未尽

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了synchronized原理相关的知识,希望对你有一定的参考价值。

在多线程中同时进行i++操作 不能保证i的原子性。i++ 可以看作为为以下几个步骤

1.读取i的值

2.计算i+1

3.赋值

在多线程下 可能还在没有来得及赋值 其他线程已经复制,再赋值就是脏数据

synchronized则能保证原子性。synchronized 一个线程获得锁对象则会将对象标记为锁定状态。执行完毕之后释放锁

synchronize的三个特性使用方式

原子性

如i++  分为读取 计算  复制, 在这3步没有执行完毕之前 其他线程不能执行

可见性

在原子性的前提下,因为释放锁会将最新值刷入主内存。保证后面获取所得线程获取到的是最新的值

有序性

Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性

可重入性

synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

synchronize的三种使用方式

  • 修饰实例方法
  • 修饰静态方法
  • 修饰代码块

修饰实例方法

.... 
 public synchronized   void  account(){
        i++;
    }
....

 

修饰静态方法

... 
public static synchronized   void  account(){
        i++;
    }
...

修饰代码块

使用this

 

 public   void  account(){
        synchronized (this) {
             i++;
        }
    }

使用对象

int i=0;
    Object lockObj=new Object();
    public   void  account(){
        synchronized (lockObj) {
             i++;
        }
    }

使用class

 public   void  account(){
        synchronized (Accounting.class) {
             i++;
        }
    }

 

sychronized原理

反编译

 * @author liqiang
 * @date 2020/3/30 15:52
 * @Description: (what)
 * (why)
 * (how)
 */
public class Accounting implements Runnable {
    int i=0;
    public  void  account(){
        synchronized (this) {

            i++;
        }
    }
    @Override
    public void run() {
        for (int j=0;j<2000;j++){
            account();

        }
    }
    public int getI() {
        return i;
    }
    public static void main(String[] args) throws InterruptedException {
        Accounting accounting= new Accounting();
        Thread t=new Thread(accounting,"a1");
        Thread t2=new Thread(accounting,"a2");
        t.start();
        t2.start();
        t.join();//主线程挂起等待这个线程执行完毕在网下执行
        t2.join();
        System.out.print(accounting.getI());
    }
}

1.编译class文件

javac -encoding UTF-8 Accouting.java //先运行编译class文件命令

2.打印

javap -v Accouting.class //再通过javap打印出字节文件
 public void account();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter //monintorenter指令
         4: aload_0
         5: dup
         6: getfield      #2                  // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field i:I
        14: aload_1
        15: monitorexit //monitorexit指令
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             4    16    19   any
            19    22    19   any
      LineNumb 

在进入monitorenter指令后 线程将持有Monitor,在进入monnitorexit指令后将释放Monitor对象

monitor的实现类是ObjectMonitor

主要成员包括_WaitSet  _EntryList  _Owner 用来保存ObjectWaiter对象(每个等待锁的线程都会封装成ObjectWaiter)

当多线程同时访问一段同步代码块会首先进入_EntryList状态为block,获得锁的线程则会设置到_oWner 同时monitor对象的count+1

如果调用线程的wait方法则清空_oWner count-1 同时当前线程进入_WaitSet等待被唤醒

如果当前线程执行完毕也将清空_Owner count-1

其他block线程再次发起竞争

monitor结构

ObjectMonitor() {
   _header = NULL;
   _count = 0; //记录个数
   _waiters = 0,
   _recursions = 0;
   _object = NULL;
   _owner = NULL;
   _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
   _WaitSetLock = 0 ;
   _Responsible = NULL ;
   _succ = NULL ;
   _cxq = NULL ;
   FreeNext = NULL ;
   _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
   _SpinFreq = 0 ;
   _SpinClock = 0 ;
   OwnerIsThread = 0 ;
} 

线程中断

public void interrupt();//只能中断阻塞线程 需要用异常捕获
public  boolean isInterrupted();
public static boolean interrupted();
package com.liqiang.sychronize;

public class Accounting implements Runnable {
    int i = 0;

    public void account() {

    }

    @Override
    public void run() {
        try {
            while (true) {

                Thread.sleep(2000);

                System.out.println("1");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public int getI() {
        return i;
    }

    public static void main(String[] args) throws InterruptedException {
        Accounting accounting = new Accounting();
        Thread t = new Thread(accounting, "a1");
        t.start();
        Thread.sleep(4000);
        t.interrupt();
        System.out.println(t.isInterrupted());
        t.join();//主线程挂起等待这个线程执行完毕在往下执行

    }
}
interrupt方法只能中断阻塞线程(需要try捕获异常 否则会一直执行下去)非阻塞线程需要我们手动中断
package com.liqiang.sychronize;

public class Accounting implements Runnable {
    int i = 0;

    public void account() {

    }

    @Override
    public void run() {

            while (true) {
                    if(Thread.currentThread().isInterrupted()){
                        break;
                    }
                System.out.println("1");
            }

    }

    public int getI() {
        return i;
    }

    public static void main(String[] args) throws InterruptedException {
        Accounting accounting = new Accounting();
        Thread t = new Thread(accounting, "a1");
        t.start();
        Thread.sleep(4000);
        t.interrupt();
        System.out.println(t.isInterrupted());
        t.join();//主线程挂起等待这个线程执行完毕在网下执行

    }
}

sleep和wait的区别

Thread.sleep 与wait的区别  wait会将线程锁释放 线程保存到monitor的 _waitSet里面 等待被唤醒。 sleep是线程休眠并不释放锁

jdk1.6的锁优化

在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。

Mark Word记录了对象和锁的相关信息

  锁升级主要由Mark Word 中的锁标志位和释放偏向锁标志位,Synchronized 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。

偏向锁

偏向锁主要用来优化同一线程多次申请同一个锁的竞争。在某些情况下,大部分时间是同一个线程竞争锁资源

线程执行同步代码块的时候 只需要到Mark Word 中去判断一下是否有偏向锁指向它的 ID,无需再进入 Monitor 去竞争对象了。当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是 01,“是否偏向锁”标志位设置为 1,并且记录抢到锁的线程 ID,表示进入偏向锁状态。

一旦出现其它线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁,反之则被其它线程抢占

偏向锁生成的流程:摘自《极客时间-java调优实战》

public class Accounting implements Runnable {
    int i=0;
    public  void  account(){
        synchronized (this) {
            i++;
        }
    }
    @Override
    public void run() {
        for (int j=0;j<2000;j++){
            account();

        }
    }
    public int getI() {
        return i;
    }
    public static void main(String[] args) throws InterruptedException {
        Accounting accounting= new Accounting();
        Thread t=new Thread(accounting,"a1");
        t.start();
        t.join();//主线程挂起等待这个线程执行完毕在网下执行
        Thread t2=new Thread(accounting,"a2");
        t2.start();
        t2.join();
        System.out.print(accounting.getI());
    }
}

上面这种情况虽然有2个线程 但是不存在锁竞争所以偏向锁 就发生了优化的作用。但是,在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁就会被撤销,发生 stop the word 后, 开启偏向锁无疑会带来更大的性能开销,这时我们可以通过添加 JVM 参数关闭偏向锁来调优系统性能,

-XX:-UseBiasedLocking //关闭偏向锁(默认打开)
或者
-XX:+UseHeavyMonitors  //设置重量级锁

轻量级锁

当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。

public class Accounting implements Runnable {
    int i=0;
    public  void  account(){
        synchronized (this) {
            i++;
        }
    }
    @Override
    public void run() {
        for (int j=0;j<2000;j++){
            account();

        }
    }
    public int getI() {
        return i;
    }
    public static void main(String[] args) throws InterruptedException {
        Accounting accounting= new Accounting();
        Thread t=new Thread(accounting,"a1");
        t.start();
        Thread t2=new Thread(accounting,"a2");
        t2.start();
        t2.join();
        t.join();//主线程挂起等待这个线程执行完毕在网下执行
        System.out.print(accounting.getI());
    }
}

虽然a1和a2存在竞争,但是当a1执行期间 a2 cas自旋重试获取锁(固定重试次数 如果重试失败则暂停所有线程 开始升级锁)。a1不是耗时操作0.几毫秒释放后a2继续执行

所以针对存在竞争但是竞争不大的情况下轻量级锁才能发挥做用

偏向锁升级为轻量级锁图 摘自《极客时间java性能调优实战》

自旋锁与重量级锁

轻量级锁 CAS 抢锁失败,线程将会被挂起进入阻塞状态。如果正在持有锁的线程在很短的时间内释放资源,那么进入阻塞状态的线程无疑又要申请锁资源。JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞。这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 设置决定,这里我不建议设置的重试次数过多,因为 CAS 重试操作意味着长时间地占用 CPU。自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。、

重量级锁图,摘自:《极客时间-java调优实战

锁粗化

就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。

优化前

 

    public  void  account(){
        ArrayList<Integer> arrs=new ArrayList<>();
        for(int i=0;i<10;i++){
            synchronized (Accounting.class){
                arrs.add(i);
            }
        }
    }

 

优化后

  public void account() {
        synchronized (Accounting.class) {
            ArrayList<Integer> arrs = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                arrs.add(i);

            }
        }
    }

锁消除

 public void account() {
        StringBuilder stringBuilder=new StringBuilder();
        stringBuilder.append("1");
    }

 

个人理解 虽然StringBuilder的append是同步方法,但是StringBuilder是方法内变量 不存在锁竞争 就会吧同步锁去掉

 

以上是关于synchronized原理的主要内容,如果未能解决你的问题,请参考以下文章

synchronize底层原理

Synchronized及其实现原理

从三个层面解析synchronized原理

synchronized原理是啥?

synchronized 原理分析

多线程之synchronized实现原理