美文网首页Java工程师知识树
Java基础-并发编程-synchronized关键字使用与原理

Java基础-并发编程-synchronized关键字使用与原理

作者: HughJin | 来源:发表于2021-02-04 09:30 被阅读0次

    Java工程师知识树 / Java基础


    synchronized的使用

    JDK针对共享资源数据同步问题有一种方式为使用synchronized关键字,synchronized提供了一种排他锁机制,可以让程序在同一时间段内只有一个线程执行某些操作。

    使用synchronized修饰执行内容后:

    package com.thread.study;
    
    public class TicketWindow implements Runnable {
    
        public static int TICKET_NUM = 10;
    
        @Override
        public void run() {
            while (true) {
                synchronized (this) {
                    if (TICKET_NUM > 0) {
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "卖了票号为" + TICKET_NUM-- + "的票");
                    } else {
                        return;
                    }
                }
            }
        }
    
        public static void main(String[] args) {
    
            TicketWindow ticketWindow = new TicketWindow();
    
            Thread t1 = new Thread(ticketWindow, "1号售票窗口");
            Thread t2 = new Thread(ticketWindow, "2号售票窗口");
            Thread t3 = new Thread(ticketWindow, "3号售票窗口");
    
            t1.start();
            t2.start();
            t3.start();
    
        }
    }
    // 执行结果
    1号售票窗口卖了票号为10的票
    3号售票窗口卖了票号为9的票
    2号售票窗口卖了票号为8的票
    2号售票窗口卖了票号为7的票
    2号售票窗口卖了票号为6的票
    2号售票窗口卖了票号为5的票
    2号售票窗口卖了票号为4的票
    2号售票窗口卖了票号为3的票
    2号售票窗口卖了票号为2的票
    2号售票窗口卖了票号为1的票
    

    将synchronized改为修改run()方法:

    package com.thread.study;
    
    public class TicketWindow implements Runnable {
    
        public static int TICKET_NUM = 10;
    
        @Override
        public synchronized void run() {
            while (true) {
                if (TICKET_NUM > 0) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖了票号为" + TICKET_NUM-- + "的票");
                } else {
                    return;
                }
            }
        }
    
        public static void main(String[] args) {
    
            TicketWindow ticketWindow = new TicketWindow();
    
            Thread t1 = new Thread(ticketWindow, "1号售票窗口");
            Thread t2 = new Thread(ticketWindow, "2号售票窗口");
            Thread t3 = new Thread(ticketWindow, "3号售票窗口");
    
            t1.start();
            t2.start();
            t3.start();
    
        }
    }
    //执行结果 不管执行多少次都是
    1号售票窗口卖了票号为10的票
    1号售票窗口卖了票号为9的票
    1号售票窗口卖了票号为8的票
    1号售票窗口卖了票号为7的票
    1号售票窗口卖了票号为6的票
    1号售票窗口卖了票号为5的票
    1号售票窗口卖了票号为4的票
    1号售票窗口卖了票号为3的票
    1号售票窗口卖了票号为2的票
    1号售票窗口卖了票号为1的票
    

    通过上述两个例子对比,总结synchronized的使用:

    1. 由于synchronized关键字存在排他性,也就是说所有的线程必须串行地经过 synchronized保护的共享区域,如果synchronized作用域越大,则代表着其效率越低,甚至还会丧失并发的优势
    2. synchronized关键字应该尽可能地只作用于共享资源(数据)的读写作用域,或者说是synchronized锁的是有增删改操作的对象。eg:
    List<String> list = new ArrayList<String>();
    for (int i = 0; i < 10000; i++) {
        new Thread(() -> {
            synchronized (list) {//synchronized锁的是有增删改操作的对象
                list.add(Thread.currentThread().getName());
            }
        }, String.valueOf(i)).start();
    }
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(list.size());
    

    synchronized 关键字原理

    synchronized说明:

    synchronized 关键字是解决共享资源数据同步的常用解决方案,有以下三种使用方式:

    • 同步普通方法,锁的是当前对象
    • 同步静态方法,锁的是当前 Class 对象。
    • 同步块,锁的是 {} 中的对象。

    synchronized 实现原理:

    JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。

    具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和可能发生异常处插入 monitor.exit 的指令。monitor指令是使用C++实现的。

    其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。

    而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。

    synchronized 特性:

    • 互斥性(确保线程互斥的访问同步代码)
    • 可见性(保证共享变量的修改能够及时可见)
    • 有序性(有效解决重排序问题)
    互斥性

    1.互斥性,可以认为独享的意思,每次只允许一个操作者拥有共享资源;
    2.被synchronized修饰的代码块、实例方法、静态方法,多线程并发访问时,只能有一个线程获取到锁,其它线程都处于阻塞等待,但在此期间,这些线程仍然可以访问其它非synchronized修饰的方法;

    可见性

    1.可见性,就是每次线程的到来,都能访问到最新的值;
    2.因为在互斥性的基础上,由于每次仅有一个线程执行临界区的代码,因此其修改的任何变量值对于稍后执行该临界区的线程来说是可见的;
    3.因为互斥性的存在,也保证了临界区变量修改的原子性,而volatile仅仅只能保证变量修改的可见性,并不能保证原子性;

    有序性

    1.有序性,就是按照顺序来执行;
    2.同样因为在互斥性的基础上,代码块也好,实例方法或静态方法也好,一旦被synchronized后,各个线程相互竞争,反正每次只能有一个线程执行;
    3.打个比方,举例静态方法,TestSynchronized.java 中有个静态 synchronized static test(){ i++, j++} 方法,并且代码块被synchronized修饰,让N个线程都去调用这个方法,最后会发现每次i和j的输出值都是一样的。i++和j++要么一起执行完,要么都不执行,不会出现先i++后,执行了其他代码,过一会再执行j++的情况。

    流程图如下:

    同步代码块

    public class Synchronize{
        public static void main(String[] args) {
            synchronized (Synchronize.class){
                System.out.println("Synchronize");
            }
        }
    }
    ---------使用 javap-c 编译 Synchronize类 可以查看编译之后的具体信息。-----------
    {
      public com.thread.study.Synchronize();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=3, args_size=1
             0: ldc           #2                  // class com/thread/study/Synchronize
             2: dup
             3: astore_1
             4: monitorenter    //同步方法调用前加入一个 monitor.enter 指令
             5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
             8: ldc           #4                  // String Synchronize
            10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            13: aload_1
            14: monitorexit     //退出的地方插入 monitor.exit 的指令
            15: goto          23
            18: astore_2
            19: aload_1
            20: monitorexit     //异常可能出现的地方插入 monitor.exit 的指令 确保可以正常退出
            21: aload_2
            22: athrow
            23: return
          Exception table:
             from    to  target type
                 5    15    18   any
                18    21    18   any
          LineNumberTable:
            line 5: 0
            line 6: 5
            line 7: 13
            line 8: 23
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 18
              locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
            frame_type = 250 /* chop */
              offset_delta = 4
    }
    SourceFile: "Synchronize.java"
    

    可以看到在同步块的入口和出口分别有 monitorenter,monitorexit指令。

    monitorenter指令JVM规范翻译:
    ​ 每个对象有打自娘胎出来就自带一个内置监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
    ​ • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
    ​ • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
    ​ • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    monitorexit指令JVM规范翻译:
    ​ • 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
    ​ • 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

    同步方法

    package com.thread.study;
    
    public class TicketThread {
    
        public static void main(String[] args) {
            TicketRunnable ticketRunnable = new TicketThread.TicketRunnable();
            Thread t1 = new Thread(ticketRunnable,"zhao");
            Thread t2 = new Thread(ticketRunnable,"qian");
            t1.start();
            t2.start();
        }
    
        static class TicketRunnable implements Runnable{
            private int TICKET_NUM = 10;
            @Override
            public synchronized void run() {  // 同步方法
                if (TICKET_NUM > 0) {
                    System.out.println(Thread.currentThread().getName() +TICKET_NUM);
                    TICKET_NUM --;
                }
            }
    
        }
    }
    ----------使用 javap-c 编译 TicketThread 可以查看编译之后的具体信息。主要看内部类TicketRunnable------------
    
    {
      com.thread.study.TicketRunnable();
        descriptor: ()V
        flags:
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 16: 0
    
      public synchronized void run();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED     // ACC_SYNCHRONIZED标志
        Code:
          stack=3, locals=1, args_size=1
             0: getstatic     #2                  // Field TICKET_NUM:I
             3: ifle          45
             6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
             9: new           #4                  // class java/lang/StringBuilder
            12: dup
            13: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
            16: invokestatic  #6                  // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
            19: invokevirtual #7                  // Method java/lang/Thread.getName:()Ljava/lang/String;
            22: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            25: getstatic     #2                  // Field TICKET_NUM:I
            28: invokevirtual #9                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
            31: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            34: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            37: getstatic     #2                  // Field TICKET_NUM:I
            40: iconst_1
            41: isub
            42: putstatic     #2                  // Field TICKET_NUM:I
            45: return
          LineNumberTable:
            line 20: 0
            line 21: 6
            line 22: 37
            line 24: 45
          StackMapTable: number_of_entries = 1
            frame_type = 45 /* same */
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=1, locals=0, args_size=0
             0: bipush        10
             2: putstatic     #2                  // Field TICKET_NUM:I
             5: return
          LineNumberTable:
            line 17: 0
    }
    SourceFile: "TicketThread.java"
    

    可以看出在synchronized修饰的同步方法的flags中会有ACC_SYNCHRONIZED标识

    ACC_SYNCHRONIZED指令JVM规范翻译:
    ​ • 方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。
    ​ • 当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,
    ​ • 如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。
    ​ • 这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
    ​ • 值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

    无论是monitorenter、 monitorexit,或者是ACC_SYNCHRONIZED,其都是基于Monitor机制实现的。

    Monitor

    monitor直译过来是监视器的意思,专业一点叫管程。Monitor机制一个重要特点是,在同一时间,只有一个线程/进程能进入monitor所定义的临界区,这使得monitor能够实现互斥的效果无法进入monitor的临界区的进程/线程,应该被阻塞,并且在适当的时候被唤醒

    java则基于monitor机制实现了它自己的线程同步机制,就是synchronized内置锁。

    基本元素
    • 临界区

    临界区是被synchronized包裹的代码块,可能是个代码块,也可能是个方法。

    • monitor对象和锁

    monitor对象是monitor机制的核心,它本质上是jvm用c语言定义的一个数据类型。对应的数据结构保存了线程同步所需的信息,比如保存了被阻塞的线程的列表,还维护了一个基于mutex的锁,monitor的线程互斥就是通过mutex互斥锁实现的。

    • 条件变量

    条件变量和下方wait signal方法的使用有密切关系 。在获取锁进入临界区之后,如果发现条件变量不满足使用wait方法使线程阻塞,条件变量满足后signal唤醒被阻塞线程。 tips:当线程被signal唤醒之后,不是从wait那继续执行的,而是重新while循环一次判断条件是否成立

    • 定义在monitor对象上的wait,signal操作

    相关文章

      网友评论

        本文标题:Java基础-并发编程-synchronized关键字使用与原理

        本文链接:https://www.haomeiwen.com/subject/tobgzktx.html