前言
-
上篇文章总结了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关键字,不考虑其他的情况)。于是,我们进行代码修改,如下所示:
修改后的代码为,新建了一个Lock类作为锁对象。这样就完成了同步的操作。下面总结下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 { }
二、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 // 输出结果参考如下内容: // 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.