并发编程--Synchronized

一个问题引发思考


public static int count=0;
public static void incr(){ 
    try {
        Thread.sleep(1); 
    } catch (InterruptedException e) {
         e.printStackTrace(); 
    }
    count++; 
}
public static void main( String[] args ) throws InterruptedException {
    for(int i=0;i<1000;i++){
          new Thread(()->App.incr()).start();
    }
    //保证线程执行结束
    Thread.sleep(3000); 
    System.out.println("运行结果:"+count);
}

结果是小于等于1000的随机数。
原因: 可见性、原子性

count++的指令


  14: getstatic #5 // Field count:I
  15: iconst_1 
  16: iadd 
  17: putstatic #5

getstatic 获取到的i,可能不是最新的,线程之间的不可见导致结果出现不一致。

锁(Synchronized)


互斥锁的本质是什么.

共享资源


加锁逻辑图:


2.png

锁的使用


可以修饰在方法层面和代码块层面

class Test {
    // 修饰非静态方法 
    synchronized void demo() { 
    // 临界区 
    }
     // 修饰代码块 
    Object obj = new Object();
    void demo01() { 
        synchronized(obj) { 
         // 临界区
        } 
  }
}

锁的作用范围


synchronized有三种方式来加锁,不同的修饰类型,代表锁的控制粒度:

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

锁的存储(对象头)


  • 32位、64位对象头
// -------- 
# 32 bits: 
# hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object) 
# JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
# size:32 ------------------------------------------>| (CMS free block) 
# PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object) 
//-----------

 // -------- 
# 64 bits:
# unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object) 
# JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) 
# PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
# size:64 ----------------------------------------------------->| (CMS free block)
//-------

看图说话:根据对象头信息判断加锁的类型。

  • 偏向锁直接在对象头中存储线程信息,注意偏向锁已无法存储hashCode,所以一旦调用对象的hashcode,偏向锁立刻会升级成重量级锁
  • 轻量级锁和重量级锁会在栈中保存线程信息。
1.png

打印类的布局

打印对象头信息->ClassLayout.parseInstance(object).toPrintable()

<dependency>
     <groupId>org.openjdk.jol</groupId> 
     <artifactId>jol-core</artifactId>
     <version>0.10</version> 
</dependency>

如何查看锁状态位

  • 16进制: 0x 00 00 00 00 00 00 00 01
    (64位)2进制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000 0
    01 (无锁状态)
    通过最后三位来看锁的状态和标记。

  • 【大端存储和小端存储】
0 4 (object header)   01 00 00 00      (00000001 00000000 00000000 00000000) (1)
4 4 (object header)   00 00 00 00      (00000000 00000000 00000000 00000000) (0)

通过打印加锁类来查看对象头


public static void main(String[] args) { 
    ClassLayoutDemo classLayoutDemo=new ClassLayoutDemo(); 
    synchronized (classLayoutDemo){ 
        System.out.println("locking");        
        System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable()); 
    }
 }

输出结果. (轻量级锁) 最后三位为 000 表示轻量级锁

org.example.ClassLayoutDemo object internals:
 OFFSET  SIZE     TYPE               DESCRIPTION VALUE 
0       4         (object header)    88 f1 bb 02(10001000 11110001 10111011 00000010) (45871496) 
4       4         (object header)    00 00 00 00(00000000 00000000 00000000 00000000) (0) 
8       4         (object header)    05 c1 00 f8(00000101 11000001 00000000 11111000) (-134168315) 
12      4         (loss due to the next object alignment) 
Instance size: 16 bytes 
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

锁的升级


偏向锁


  • 在大多数情况下,锁不仅仅不存在多线程的竞争,而且总是由同一个线程多次获得。在这个背景下就设计了偏向锁。
  • 偏向锁,顾名思义,就是锁偏向于某个线程。当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。
  • 如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。(偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。)

偏向锁的对象头

org.example.ClassLayoutDemo object internals:
 OFFSET SIZE TYPE  DESCRIPTION     VALUE 
  0       4         (object header)   05 e8 45 03 (00000101 11101000 01000101 00000011) (54913029) 
  4       4         (object header)   00 00 00 00 (00000000 00000000 00000000 00000000) (0) 
  8       4         (object header)   05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 
  12      4         (loss due to the next object alignment) 
  Instance size: 16 bytes 
  Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

轻量级锁


如果偏向锁被关闭或者当前偏向锁已经已经被其他线程获取,那么这个时候如果有线程去抢占同步锁时,锁会升级到轻量级锁。

重量级锁


  • 多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程;
  • Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的:os.pthread_mutex_lock()
  • 升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态

重量级锁的案例

public static void main(String[] args) throws Exception { 
    TestDemo testDemo = new TestDemo(); 
    Thread t1 = new Thread(() -> { 
        synchronized (testDemo){ 
            System.out.println("t1 lock ing"); 
            System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
       } 
    }); 
    t1.start(); 
    synchronized (testDemo){ 
        System.out.println("main lock ing"); 
        System.out.println(ClassLayout.parseInstance(testDemo).toPrintable()); 
    } 
}
  • 每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized修饰的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。
  • monitorenter表示去获得一个对象监视器。monitorexit表示释放monitor监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器
    monitor依赖操作系统的MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能
  • 任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

总结


  • 偏向锁只有在第一次请求时采用CAS在锁对象的标记中记录当前线程的地址,在之后该线程再次进入同步代码块时,不需要抢占锁,直接判断线程ID即可,这种适用于锁会被同一个线程多次抢占的情况。
  • 轻量级锁才用CAS操作,把锁对象的标记字段替换为一个指针指向当前线程栈帧中的LockRecord,该工件存储锁对象原本的标记字段,它针对的是多个线程在不同时间段内申请通一把锁的情况。
  • 重量级锁会阻塞、和唤醒加锁的线程,它适用于多个线程同时竞争同一把锁的情况。

线程的通信(wait/notify)


在Java中提供了wait/notify这个机制,用来实现条件等待和唤醒。这个机制我们平时工作中用的少,但是在很多底层源码中有用到。比如以抢占锁为例,假设线程A持有锁,线程B再去抢占锁时,它需要等待持有锁的线程释放之后才能抢占,那线程B怎么知道线程A什么时候释放呢?这个时候就可以采用通信机制。

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

推荐阅读更多精彩内容