热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

并发系列二:synchronized关键字常见api、对象头及证明hashcode-前言上篇文章总结了java线程与os线程的联系,以及模拟java调用os函数创建线程。通过上

前言
  • 上篇文章总结了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关键字,不考虑其他的情况)。于是,我们进行代码修改,如下所示:
    /**
     * 模拟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.jolgroupId>
        <artifactId>jol-coreartifactId>
        <version>0.9version>
    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 何为大小端模式

  • 参考链接:www.cnblogs.com/0YHT0/p/340…。大致总结为:我们的数据是存在内存中的,而每个cpu对应的存储方式是不一致的。所谓大端模式就是高位存在内存低位上,eg:假设要存储12345678这个数字时,两两为一对。87属于第一位、56属于第二位.....以此类推。那么,我们就能知道12是最高位,所以它会被存到内存的低位。拿上述链接的总结来说就是如下表所示:
    内存地址存储的数据(Byte)
    0x000000000x12
    0x000000010x34
    0x000000020x56
    0x000000030x78
    大致意思就是这样,所以在大端模式下,最终取数据时(从低位开始取),于是完美还原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.


推荐阅读
  • 我正在制作价值噪声发生器,发现我当前的哈希值在图像中产生了一种图案: ... [详细]
  • MapReduce原理是怎么剖析的
    这期内容当中小编将会给大家带来有关MapReduce原理是怎么剖析的,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。1 ... [详细]
  • 深入解析 Apache Shiro 安全框架架构
    本文详细介绍了 Apache Shiro,一个强大且灵活的开源安全框架。Shiro 专注于简化身份验证、授权、会话管理和加密等复杂的安全操作,使开发者能够更轻松地保护应用程序。其核心目标是提供易于使用和理解的API,同时确保高度的安全性和灵活性。 ... [详细]
  • 不确定性|放入_华为机试题 HJ9提取不重复的整数
    不确定性|放入_华为机试题 HJ9提取不重复的整数 ... [详细]
  • 对象自省自省在计算机编程领域里,是指在运行时判断一个对象的类型和能力。dir能够返回一个列表,列举了一个对象所拥有的属性和方法。my_list[ ... [详细]
  • 深入解析Java枚举及其高级特性
    本文详细介绍了Java枚举的概念、语法、使用规则和应用场景,并探讨了其在实际编程中的高级应用。所有相关内容已收录于GitHub仓库[JavaLearningmanual](https://github.com/Ziphtracks/JavaLearningmanual),欢迎Star并持续关注。 ... [详细]
  • 深入解析SpringMVC核心组件:DispatcherServlet的工作原理
    本文详细探讨了SpringMVC的核心组件——DispatcherServlet的运作机制,旨在帮助有一定Java和Spring基础的开发人员理解HTTP请求是如何被映射到Controller并执行的。文章将解答以下问题:1. HTTP请求如何映射到Controller;2. Controller是如何被执行的。 ... [详细]
  • 深入理解Java多线程并发处理:基础与实践
    本文探讨了Java中的多线程并发处理机制,从基本概念到实际应用,帮助读者全面理解并掌握多线程编程技巧。通过实例解析和理论阐述,确保初学者也能轻松入门。 ... [详细]
  • 深入剖析JVM垃圾回收机制
    本文详细探讨了Java虚拟机(JVM)中的垃圾回收机制,包括其意义、对象判定方法、引用类型、常见垃圾收集算法以及各种垃圾收集器的特点和工作原理。通过理解这些内容,开发人员可以更好地优化内存管理和程序性能。 ... [详细]
  • 本文详细探讨了Java中的ClassLoader类加载器的工作原理,包括其如何将class文件加载至JVM中,以及JVM启动时的动态加载策略。文章还介绍了JVM内置的三种类加载器及其工作方式,并解释了类加载器的继承关系和双亲委托机制。 ... [详细]
  • Java 架构:深入理解 JDK 动态代理机制
    代理模式是 Java 中常用的设计模式之一,其核心在于代理类与委托类共享相同的接口。代理类主要用于为委托类提供预处理、过滤、转发及后处理等功能,以增强或改变原有功能的行为。 ... [详细]
  • 可能存在无限递归_递归算法看这一篇就够了|多图
    前言递归是一种非常重要的算法思想,无论你是前端开发,还是后端开发,都需要掌握它。在日常工作中,统计文件夹大小, ... [详细]
  • 我正在尝试使用带有自定义类和哈希码方法的自定义类的哈希映射进行测试:publi ... [详细]
  • 讲下equals和hashcode,他们为何必须一起重写?hashcode方法重写规则。
    讲下equals和hashcode,他们为何必须一起重写?hashcode方法重写规则。-1、equals和hashCode(哈希码)是什么? 两个都是Object类的方法equ ... [详细]
  • 还要谈谈Equals和GetHashcode
    这篇随笔和上篇随笔《从两个数组中查找相同的数字谈Hashtable》都是为了下面分析Dictionary的实现做的铺垫一.两个逻辑上相等的实例对象。两个对象相等,除了指两个不同变量引用了 ... [详细]
author-avatar
QQ文科
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有