zoukankan      html  css  js  c++  java
  • synchronized的原理

    synchronized的使用

      synchronized是一个java中的关键字,是基于JVM层面的,用于保证java的多线程安全,它具有四大特性,可用于完全替代volatile:

    • 原子性:所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
    • 可见性:可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。而volatile的实现类似,被volatile修饰的变量,每当值需要修改时都会立即更新主存,主存是共享的,所有线程可见,所以确保了其他线程读取到的变量永远是最新值,保证可见性。
    • 有序性:synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
    • 可重入性:synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,可以进入,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

    synchronized的四大特性保证了多线程在操作共享资源时的安全。synchronized的使用方法如下:

    package com.javaBase.LineDistance;
    
    /**
     * 〈一句话功能简述〉;
     * 〈功能详细描述〉
     *
     * @author jxx
     * @see [相关类/方法](可选)
     * @since [产品/模块版本] (可选)
     */
    public class TestSynchronized {
    
        static TestSynchronized testSynchronized = new TestSynchronized();
    
        private synchronized void method1() {
            System.out.println("修饰实例方法。锁为实例对象。");
        }
    
        private static synchronized void method2() {
            System.out.println("修饰静态方法。锁为类对象。");
        }
    
        private static synchronized void method3() {
    
            synchronized (TestSynchronized.class) {
                System.out.println("修饰代码块,锁对象为类对象。");
            }
    
            synchronized (testSynchronized) {
                System.out.println("修饰代码块,锁对象为实例对象。");
            }
        }
    }

    synchronized可以修饰实例方法,静态方法,代码块,但她的锁资源只有两种,即类锁和对象锁。当使用对象锁时,稍有不慎会出问题,且看下面的代码:

    package com.javaBase.LineDistance;
    
    /**
     * 〈一句话功能简述〉;
     * 〈功能详细描述〉
     *
     * @author jxx
     * @see [相关类/方法](可选)
     * @since [产品/模块版本] (可选)
     */
    public class TestSynchronized2 implements Runnable {
    
        public static int i = 0;
    
        private synchronized void add () {
            i++;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                add();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(new TestSynchronized2());
            Thread t2 = new Thread(new TestSynchronized2());
    
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            System.out.println(i);
        }
    }

    运行结果:

    169816

    将add方法改为:

    private static synchronized void add () {
            i++;
        }

    便可正常运行。运行出错的原因是t1,t2 new了两个实例,导致进入add方法的线程持有的不是同一个锁,因此操作共享数据时出错,但若add变为静态方法,那么add同步锁就变为了类锁,类锁始终只有一个,因此运行结果正常。

    synchronized原理

       了解synchronized原理之前需要先知道java对象头的概念。

    java实例对象在堆中的结构如上图,包含对象头,实例变量,填充数据。实例变量保存的是类的属性信息以及父类的属性信息。填充数据用于补齐字节数,jvm要求对象的起始字节必须为8的整数倍。java对象头存储着synchronized的锁信息,它由Mark Word 和 Class Metadata Address两部分组成,Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address存储类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。那么与synchronized密切相关的就是Mark Word,Mark Word的详细结构如下:

     下面来研究下synchronized在同步方法和同步代码块两种情况下线程是如何获取锁和释放锁的。我们先对同步方法进行反编译,结果如下:

     public synchronized void add();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field i:I
             3: iconst_1
             4: iadd
             5: putstatic     #2                  // Field i:I
             8: return
          LineNumberTable:
            line 14: 0
            line 15: 8

    JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

    下面对同步代码块进行反编译:

     public void add();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=3, args_size=1
             0: ldc           #2                  // class TestSynchronized2
             2: dup
             3: astore_1
             4: monitorenter
             5: getstatic     #3                  // Field i:I
             8: iconst_1
             9: iadd
            10: putstatic     #3                  // Field i:I
            13: aload_1
            14: monitorexit
            15: goto          23
            18: astore_2
            19: aload_1
            20: monitorexit
            21: aload_2
            22: athrow
            23: return

    从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

     synchronized优化

     参见另一篇博客:多线程锁的升级(膨胀)原理

    参考链接:synchronized使用及原理解析

           深入理解Java并发之synchronized实现原理   

           深入理解synchronized底层原理,一篇文章就够了!

  • 相关阅读:
    第一节:Node.js简介
    Socket实现java服务端与AndroidApp端数据交互
    zz Android Studio --“Cannot resolve symbol” 解决办法
    git 阿里云代码托管
    zzvisual studio系列(vs)启动调试网站使用ip+端口局域网访问
    阿里云的域名给七牛云的配置CDN和ssl
    win10 visual studio IIS Express 局域网调试,默认只能localhost
    window2012 iis8.0 配置https 默认居然是TLS1.1
    andriod 连接wcf ,HttpURLConnection FileNotFoundException
    zzWCF实现RESTFul Web Service
  • 原文地址:https://www.cnblogs.com/jxxblogs/p/11911547.html
Copyright © 2011-2022 走看看