java多线程-锁

我们都知道,多个线程并发访问共享变量或者共享资源就回来带来线程安全问题。
于是就可以想到一种保障线程安全的方法--将多个线程的并发访问转换位串行访问,即一个共享数据一次只能被一个线程访问。这个就是锁了。

java平台的锁包括内部锁和显式锁。
内部锁是通过synchronized关键字实现的,显式锁是通过Lock接口的实现类实现的。

锁的作用:保障原子性,可见性,和有序性。

  • 锁是通过互斥保障原子性的,因为锁一次只能被一个线程持有,就保证了临界区代码一次只能被一个线程执行,这使得临界区代码所执行的操作具有不可分割的特性,即具备原子性。
  • 锁的获得隐含着刷新处理器缓存这个动作,即执行临界区代码前,可以将写线程对共享变量所做的更新同步到该线程执行处理器的高速缓存中。而锁的释放隐含着冲刷处理器缓存这个动作。使得写线程对共享变量所作的更新能够被推送到该线程执行处理器的高速缓存中,从而对读线程可同步。因此,锁能够保证可见性。
  • 由于锁对可见性的保证,写线程在临界区中对任何一个共享变量所做的更新都对线程可见。由于临界区内的操作具有原子性,因此写线程对共享变量的更新同时对读线程可见

synchronized关键字

  • synchronized可以用来修饰方法或者代码块
  • 作为锁句柄的变量通常用final修饰,这是因为锁句柄变量的值一旦改变,会导致执行同一个同步快的多个线程实际上使用不同的锁,从而导致竞态。
  • 线程对内部锁的申请与释放的动作由java虚拟机负责代为实施。

内部锁的调度:java虚拟机为每个内部锁分配一个入口集,用于记录等待获得相应内部锁的线程。多个线程申请同一个锁的时候,只有一个申请者能申请成为该锁的持有线程,而其他申请者的申请操作会失败。申请失败的线程会被暂停并被存入相应锁的入口集中等待再次申请锁的机会。当线程申请的锁被释放时,该锁的入口集中的一个任意线程会被java虚拟机唤醒,从而得到再次申请锁的机会。

  • 原理
    1.理解Java对象头与Monitor
    在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充,如图:
    image.png

    实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
    对齐填充:由于虚拟机要求对象得起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
    对象头:包含Mark word和class metadata address两部分。
    mark work的存储内容如下
    image.png

    metadata address:即元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

如果对象是数组,那对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中无法确定数组的大小。

mark work里面的重量级锁,也就是我们通常说的synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或者监视器锁)的起始地址。每个对象都存在一个monitor与之关联,monitor可以与对象一起创建销毁,也可以当线程试图获取对象锁时自动生成,当一个monitor被某个线程持有后,它便处于锁定状态。
monitor的实现
monitor其实是一种同步工具,被描述为一个对象,他的义务是保证只有一个线程可以访问被保护的数据和代码。
在HotSpot中,monitor是基于c++实现的,由ObjectMonitor实现,结构如下

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;//用来记录该线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;//指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL;//存放处于wait状态的线程队列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;//锁的重入次数
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;//存放处于等待锁block状态的线程队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

当多个线程同时访问一段同步代码的时候,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域,并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1,即获得对象锁

若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor并复位变量的值,以便其他线程进入获取monitor.
以下是获取锁的过程:


image.png

monitor对象存在于每个java对象的对象头中,synchronized锁便是通过这种方式获取锁的,这也是为什么java中任意对象可以作为锁的原因。
同时也是notify/notifyAll/wait等方法存在于顶级对象object中的原因,在使用这几个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll/wait这几个方法依赖于monitor对象,而要获取monitor,就必须通过synchronized关键字,这也就是notify/notifyAll/wait方法必须在synchronized代码块或者synchronized方法调用的原因了。

synchronized的实现原理

public class SynchronizedDemo {
//同步方法
    public synchronized void doSth(){
        System.out.println("Hello World");
    }

//同步代码块
    public void doSth1(){
        synchronized (SynchronizedDemo.class){
            System.out.println("Hello World");
        }
    }
}

看一下上面代码的字节码,如下

public synchronized void doSth();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
  public void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5                  // class com/hollis/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #3                  // String Hello World
        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return

通过反编译后代码可以看出,对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。对于同步代码块,JVM采用monitorenter,monitorexit两个指令来实现同步。

当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器,然后再执行方法,方法执行后释放监视器锁。如果在方法执行过程中,发生异常,并且方法内部没有处理该异常,那么异常被抛到方法外面之前监视器锁会自动释放。

同步代码块使用monitorenter和monitorexit两个指令实现。可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。每个对象维护着一个记录着被锁次数得计数器。未被锁定得对象得该计数器为0,当一个线程获得锁后,该计数器自增变为1,当一个线程再次获得该对象得锁时,计数器再次自增。当线程释放锁时,计数器自减。当计数器为0的时候,锁被释放。

synchronized为什么被叫重量级锁?
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步快状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说是一个重量级的操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容