zoukankan      html  css  js  c++  java
  • 【Java并发编程学习笔记】synchronized关键字

    线程安全问题

        表现为三个方面:原子性,可见性和有序性。

        原子性:对于共享变量,当前线程一旦操作,在其他线程看来是不可分割的。当前线程要么操作完毕,要么没有操作,操作过程中的中间结果,其他线程是不可见的。

        可见性:一个线程对共享变量进行修改以后,另外一个线程没办法立即看到。由于每一个线程都从主内存复制了一份共享变量到自己的内存中,所以就导致了可见性问题。

        有序性:编译器为了优化性能,有时候会改变程序中语句的先后顺序,这种优化不会影响程序的执行结果,但是有时候也可能导致线程安全问题。

    synchronized关键字

          synchronized是一个内部锁,它可以修饰方法和代码块。在多线程环境下,同步方法或者代码块在同一个时刻只能有一个线程执行,其余线程只能阻塞或者等待获取锁,也就是乐观锁。

    • synchronized在上锁的过程中有一个锁升级的过程

      • 无锁:刚把对象new出来
      • 偏向锁:第一次上锁的时候加偏向锁
      • 轻量级锁(无锁,自旋锁,自适应锁):一旦多个线程争用锁对象,升级为轻量级锁
      • 重量级锁:多个线程竞争锁比较激烈的时候,升级为重量级锁
    • 锁升级底层过程
      • Java代码中使用synchronized
      • 编译生成字节码文件,monitor监视锁状态
      • monitorenter:获取锁对象
        • 无锁、偏向锁、轻量级锁、重量级锁
          • lock_comxchg来控制锁 
      • monitorexit:释放锁对象

    synchronized是如何解决线程安全问题

    原子性

    synchronized锁住共享资源,能够保证一次只允许一个线程操作共享资源。

    public class Main {
        static int num = 0;
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; ++ i) { // 不加synchronized 27375
                        synchronized (Main.class) { // 加synchronized 30000
                            num ++;
                        }
                    }
                }
            };
            Thread t1 = new Thread(runnable);
            Thread t2 = new Thread(runnable);
            Thread t3 = new Thread(runnable);
            t1.start(); t2.start(); t3.start();
            t1.join(); t2.join(); t3.join();
            System.out.println(num);
        }
    }

    可见性

        使用synchronized以后,Lock原子操作首先会从主存中复制一份共享资源到自己的内存中,然后修改共享资源以后将更新后的值刷新到主内中中,这样一来主内中的值对于其他的线程就可见了。

    public class Main {
        static boolean flag = true;
        static int num = 0;
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                while (flag) {
                    // 没有synchronized的时候,程序在打印"线程2:flag = false"之后会进入死循环
                    // 因为t1会将flag复制一份到自己的内存中,因为没有synchronized
                    // 所以flag一直使用的是自己内存中的值,因此当主内存中的值flag=false的时候
                    // 当前线程依然没有停下,当使用synchronized的时候,当前线程就回去主内存中更新flag的值
                    // 程序自然也就停下来了
                    System.out.println("--------"); // 相当于使用了synchronized
                    //public void println(String x) {
                    //    synchronized (this) { 有synchronized就会刷新t1工作内存中的flag值
                    //        print(x);
                    //        newLine();
                    //    }
                    //}
                }
            });
            Thread.sleep(2000);
            t1.start();
            Thread t2 = new Thread(() -> {
                flag = false;
                System.out.println("线程2:flag = false"); //源代码中有synchronized,会执行lock执行原子操作
            });
            t2.start();
        }
    }

    有序性

    • 指令重排序是为了保证程序的执行效率,加了synchronized以后仍然可能出现指令重排序,但是synchronized可以保证指令重排序以后,单线程情况下最终的结果是不会改变的。

      • synchronized遵循as-if-serial语义:不管编译器和CPU如何重排序,保证单线程情况下程序的运行结果依然是正确的。
      • volatile遵循happened-befores规则:不管编译器和CPU如何重排序,保证多线程情况下程序的运行结果是正确的。
      • 这两个规则都是为了保证在不改变程序结果的情况下,尽量的提高程序的并行度。

    synchronized特性

    可重入性

        一个线程可以重复使用synchronizedsynchronized的锁对象中有一个计数器recursions记录当前线程获取了多少次锁,计数器为0的时候就释放锁,这样可以更好的封装代码,避免死锁。

    public class Main {
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (Main.class) {
                        System.out.println("---------------");
                        test();
    
                    }
                }
                private void test() {
                    synchronized (Main.class) {
                        System.out.println("=============");
                    }
                }
            };
            new Thread(runnable).start();
            new Thread(runnable).start();
        }
    }

    不可中断性

        当前线程一旦获取了锁,其他线程要想获取锁对象,必须等到当前线程释放锁以后,其他线程才能获取锁,如果当前线程持有锁的时候,其他的线程必须处于堵塞或者等待状态,当前线程不能中断。

    synchronized底层原理

          synchronized的锁对象的对象头Mark Down中关联了一个monitorJVM的线程执行到同步代码块的时候,如果发现锁对象中没有monitor就会创建一个,monitor有两个比较重要的变量:owner(拥有锁的线程)、recursions(计数器)。一个线程拥有了monitor之后,其他的线程只能进入阻塞或者等待状态。monitor是重量级锁。monitorenter:获取锁;monitorexit:释放锁。

    Java对象布局

    • 一个对象在内存中分为三个区域:对象头、实例数据、对齐数据
    • 64位系统中,对象头占16字节,但是JVM默认开启了指针压缩,将对象头压缩为12字节
      • Mark Down8字节):关于synchronized的所有信息(锁信息)都存储在这里
        • 001:无锁
        • 101:偏向锁
        • 00:轻量级锁
        • 10:重量级锁
      • 类型指针(8字节):class pointer,对象是属于哪一个class类的
      • 实例数据:成员变量占用的字节
      • 对齐数据:整体的字节数不是8的倍数的时候需要补齐到8的倍数个字节
    • 查看对象布局:在项目pom文件中,引入依赖。在Java代码中使用API
        <dependencies>
            <dependency>
                <groupId>org.openjdk.jol</groupId>
                <artifactId>jol-core</artifactId>
                <version>0.9</version>
            </dependency>
        </dependencies>
    public class Main {
        public static void main(String[] args) {
            Obj obj = new Obj();
            obj.hashCode();
            String s = ClassLayout.parseInstance(obj).toPrintable();
            System.out.println(s);
        }
    }
    class Obj {
        int val;
        boolean flag;
    }
    /*
    JVM默认开启了指针压缩,将对象头压缩为12个字节(如果不压缩则为16字节)
    1265094477
    4b67cf4d //对应下面的对象头看一看
    syn.Obj object internals:
     OFFSET  SIZE      TYPE DESCRIPTION      VALUE
          0     4           (object header)  01 4d cf 67 (00000001 01001101 11001111 01100111) (1741638913)
          4     4           (object header)  4b 00 00 00 (01001011 00000000 00000000 00000000) (75)
          8     4           (object header)  43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
         12     4       int Obj.val          0 // int占用4个字节
         16     1   boolean Obj.flag         false // boolean占用1个字节
         17     7           (loss due to the next object alignment) // 字节不够8的倍数,补充7个字节
    Instance size: 24 bytes //一共24个字节
    Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
    */

    偏向锁

          JDK1.6引入,当锁总是在被同一个线程获取的时候,为了让获取锁的代价更低引入了偏向锁JDK1.6之后偏向锁是默认开启的,锁会偏向第一个获取到它的线程,然后在对象头中存储这个线程的ID,线程进入同步代码块之前检查当前锁对象是否是偏向锁、锁标志位和ThreadID即可,如果是则直接进入代码块。

    import org.openjdk.jol.info.ClassLayout;
    
    public class Main {
        static Main main = new Main();
        public static void main(String[] args) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    for (int i = 0; i < 3; ++ i) { // 反复使用同一个线程获取锁
                        synchronized (main) {
                            System.out.println(ClassLayout.parseInstance(main).toPrintable());
                        }
                    }
                }
            });
            thread.start();
        }
    }
    /*
    偏向锁在 Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用 -
    XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争
    状态,可以通过 XX: -UseBiasedLocking=false 参数关闭偏向锁。
    */
    
    /*
    00000101 最低8位,结尾是101,偏向锁
    syn.Main object internals:
     OFFSET  SIZE   TYPE DESCRIPTION           VALUE
          0     4        (object header)       05 c8 23 17 (00000101 11001000 00100011 00010111) (388220933)
          4     4        (object header)       00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)       05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    syn.Main object internals:
     OFFSET  SIZE   TYPE DESCRIPTION            VALUE
          0     4        (object header)        05 c8 23 17 (00000101 11001000 00100011 00010111) (388220933)
          4     4        (object header)        00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)        05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    syn.Main object internals:
     OFFSET  SIZE   TYPE DESCRIPTION             VALUE
          0     4        (object header)         05 c8 23 17 (00000101 11001000 00100011 00010111) (388220933)
          4     4        (object header)         00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)         05 c1 00 20 (00000101 11000001 00000000 00100000) (536920325)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    
    Process finished with exit code 0
    */

        原理:刚开始是无锁状态,线程第一次获取锁对象的时候,JVM会把对象头中偏向锁和锁标志位修改为101,表示当前是偏向锁,同时使用CAS操作将这个线程的ID存储到对象头的Mark Down中,如果存储成功,那么线程在反复进入这个同步代码块的时候,JVM就不需要进行任何的操作了,就提高了执行效率。

    轻量级锁

        JDK1.6引入,如果有多个线程来竞争锁的时候,要尽量避免重量级锁引起的消耗,偏向锁就可以升级成轻量级锁,偏向锁会被撤销,可以撤销为无锁状态和轻量级锁状态,特定情况下轻量级锁的开销会比较小。但是如果多个线程在同一适合进入临界区,那么轻量级锁就会升级为重量级锁。

    • 等待的线程不多
    • 执行线程消耗的时间比较少

    原理:将对象的Mark Down复制到栈帧的Lock Record中,Mark Down更新为指向Lock Record的指针

    • 判断当前是对象是不是无锁状态(hashcode,0,01),如果是JVM在当前线程栈帧中创建出一个Lock Record空间,其中dispalaced hdr存储了Mark Down的信息(hashcode,分代年龄,锁标志),owner指针指向当前对象头。

    • JVM利用CAS操作将Mark Down中的信息更新为指向Lock Record的指针,如果成功竞争到锁以后就将锁标志位修改为00,然后执行同步代码块。

    • 如果不是无锁状态,那么就判断Mark Down是否指向当前线程的Lock Record,如果是则表示当前线程已经拥有了当前对象的锁,直接执行同步代码块,否则就说明锁被其他线程抢占了,此时轻量级锁升级为重量级锁,锁标志位修改为10,其他的线程进入阻塞或者等待状态。

    自旋锁&自适应锁

          JDK1.6引入以后默认开启自旋锁。

    • 重量级锁:monitor会阻塞和等待线程,当线程没有竞争到锁的时候,线程就会进入阻塞或者等待状态,只有持有锁的线程执行完同步代码块以后,monitor才回去唤醒其他的线程去竞争锁。

    • 频繁的阻塞和唤醒线程对于CPU来说,消耗很大,这个过程中CPU需要从用户态转换为核心态。在执行同步代码块的时候,由于花费的时间比较短,那么就有可能出现一种情况:同步代码块执行结束,阻塞的线程可能还在进入到阻塞状态的过程中,这段时间内阻塞线程,然后再唤醒线程CPU的消耗就会变大。如果同步代码块中的执行时间比较短,那么可以尝试让竞争锁的线程在外面多循环几次,这样就可以不让其进入阻塞状态,循环一定次数的时候就可以获取到锁,这就是自旋锁。自旋锁默认自旋次数为10次。在自旋的过程中当前线程也是需要占用CPU的资源的,如果同步代码块的执行时间比较短,那么这个线程占用CPU的时间就会比较少,自旋的就可以降低开销。如果同步代码块的执行时间比较长,那么自旋的时间就会比较长,这样一来还不如让线程阻塞。
    • 自适应锁:自旋次数和时间不固定,有JVM决定。

    锁消除

        JDK1.6有优化,当没有锁竞争的情况下,即便是又synchronized,那么JIT也会帮助我们自动消除synchronized,也就是取消了获取锁的过程。

    锁粗化

    public class Main {
        static Main main = new Main();
        public static void main(String[] args) throws Exception {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < 100; i++) {
                sb.append("aa");
            }
        }
    }
    /*
    上面的代码中可能要频繁的获取锁和释放锁,
    那么在优化的时候,直接消除append中的synchronized,
    将synchronized加入到for循环的外面,把小锁变成大锁。
    
    锁粗化:JVM探测到一连串细小的操作都是用同一个锁对象,
    将同步代码块的范围放大,放到这串操作的外面,这样只需要加依次锁即可。
    */

    synchronized优化

    • 同步代码块中尽量短,减少同步代码块的执行时间,减少锁的竞争,执行时间变短,那么等待的线程就会变得少一些,这样一来可能轻量级锁和自旋锁就能过解决。

    • 将一个锁拆分为多个锁提高并发度,降低锁的粒度。
      • ConcurrentHashMap:效率高,线程可以并行
      • Hashtable:效率低,只能串行,可以读写分离
    • 读写分离:读不加锁,写入和删除加锁
      • ConCurrentHashMap
      • CopyOnWriteArrayList
      • CopyOnWirteSet  
      • LinkedBlockedQueue:入队和出队使用的是不同的锁
  • 相关阅读:
    python BeautifulSoup库的基本使用
    python操作RabbitMQ
    MySQL主从复制
    python字典与集合操作
    常见术语
    Mac下如何使用homebrew
    springboot整合freemarker
    Servlet与JSP概念理解
    slf4j-api、slf4j-log4j12以及log4j之间什么关系?
    使用nodeJs安装Vue-cli并用它快速构建Vue项目
  • 原文地址:https://www.cnblogs.com/zut-syp/p/15361210.html
Copyright © 2011-2022 走看看