关于java:并发系列二synchronized关键字常见api对象头及证明hashcode

5次阅读

共计 6128 个字符,预计需要花费 16 分钟才能阅读完成。

前言

  • 上篇文章总结了 java 线程与 os 线程的分割,以及模仿 java 调用 os 函数创立线程。通过上篇文章的总结,咱们理解了 java 的线程与 os 线程是一一对等的。同时也理解到了应用多线程的起因。凡事都有利与弊,在多线程晋升程序运行效率的长处下,也带来了另外的问题——同步 。没错,只有应用到多线程,咱们就要思考同步,不然就乱套了!在同步问题中,java 有一个亲儿子——synchronized 关键字。在 jdk 1.5 后,它就有了一些孪生兄弟 ——JUC包下的各种锁实现。它们之间的特点将在后续的文章中做出总结。

一、为什么在多线程中要应用同步?

  • 如以后章节的主题,为什么在多线程中要应用同步?咱们来看一下上面这段代码:

    /**
     * 模仿 4 个窗口卖 20 张票
     */
    public class TestMulThread {
    
        private static int ticketNum = 10;
    
        public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 4; i++) {new Thread(() -> {while (!Thread.currentThread().isInterrupted() && ticketNum > 0) {System.out.println(Thread.currentThread().getName() + "售出第" + ticketNum-- + "张票");
                   }
                }, "窗口" + (i + 1)).start();}
        }
    }
    运行上段代码,你会发现输入的后果毫无法则,可能呈现票号为正数的状况,也有可能呈现卖出反复票的状况(` 自己电脑 cpu 为 12 核的,处理速度比拟快,不会呈现上述情况 `)。这显著是有问题的。要解决这个问题咱们能够应用同步策略,所谓同步策略即是应用 synchronized 关键字(** 这里只思考 synchronized 关键字,不思考其余的状况 **)。于是,咱们进行代码批改,如下所示:```java
    /**
     * 模仿 4 个窗口卖 20 张票
     */
    public class TestMulThread {
    
        private static int ticketNum = 10;
    
        static Lock lock = new Lock();
    
        public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 4; i++) {new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {synchronized (lock) {if (ticketNum > 0) {System.out.println(Thread.currentThread().getName() + "售出第" + ticketNum-- + "张票");
                           } else {Thread.currentThread().interrupt();}
                       }
                   }
                }, "窗口" + (i + 1)).start();}
        }
    }
    
    class Lock {
    
    }
    
    ```
    批改后的代码为,新建了一个 Lock 类作为锁对象。这样就实现了同步的操作。上面总结下 synchronized 关键字的常见应用形式、经典案例及其特点。

    二、synchronized 关键字的几种用法即特点

  • 这里要明确一个点:synchronized 锁住的是对象,是通过一个标识来示意是具体的哪一种锁

    2.1 锁类实例和类对象

  • 具体参考如下代码:

    // 状况一:锁 object 对象
    public class Demo {private Object object = new Object();
    
        public void test(){synchronized (object) {System.out.println(Thread.currentThread().getName());
            }
        }
    }
    
    // 状况二: 锁以后对象 this,锁定某个代码块
    // 应用此种形式要留神调用进来的 this 是否为同一对象
    // 若 Demo 的实例不是单例的,那么这把锁基本上起不到同步的作用
    public class Demo {public void test() {//synchronized(this)锁定的是以后类的实例, 这里锁定的是 Demo 类的实例
            synchronized (this) {System.out.println(Thread.currentThread().getName());
            }
        }
    }
    
    // 状况三: 锁以后对象 this,锁定整个办法
    // 与状况二相似,然而它是锁住了整个办法,粒度比状况二大
    public class Demo {public synchronized void test() {System.out.println(Thread.currentThread().getName());
        }
    }
    
    // 
    // ===> 当调用以后类的所有同步静态方法将会期待获取锁
    // 留神: 然而此时还是能调用类实例的同步办法。为什么呢?// 因为动态同步办法和类实例同步办法领有的锁不一样
    // 一个是类对象一个是类实例对象。// 同时,此时还能调用类对象的动态非同步办法以及类实例的
    // 非同步办法。为什么呢?因为这些办法没有加锁啊,能够间接调用。public class Demo {public static synchronized void test() {System.out.println(Thread.currentThread().getName());
        }
    }

    2.2 锁同一个 String 常量

  • 查看如下代码:

    /**
      下面说了,synchronized 关键字锁的是对象,而对于 s1 和 s2 这两个对象,他们的值都是 lock,也就是放在常量池中的(堆内的办法区),所以 s1 和 s2 指向的是同一个对象。所以
      上面的 test1 和 test2 办法应用的都是同一把锁,最终的运行后果就是线程 2 会期待线程 1 把锁开释结束后
      能力获取锁并执行如下代码。*/
    public class Demo {
    
        String s1 = "lock";
        String s2 = "lock";
    
        public void test1() {synchronized (s1) {System.out.println("t1 start");
                try {TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println("t1 end");
            }
        }
    
        public void test2() {synchronized (s2) {System.out.println("t2 start");
            }
        }
    
        public static void main(String[] args) {Demo demo = new Demo();
            new Thread(demo :: test1, "test1").start();
            new Thread(demo :: test2, "test2").start();}
    
    }

    2.3 锁 Integer 对象

  • 见如下代码:

    public static class BadLockOnInteger implements Runnable{
        public static Integer i = 0;
    
        static BadLockOnInteger instance = new BadLockOnInteger();
    
        @Override
        public void run() {for (int j = 0; j < 10000000; j++) {synchronized(i) {// 在 jvm 执行时, 这是这样的一段代码:  i = Integer.valueOf(i.intValue() + 1),
                    // 跟踪 Integer.valueOf()源码可知, 每次都是返回一个新的 Integer 对象, 导致加锁的都是新对象, 当然会导致多线程同步生效
                    i++;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
    
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            System.out.println(i);
        }
    }

2.4 可重入性(包含继承)

  • 概念解释:所谓可重入性就是间断申请获取同一把锁
  • 见如下代码

    /**
     一个同步办法调用另外一个同步办法,反对可重入
     */
    public class Demo {public synchronized void test1() {System.out.println("test1 start");
            try {TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            test2();}
    
        public synchronized void test2() {System.out.println("test2 start");
        }
    
        public static void main(String[] args) {Demo demo = new Demo();
            demo.test1();}
    
    }
    
    /**
     继承也反对可重入个性
     */
    public class Demo {synchronized void test() {System.out.println("demo test start");
            try {TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println("demo test end");
        }
    
        public static void main(String[] args) {new Demo2().test();}
    
    }
    
    class Demo2 extends Demo {
    
        @Override
        synchronized void test() {System.out.println("demo2 test start");
            // 此处调用了父类的办法
            super.test();
            System.out.println("demo2 test end");
        }
    
    }

    2.5 synchronized 开释锁的几种状况

  • synchronized 关键字是 手动上锁主动开释锁 的。同时主动开释锁包含:加锁代码块执行完结或者抛出的异样
  • 在执行 await 办法时,锁会被主动开释。

三、初识对象头

3.1 对象头构造

  • 下面介绍了 synchronized 的一些根本用法和个性。接下来咱们开始意识下对象头。(ps:要想了解 synchronized 关键字,理解对象头是根底)
  • 大家都晓得,synchroinzed 在 jdk1.6 之后会存在锁降级过程,所以会依据不同的状况产生不同的锁:偏差锁、轻量锁、分量锁。而这些所谓锁对应的仅仅是对象头的一些信息。上面两张图将列举出不同状态下的对象头信息

    3.2 如何查看对象头

  • 第一步:maven 我的项目引入如下 jar 包

    <dependency>
       <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.9</version>
    </dependency>
  • 第二步:新建 User.java 类

    public class User {public static void main(String[] args) {User user = new User();
            System.out.println(ClassLayout.parseInstance(user).toPrintable());
        }
    
    }
  • 第三步:运行 main 办法查看对象头信息

    3.3 证实无锁状态对象的前 56 位存储的是 hascode

    3.3.1 cpu 的大小端模式

  • 为什么要总结这个呢?因为 jol 打印进去的一些对象信息外面有很多 0101 以及对应的十六进制的值。咱们要晓得 hashcode 存在哪,就要晓得 cpu 的大小端模式。

3.3.2 何为大小端模式

  • 参考链接:https://www.cnblogs.com/0YHT0/p/3403474.html。大抵总结为:咱们的数据是存在内存中的,而每个 cpu 对应的存储形式是不统一的。所谓大端模式就是高位存在内存低位上,eg:假如要存储 12345678 这个数字时,两两为一对。87 属于第一位、56 属于第二位 ….. 以此类推。那么,咱们就能晓得 12 是最高位,所以它会被存到内存的低位。拿上述链接的总结来说就是如下表所示:
    | 内存地址 | 存储的数据(Byte)|
    | :——–: | :—————-: |
    | 0x00000000 | 0x12 |
    | 0x00000001 | 0x34 |
    | 0x00000002 | 0x56 |
    | 0x00000003 | 0x78 |

    大抵意思就是这样,** 所以在大端模式下,最终取数据时(从低位开始取)**,于是完满还原 **12345678**。小端模式的话,相同的。这里就不总结了。那么问题来了,咱们如何晓得咱们的 cpu 是大端存储模式还是小端存储模式呢?java 提供了如下 api:```java
    // 输入后果参考如下内容:// BIG_ENDIAN:大端模式
    // LITTLE_ENDIAN: 小端模式
    System.out.println(ByteOrder.nativeOrder().toString());
    ```
    

3.3.3 证实 hashcode

  • 接下来咱们来证实前 56 位存储的 hashcode。
  • 新建如下类

    public class Valid {public static void main(String[] args) {System.out.println(ByteOrder.nativeOrder().toString());
    
            User user = new User();
            System.out.println("before hashcode");
            System.out.println(ClassLayout.parseInstance(user).toPrintable());
            // 将 hashcode 转成 16 进制,因为 jol 在输入的内容中蕴含 16 进制的值
            System.out.println(Integer.toHexString(user.hashCode()));
    
            System.out.println("after hashcode");
            System.out.println(ClassLayout.parseInstance(user).toPrintable());
        }
    }
  • 运行后果如下图所示:

    四、总结

  • 综上,咱们理解了 java 对象头的构造以及证实了无锁状态下的前 56 为存储的是 hashcode。咱们要深刻理解 java 的对象头,这是了解 synchronized 关键字的基石。下篇文章主题为:证实分代年龄、无锁、偏差锁、轻量锁、重 (chong) 偏差、重 (chong) 轻量、分量锁
  • 并发模块对应 github 地址:传送门
  • I am a slow walker, but I never walk backwards.
正文完
 0