• JUC笔记(三) --- 线程的通信


    3.线程的通信

    如何让共享资源不会进入**“竞态条件”**? 如何让线程间的通信"有序"?

    static int counter = 0;
    public static void main(String[] args) throws InterruptedException {
         Thread t1 = new Thread(() -> {
             for (int i = 0; i < 5000; i++) {
                counter++;
             }
         }, "t1");
         Thread t2 = new Thread(() -> {
         for (int i = 0; i < 5000; i++) {
             counter--;
          }
         }, "t2");
         t1.start();
         t2.start();
         t1.join();
         t2.join();
         log.debug("{}",counter);
    }
    
    //  out : 结果会有多种状态
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    为什么会有这样的结果? 原因是因为counter++ 并非原子性操作. 从字节码层面分析

    getstatic i // 获取静态变量i的值
    iconst_1 // 准备常量1
    iadd // 自增
    putstatic i // 将修改后的值存入静态变量i
        
        
    getstatic i // 获取静态变量i的值
    iconst_1 // 准备常量1
    isub // 自减
    putstatic i // 将修改后的值存入静态变量i
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.1临界区

    • 多个线程对共同资源(数据等)进行读写操作的这段代码叫做临界区

    3.2静态条件

    • 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

    3.3解决方案

    • 阻塞式的解决方案:synchronized,Lock
    • 非阻塞式的解决方案:原子变量
    synchronized

    它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

    注意

    虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

    • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
    • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

    其实也就是将synchronized包括住的临界区 ,当做了一次 原子性操作

    sychronized方法
    class Test{
     public synchronized void test() {
     
     }
    }
    等价于
    class Test{
         public void test() {
         synchronized(this) {
     		
     		}
     	}
    }
    
    class Test{
     public static synchronized void test() {
     
     }
    }
    
    class Test{
         public void test() {
         synchronized(Test.class) {
     		
     		}
     	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    变量的线程安全分析

    成员变量和静态变量是否线程安全?

    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
      • 如果只有读操作,则线程安全
      • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

    局部变量是否线程安全?

    • 局部变量是线程安全的
    • 但局部变量引用的对象则未必
      • 如果该对象没有逃离方法的作用访问,它是线程安全的
      • 如果该对象逃离方法的作用范围,需要考虑线程安全

    局部变量线程安全分析

    public static void test1() {
     int i = 10;
     i++;
    }
    
    • 1
    • 2
    • 3
    • 4
    public static void test1();
     descriptor: ()V
     flags: ACC_PUBLIC, ACC_STATIC
     Code:
     stack=1, locals=1, args_size=0
     0: bipush 10
     2: istore_0
     3: iinc 0, 1
     6: return
     LineNumberTable:
     line 10: 0
     line 11: 3
     line 12: 6
     LocalVariableTable:
     Start Length Slot Name Signature
     3 4 0 i I
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可以看出来局部变量的形式 是线程安全的

    成员变量线程安全分析

    ArrayList<String> list = new ArrayList<>();
        public void method1(int loopNumber) {
            for (int i = 0; i < loopNumber; i++) {
                method2();
                method3();
            }
        }
    
        private void method2() {
            list.add("1");
        }
    
        private void method3() {
            list.remove(0);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    执行:

     static final int THREAD_NUMBER = 2;
        static final int LOOP_NUMBER = 200;
        public static void main(String[] args) {
            ThreadSafeSubClass test = new ThreadSafeSubClass();
            for (int i = 0; i < THREAD_NUMBER; i++) {
                new Thread(() -> {
                    test.method1(LOOP_NUMBER);
                }, "Thread" + (i+1)).start();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    out:

    Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
     at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
     at java.util.ArrayList.remove(ArrayList.java:496) 
     at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
     at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
     at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
     at java.lang.Thread.run(Thread.java:748)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    原因:

    两个线程栈帧的数据list指向了堆中的同一个list,导致了list出现了"“竞态条件”"

    解决: 将list移进 method1

    超级注意:

    子类覆盖父类方法时,同时也应当注意是否线程安全!!!

    class ThreadSafe {
        public final void method1(int loopNumber) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < loopNumber; i++) {
                method2(list);
                method3(list);
            }
        }
    
        public void method2(ArrayList<String> list) {
            list.add("1");
        }
    
        public void method3(ArrayList<String> list) {
            System.out.println(1);
            list.remove(0);
        }
    }
    
    class ThreadSafeSubClass extends ThreadSafe{
        @Override
        public void method3(ArrayList<String> list) {
            System.out.println(2);
            new Thread(() -> {
                list.remove(0);
            }).start();
        }
    
    //    @Override
    //    public void method2(ArrayList list) {
    //        System.out.println("sub1");
    //    }
    } 	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    从这个例子可以看出 private 或 fifinal 提供【安全】的意义所在,请体会开闭原则中的【闭】

    3.4常见线程安全类:

    • String
    • Integer
    • StringBuffffer
    • Random
    • Vector
    • Hashtable
    • java.util.concurrent 包下的类

    ​ 它们的每个方法是原子的,但是他们之间组合的方法不一定是原子性

    eg.两个线程同时往Hashtable中put元素

    3.5不可变类线程安全性

    String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

    深入源码一探究竟:

        private final char value[];    
    
    /**
         * Returns a string resulting from replacing all occurrences of
         * {@code oldChar} in this string with {@code newChar}.
         * 

    * If the character {@code oldChar} does not occur in the * character sequence represented by this {@code String} object, * then a reference to this {@code String} object is returned. * Otherwise, a {@code String} object is returned that * represents a character sequence identical to the character sequence * represented by this {@code String} object, except that every * occurrence of {@code oldChar} is replaced by an occurrence * of {@code newChar}. *

    * Examples: *

         * "mesquite in your cellar".replace('e', 'o')
         *         returns "mosquito in your collar"
         * "the war of baronets".replace('r', 'y')
         *         returns "the way of bayonets"
         * "sparring with a purple porpoise".replace('p', 't')
         *         returns "starring with a turtle tortoise"
         * "JonL".replace('q', 'x') returns "JonL" (no change)
         * 
    * * @param oldChar the old character. * @param newChar the new character. * @return a string derived from this string by replacing every * occurrence of {@code oldChar} with {@code newChar}. */
    public String replace(char oldChar, char newChar) { if (oldChar != newChar) { int len = value.length; int i = -1; char[] val = value; /* avoid getfield opcode */ while (++i < len) { if (val[i] == oldChar) { break; } } if (i < len) { char buf[] = new char[len]; /** 找到内鬼 **/ for (int j = 0; j < i; j++) { buf[j] = val[j]; } while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } return new String(buf, true); } } return this; }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    可以看出,这里是通过返回新的String对象从而维持线程的安全

    因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。

    3.6Monitor锁(**)

    Java 对象头

    普通对象
    |--------------------------------------------------------------|
    | Object Header (64 bits) |
    |------------------------------------|-------------------------|
    | Mark Word (32 bits) | Klass Word (32 bits) |
    |------------------------------------|-------------------------|
    数组对象
    |---------------------------------------------------------------------------------|
    | Object Header (96 bits) |
    |--------------------------------|-----------------------|------------------------|
    | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
    |--------------------------------|-----------------------|------------------------|
    其中 Mark Word 结构为
    |-------------------------------------------------------|--------------------|
    | Mark Word (32 bits) | State |
    |-------------------------------------------------------|--------------------|
    | hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
    |-------------------------------------------------------|--------------------|
    | thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
    |-------------------------------------------------------|--------------------|
    | ptr_to_lock_record:30 | 00 | Lightweight Locked |
    |-------------------------------------------------------|--------------------|
    | ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
    |-------------------------------------------------------|--------------------|
    | | 11 | Marked for GC |
    |-------------------------------------------------------|--------------------|
    64 位虚拟机 Mark Word
    |--------------------------------------------------------------------|--------------------|
    | Mark Word (64 bits) | State |
    |--------------------------------------------------------------------|--------------------|
    | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
    |--------------------------------------------------------------------|--------------------|
    | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
    |--------------------------------------------------------------------|--------------------|
    | ptr_to_lock_record:62 | 00 | Lightweight Locked |
    |--------------------------------------------------------------------|--------------------|
    | ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
    |--------------------------------------------------------------------|--------------------|
    | | 11 | Marked for GC |
    |--------------------------------------------------------------------|--------------------|
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的

    Mark Word 中就被设置指向 Monitor 对象的指针

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u1LqWDWC-1666873167568)(assets/image-20221025224306735.png)]

    • 刚开始 Monitor 中 Owner 为 null
    • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
    • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
    • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
    • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面wait-notify 时会分析

    3.7sychronized原理(**)

    static final Object lock = new Object();
    static int counter = 0;
    public static void main(String[] args) {
     synchronized (lock) {
     counter++;
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    对应的字节码

    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: getstatic #2 // <- lock引用 (synchronized开始)
     3: dup
     4: astore_1 // lock引用 -> slot 1
     5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
     6: getstatic #3 // <- i
     9: iconst_1 // 准备常数 1
     10: iadd // +1
     11: putstatic #3 // -> i
     14: aload_1 // <- lock引用
     15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
     16: goto 24
     19: astore_2 // e -> slot 2 
     20: aload_1 // <- lock引用
     21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
     22: aload_2 // <- slot 2 (e)
     23: athrow // throw e
     24: return
     Exception table:
     from to target type
     6 16 19 any
     19 22 19 any
     LineNumberTable:
     line 8: 0
     line 9: 6
     line 10: 14
     line 11: 24
     LocalVariableTable:
     Start Length Slot Name Signature
     0 25 0 args [Ljava/lang/String;
     StackMapTable: number_of_entries = 2
     frame_type = 255 /* full_frame */
     offset_delta = 19
     locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
     stack = [ class java/lang/Throwable ]
     frame_type = 250 /* chop */
     offset_delta = 4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    注意

    方法级别的 synchronized 不会在字节码指令中有所体现

    1.轻量级锁

    轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以

    使用轻量级锁来优化。

    markword数据结构:

    64位jvm虚拟机
    |--------------------------------------------------------------------|--------------------|
    | Mark Word (64 bits) | State |
    |--------------------------------------------------------------------|--------------------|
    | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
    |--------------------------------------------------------------------|--------------------|
    | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
    |--------------------------------------------------------------------|--------------------|
    | ptr_to_lock_record:62 | 00 | Lightweight Locked |
    |--------------------------------------------------------------------|--------------------|
    | ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
    |--------------------------------------------------------------------|--------------------|
    | | 11 | Marked for GC |
    |--------------------------------------------------------------------|--------------------|
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看到:末尾 为

    00 : 轻量级锁

    10 : 重量级锁 也就是 监视器

    01 : 未锁状态

    101: 偏向锁

    • 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的

      Mark Word

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bhSXOZKH-1666873167569)(assets/image-20221026131427035.png)]

    • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存

      入锁记录[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DdE5wfeU-1666873167570)(assets/image-20221026131931335.png)]

    • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
      在这里插入图片描述

    • 如果csa失败有以下两种情况:

      • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀(后面介绍)过程
      • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rrCTKR8i-1666873167571)(assets/image-20221026132525638.png)]

    • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重

      入计数减一

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-154zC6KR-1666873167571)(assets/image-20221026132757794.png)]

    • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象

      • 成功,则解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
    2.锁膨胀

    如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有

    竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

    • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-es33kdEq-1666873167572)(assets/image-20221026133026359.png)]

    • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

      • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
      • 然后自己进入 Monitor 的 EntryList BLOCKED

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m33Tdya1-1666873167573)(assets/image-20221026133112044.png)]

    • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁

      流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

    3.自旋优化

    自旋优化可以大大减小处理机的空闲时间,这是jdk6以后的版本优化

    自旋成功:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHdVoi7U-1666873167574)(assets/image-20221026133624669.png)]

    自旋失败:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q9hd38md-1666873167574)(assets/image-20221026133634505.png)]

    • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
    • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
    • Java 7 之后不能控制是否开启自旋功能
    4.偏向锁

    轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

    CAS操作会有系统开销,如何减小这一部分的系统开销? --偏向锁

    Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现

    这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

    compare:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NF3hwIyk-1666873167575)(assets/image-20221026133928307.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mXhaBlXU-1666873167576)(assets/image-20221026133938329.png)]

    可以看到,自旋锁减少了CAS操作,当只有一个线程进入该锁时,性能会有很大的提升.

    偏向状态
    |--------------------------------------------------------------------|--------------------|
    | Mark Word (64 bits) | State |
    |--------------------------------------------------------------------|--------------------|
    | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
    |--------------------------------------------------------------------|--------------------|
    | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
    |--------------------------------------------------------------------|--------------------|
    | ptr_to_lock_record:62 | 00 | Lightweight Locked |
    |--------------------------------------------------------------------|--------------------|
    | ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
    |--------------------------------------------------------------------|--------------------|
    | | 11 | Marked for GC |
    |--------------------------------------------------------------------|--------------------|
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    一个对象创建时:

    • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
    • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
    • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

    测试:

    1. 测试延迟
    2. 测试偏向锁
    // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 
    public static void main(String[] args) throws IOException {
         Dog d = new Dog();
         ClassLayout classLayout = ClassLayout.parseInstance(d);
         new Thread(() -> {
         	log.debug("synchronized 前");
        	System.out.println(classLayout.toPrintableSimple(true));
         	synchronized (d) {
                log.debug("synchronized 中");
                System.out.println(classLayout.toPrintableSimple(true));
         	}
         	log.debug("synchronized 后");
         System.out.println(classLayout.toPrintableSimple(true));
         }, "t1").start();
     }
     
     class Dog{}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    out:

    11:08:58.117 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
    //  可以看到 线程进入锁之前 markword偏向锁记录的ThreadID 为空
    11:08:58.121 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 
    11:08:58.121 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101
    //  线程释放锁后 markword偏向锁记录的TreadID没有改变
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    测试禁用

    在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

    out:

    11:13:10.018 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
    11:13:10.021 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000 
    //  轻量级锁
    11:13:10.021 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
    //  未锁状态
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    撤销 - 调用对象 hashCode

    调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被

    撤销

    • 轻量级锁会在锁记录中记录 hashCode
    • 重量级锁会在 Monitor 中记录 hashCode

    out:

    11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015 
    11:22:10.391 c.TestBiased [t1] - synchronized00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 
    11:22:10.393 c.TestBiased [t1] - synchronized00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000 
    11:22:10.393 c.TestBiased [t1] - synchronized00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以看出,在hashcode调用后:偏向锁升级成为了轻量锁

    Q&A : 可以看markword对象头, 偏向锁并不会有任何hashcode 的生成,也就是说当前对象并没有生成一个hashcode,而当系统需要给它定义一个hashcode时自然需要有一个地方记录hashcode , 那就是markword里. 此时要么在monitor中记录hashcode , 要么在锁记录中记录hashcode

    撤销 - 其它线程使用对象

    当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁. 也就是当偏向锁ThreadID记录后,有另一个Thread使用该锁,该锁会升级成为轻量锁

    public static void main(String[] args) throws InterruptedException {
     Dog d = new Dog();
     Thread t1 = new Thread(() -> {
     log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
     synchronized (d) {
     log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
     try {
         d.wait();
         } catch (InterruptedException e) {
         	e.printStackTrace();
         }
         	log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
         }
     }, "t1");
     t1.start();
     new Thread(() -> {
         try {
         Thread.sleep(6000);
             } catch (InterruptedException e) {
             e.printStackTrace();
         }
         synchronized (d) {
             log.debug("notify");
             d.notify();
         }
     }, "t2").start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    out:

    [t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
    [t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101 
    [t2] - notify 
    [t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010
        //  可以看出这里已经升级成为了重型锁,因为有竞争条件的发生直接升级成为了重型锁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    批量重偏向

    如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID

    当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

    private static void test3() throws InterruptedException {
            Vector<Dog> list = new Vector<>();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 30; i++) {
                    Dog d = new Dog();
                    list.add(d);
                    synchronized (d) {
                        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    }
                }
                synchronized (list) {
                    list.notify();
                }
            }, "t1");
            t1.start();
    
            Thread t2 = new Thread(() -> {
                synchronized (list) {
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("===============> ");
                for (int i = 0; i < 30; i++) {
                    Dog d = list.get(i);
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    synchronized (d) {
                        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    }
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
            }, "t2");
            t2.start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    out:

    [t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
    [t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
    [t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
        //  可以看到从第十八次进入,锁已经批量偏向了,观察他的PID
    [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
    [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
    [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
    [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
    [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
    [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    批量撤销

    当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象

    都会变为不可偏向的,新建的对象也是不可偏向的

    static Thread t1,t2,t3;
        private static void test4() throws InterruptedException {
            Vector<Dog> list = new Vector<>();
            int loopNumber = 39;
            t1 = new Thread(() -> {
                for (int i = 0; i < loopNumber; i++) {
                    Dog d = new Dog();
                    list.add(d);
                    synchronized (d) {
                        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    }
                }
                LockSupport.unpark(t2);
            }, "t1");
            t1.start();
            t2 = new Thread(() -> {
                LockSupport.park();
                log.debug("===============> ");
                for (int i = 0; i < loopNumber; i++) {
                    Dog d = list.get(i);
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    synchronized (d) {
                        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    }
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
                LockSupport.unpark(t3);
            }, "t2");
            t2.start();
            t3 = new Thread(() -> {
                LockSupport.park();
                log.debug("===============> ");
                for (int i = 0; i < loopNumber; i++) {
                    Dog d = list.get(i);
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    synchronized (d) {
                        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    }
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
            }, "t3");
            t3.start();
            t3.join();
            log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    最后一次out:01

    锁消除

    锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行

    @Fork(1)
    @BenchmarkMode(Mode.AverageTime)
    @Warmup(iterations=3)
    @Measurement(iterations=5)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    public class MyBenchmark {
         static int x = 0;
         @Benchmark
         public void a() throws Exception {
         x++;
     }
     @Benchmark
     public void b() throws Exception {
             Object o = new Object();
             synchronized (o) {
             x++;
          }
     }
    }
    
    java -jar benchmarks.jar
    Benchmark Mode Samples Score Score error Units 
    c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op 
    c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op
    java -XX:-EliminateLocks -jar benchmarks.jar
    Benchmark Mode Samples Score Score error Units 
    c.i.MyBenchmark.a avgt 5 1.507 0.108 ns/op 
    c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    锁膨胀

    原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽可能快地拿到锁

    大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗

    所示连续的append()方法就属于这类情况。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,以代码2为例,就是扩展到第一个append()操作之前直至最后一个append()操作之后,这样只需要加锁一次就可以了

    public String concatString(String s1, String s2, String s3) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.8wait & notify

    wait notify 原理

    ​	[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RCusc3Oe-1666873167576)(assets/image-20221026142309360.png)]

    • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
    • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
    • BLOCKED 线程会在 Owner 线程释放锁时唤醒
    • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

    常用API

    • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
    • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
    • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

    Sleep 和 wait 的区别

    1. sleep 是 Thread 方法,而 wait 是 Object 的方法
    2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
    3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
    4. 它们状态 TIMED_WAITING

    虚假唤醒:

    notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线

    程,称之为【虚假唤醒】

    3.9park&unpark

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TnwjjU9F-1666873167577)(assets/2021042611364362.png)]

    • 当前线程调用 Unsafe.park() 方法
    • 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
    • 线程进入 _cond 条件变量阻塞
    • 设置 _counter = 0

    在这里插入图片描述

    • 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
    • 唤醒 _cond 条件变量中的 Thread_0
    • Thread_0 恢复运行
    • 设置 _counter 为 0

    当counter 状态为 1 的情况下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oJfdOEKV-1666873167579)(assets/2021042611364364.png)]

    • 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
    • 当前线程调用 Unsafe.park() 方法检查
    • _counter ,本情况为 1,这时线程无需阻塞,继续运行
    • 设置 _counter 为 0

    3.10线程状态的转换

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PUGKbN26-1666873167579)(assets/image-20221027152230102.png)]

    分析

    (1)创建新的线程,此时操作系统内还未fork出线程

    (2,3,4)当线程出现join 、wait 、 park 时会编程waiting状态 interrupt、unpark、notify、notifyall 唤醒线程

    (5,6,7,8)当线程出现 sleep、join 、wait 、 park, 带参时间定时 interrupt、unpark、notify、notifyall 唤醒线程

    值得注意的时,只有wait状态会释放当前线程所持有的共享资源(锁)

    wait 状态转化为runnable 时 , 会重新进入 阻塞队列

    (9) 阻塞队列,尝试获取锁资源

    (10) 线程资源

    3.11线程活跃性

    死锁

    eg.

    锁s1,s2    线程t1,t2
    
    t1 持有s1  尝试获取s2 
    
    t2 持有s2  尝试获取s1
    
    • 1
    • 2
    • 3
    • 4
    • 5

    造成死锁

    如何定位死锁? Jconsole 非常方便!

    经典死锁问题:哲学家问题

    活锁

    活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束

    eg.

    线程t1,t2  共享资源r1 = 10
    t1: while(r1>0) r1++
    t2: while(r2<20) r1--
    造成结果就是两个线程一直运行
    
    • 1
    • 2
    • 3
    • 4
    饥饿

    一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

    3.12Reentrantlock(**)

    特点:

    • 可中断,可重入

    • 可以设置超时时间

    • 可以设置为公平锁

    • 支持多个条件变量

    eg.

    // 获取锁
    reentrantLock.lock();
    try {
     // 临界区
    } finally {
     // 释放锁
     reentrantLock.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    可重入

    可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁

    如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

    和synchronized一样,就不做demo

    可打断
    ReentrantLock lock = new ReentrantLock();
            Thread t1 = new Thread(() -> {
                log.debug("启动...");
                try {
                    lock.lockInterruptibly();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    log.debug("等锁的过程中被打断");
                    return;
                }
                try {
                    log.debug("获得了锁");
                } finally {
                    lock.unlock();
                }
            }, "t1");
            lock.lock();
            log.debug("获得了锁");
            t1.start();
            try {
                sleep(1);
                t1.interrupt();
                log.debug("执行打断");
            } finally {
                lock.unlock();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    out

    18:02:40.520 [main] c.TestInterrupt - 获得了锁
    18:02:40.524 [t1] c.TestInterrupt - 启动... 
    18:02:41.530 [main] c.TestInterrupt - 执行打断
    java.lang.InterruptedException 
     at 
    java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchr
    onizer.java:898) 
     at 
    java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchron
    izer.java:1222) 
     at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) 
     at cn.itcast.n4.reentrant.TestInterrupt.lambda$main$0(TestInterrupt.java:17) 
     at java.lang.Thread.run(Thread.java:748) 
    18:02:41.532 [t1] c.TestInterrupt - 等锁的过程中被打断
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

    ReentrantLock lock = new ReentrantLock();
            Thread t1 = new Thread(() -> {
                log.debug("启动...");
                lock.lock();
                try {
                    log.debug("获得了锁");
                } finally {
                    lock.unlock();
                }
            }, "t1");
            lock.lock();
            log.debug("获得了锁");
            t1.start();
            try {
                sleep(1);
                t1.interrupt();
                log.debug("执行打断");
                sleep(1);
            } finally {
                log.debug("释放了锁");
                lock.unlock();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    out.

    18:06:56.261 [main] c.TestInterrupt - 获得了锁
    18:06:56.265 [t1] c.TestInterrupt - 启动... 
    18:06:57.266 [main] c.TestInterrupt - 执行打断 // 这时 t1 并没有被真正打断, 而是仍继续等待锁
    18:06:58.267 [main] c.TestInterrupt - 释放了锁
    18:06:58.267 [t1] c.TestInterrupt - 获得了锁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    锁超时
    log.tryLock();   立即尝试获取锁
    log.tryLock(time);   等呆time时间没有获取到 走 try-catch
    
    • 1
    • 2
    公平锁

    默认:非公平锁

    false:公平锁

    ReentrantLock lock = new ReentrantLock(default(true) or false ); 
            lock.lock();
            for (int i = 0; i < 500; i++) {
                new Thread(() -> {
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + " running...");
                    } finally {
                        lock.unlock();
                    }
                }, "t" + i).start();
            }
    // 1s 之后去争抢锁
            Thread.sleep(1000);
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " start...");
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "强行插入").start();
            lock.unlock();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    out.

    非公平锁

    t39 running... 
    t40 running... 
    t41 running... 
    t42 running... 
    t43 running... 
    强行插入 start... 
    强行插入 running... 
    t44 running... 
    t45 running... 
    t46 running... 
    t47 running... 
    t49 running...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    公平锁

    t465 running... 
    t464 running... 
    t477 running... 
    t442 running... 
    t468 running... 
    t493 running... 
    t482 running... 
    t485 running... 
    t481 running... 
    强行插入 running
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    公平锁一般没有必要,会降低并发度,后面分析原理时会讲解

    条件变量

    synchronized 中也有条件变量,Monitor的 waitSet ,当条件不满足时进入 waitSet 等待

    而ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

    • synchronized 是那些不满足条件的线程都在一个waitSet中等消息
    • 而 ReentrantLock 支持多种waitSet、唤醒时也是按不同种类的waitSet来唤醒

    使用要点:

    • await 前需要获得锁

    • await 执行后,会释放锁,进入 conditionObject 等待

    • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁

    • 竞争 lock 锁成功后,从 await 后继续执行

  • 相关阅读:
    9.网络游戏逆向分析与漏洞攻防-游戏网络架构逆向分析-接管游戏连接服务器的操作
    50、IO流
    Android 安全功能
    论文阅读(13) 水母游泳过程中的神经机械波共振(2021)
    golang基础知识
    docker容器mysql中文?号,修改编码配置
    go入门--mult_returnval
    rCore-Tutorial-Book第二课(移除Rust std标准库依赖)
    C 语言共用体(Union)
    Servlet技术栈要点
  • 原文地址:https://blog.csdn.net/qq_57115378/article/details/127559964