ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

基于synchronized锁的深度解析

2020-11-09 09:01:29  阅读:144  来源: 互联网

标签:synchronized 对象 自旋 线程 轻量级 深度 解析 偏向




1. 问题引入


小伙伴们都接触过线程,也都会使用线程,今天我们要讲的是线程安全相关的内容,在这之前我们先来看一个简单的代码案例。


代码案例:

/**
 * @url: i-code.online
 * @author: AnonyStar
 * @time: 2020/10/14 15:39
 */
public class ThreadSafaty {
    //共享变量
    static int count = 0;

    public static void main(String[] args) {

        //创建线程
        Runnable runnable = () -> {
            for (int i = 0; i < 5; i++) {
                count ++;
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 100; i++) {
            new Thread(runnable,"Thread-"+i).start();
        }

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count = "+ count);
    }
}

执行结果:



问题说明:
在上面的代码中我们可以看到,定义了一个线程 runnable 里面对公共成员变量进行 ++ 操作,并循环五次,每次睡眠一毫秒,之后我们在主线程 main 方法中创建一百个线程并且启动,然后主线程睡眠等待五秒以此来等所有的线程执行结束。我们预期结果应该是 500 。但是实际执行后我们发现 count 的值是不固定的 ,是小于 500 的,这里就是多线程并行导致的数据安全性问题!

通过上述案例我们可以清楚的看到线程安全的问题,那么我们想想是否有什么办法来避免这种安全问题尼 ?我们可以想到导致这种安全问题的原因是因为我们访问了共享数据,那么我们是否能将线程访问共享数据的过程变成串行的过程那么不就是不存在这个问题了。这里我们可以想到之前说的 ,我们知道锁是处理并发的一种同步方式,同时他也具备互斥性,在Java中实现加锁是通过 synchronized 关键字


2. 锁的基本认识

2.1 Synchronized 的认识

Java 中我们知道有一个元老级的关键字 synchronized ,它是实现加锁的关键,但是我们一直都认为它是一个重量级锁,其实早在 jdk1.6 时就对其进行了大量的优化,让它已经变成非常灵活。也不再一直是重量级锁了,而是引入了 **偏向锁 **和 **轻量级锁。 **关于这些内容我们将详细介绍。

synchronized的基础使用

  • synchronized 修饰实例方法,作用于当前实例加锁
  • synchronized 修饰静态方法,作用于当前类对象加锁,
  • synchronized 修饰代码块,指定加锁对象,对给定对象加锁,

在上述情况中,我们要进入被 synchronized 修饰的同步代码前,必须获得相应的锁,其实这也体现出来针对不同的修饰类型,代表的是锁的控制粒度

  • 我们修改一下前面我们写的案例,通过使用 synchronized 关键字让其实现线程安全
 //创建线程
        Runnable runnable = () -> {
            synchronized (ThreadSafaty.class){
                for (int i = 0; i < 5; i++) {
                    count ++;
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        };

只需要添加 synchronized (ThreadSafaty.class) 的修饰,将操作的内容放入代码块中,那么就会实现线程安全

  • 通过上面的实践我们可以直观感受 synchronized 的作用,这是我们平时开发中常规使用,大家有没有过疑问,这个锁到底是怎么存储实现的?那么下面我们将对探索其中的奥秘

Java中锁的实现

  • 我们知道锁是具有互斥性(Mutual Exclusion)的 ,那么它是在什么地方标记存在的尼?

  • 我们也知道多个线程都可以获取锁,那么锁必然是可以共享的

  • 我们最熟悉的 synchronized 它获取锁的过程到底是怎么样的呢?它的锁是如何存储的呢?

  • 我们可以注意观察 synchronized 的语法,可以看到 synchronized(lock) 是基于 lock 的生命周期来实现控制锁粒度的,这里一定要理解,我们获得锁时都时一个对象,那么锁是不是会和这个对象有关系呢?

  • 到这里为止,我们将所有的关键信息都指向了对象,那么我们有必要以此为切入点,来首先了解对象在 jvm 中的分布形式,再来看锁是怎么被实现的。

对象的内存布局

  • 这里我们只谈论对象在 Heap 中的布局,而不会涉及过多的关于对象的创建过程等细节,这些内容我们再单独文章详细阐述,可以关注 i-code.online 博客或wx "云栖简码"

  • 在我们最常用的虚拟机 hotspot 中对象在内存中的分布可以分为三个部分:对象头(Header)、实列数据(Instance Data)、对其填充(Padding


  • 通过上述的图示我们可以看到,对象在内存中,包含三个部分,   其中对象头内分为 对象标记与类元信息,在对象标记中主要包含如图所示 hashcode、GC分代年龄、锁标记状态、偏向锁持有线程id、线程持有的锁(monitor)等六个内容,这部分数据的长度在 32 位和64位的虚拟机中分别为32bit 和 64bit,在官方将这部分称为 Mark Word
  • Mark Word 实际是一中可以动态定义的数据结构,这样可以让极小的空间存储尽量多的数据,根据对象的状态复用自己的内存空间,比如在32位的虚拟机中,如果对象未被同步锁锁定的状态下, Mark Word 的32个比特存储单元中,25个用于存储哈希码,4个用于存储GC分代年龄,2个存锁标记位,1个固定位0,针对各个状态下的分布可以直观的参看下面的图表


32位HotSpot虚拟机对象头Mark Word

锁状态 25bit 4bit 1bit
(是否是偏向锁)
2bit
(锁标志位)
23bit 2bit
无锁 对象的HashCode 分代年龄 0 01
偏向锁 线程ID Epoch(偏向时间戳) 分代年龄 1 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向重量级锁的指针 10
GC标记 11

上述说的是32位虚拟机,需要注意。关于对象头的另一部分是类型指针,这里我们不展开再细说了,想了解的关注 i-code.online ,会持续更新相关内容

标签:synchronized,对象,自旋,线程,轻量级,深度,解析,偏向
来源: https://www.cnblogs.com/i-code/p/13946843.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有