多线程二-同步锁

关于线程安全问题的简述

多个线程做同一件事的时候

  • 原子性:Syncronized,AtomicXXX,Lock
  • 可见性:Syncronized,volatile
  • 有序性:Syncronized,volatile

原子性问题

代码演示了两个线程分别调用incr()方法来对i进行累加,预期结果应该是20000,但是实际结果却是小于等于20000的值,这就是线程安全问题中原子性的体现。在这段代码中i++属于Java高级语言中的编程指令,而这些指令最终可能会有多条CPU指令组成。通过javap -v Demo.class查看字节码指令如下:

  public void incr();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #7                  // Field i:I  访问变量i
         5: iconst_1                         //将整形常量1放入操作数栈
         6: iadd                               //把操作数栈中的常量1出栈并相加,将相加的结果放入操作数栈
         7: putfield      #7                  // Field i:I  访问类变量复制给demo.i这个变量
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/caozz/demo2/thread/Demo;

这三个操作,如果要满足原子性,那么就需要保证某个线程在执行这个指令时,不允许其他线程干扰。然后实际上,确实存在该问题。简单来说就是将变量i加载后,被切换到其他线程,导致的问题。

代码如下:

package com.caozz.demo2.thread;

public class Demo {
    int i;
    public void incr(){
        i++;
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        Thread[] threads = new Thread[2];
        for (int j = 0; j < 2; j++) {
            threads[j] = new Thread(() -> {            //创建两个线程
                for (int k = 0; k < 10000; k++) {      //每个线程跑10000次
                    demo.incr();
                }
            });
            threads[j].start();
        }
        try {
            threads[0].join();
            threads[1].join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(demo.i);
    }
}

Java中的同步锁syncronized

Markword对象头

对象在堆内存中的存储分布


image.png
  • 对象标记,也就是markword对象头,四个字节,用于存储一些列的标记位,比如哈希值,锁信息,分代年龄 等
  • 类元信息,即Klass Pointer,jdk8默认开启指针压缩后为4字节,可以使用参数-XX:-UseCompressedOops关闭指针压缩,关闭后长度为8位,其指向的位置是对象对应的class对象的内存地址
  • 实例数据:包括对象的所有成员变量,大小由各个成员变量决定
  • 对齐填充:并非必须,起到占位符作用。由于hotspot虚拟机的内存管理系统要求对象起始地址必须是8字节的整数倍,当对象实例数据部分没有对齐的话需要对其填充来补全。

markword分布

1720259587432.png

通过ClassLayout打印对象头

  • 添加依赖
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>
  • 测试代码
package com.caozz.demo2.thread;

import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

public class ClassLayoutTest {
    Object obj = new Object();

    public void testLock(){
        synchronized (this) {
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ClassLayoutTest classLayoutTest = new ClassLayoutTest();
        System.out.println(ClassLayout.parseInstance(classLayoutTest).toPrintable());

        System.out.println("-----------------------------------------------");

        ClassLayoutTest classLayoutTest02 = new ClassLayoutTest();
        new Thread(() -> {
            classLayoutTest02.testLock();
        }).start();

        new Thread(() -> {
            classLayoutTest02.testLock();
        }).start();

        new Thread(() -> {
            classLayoutTest02.testLock();
        }).start();

        System.out.println(ClassLayout.parseInstance(classLayoutTest02).toPrintable());
    }
}

  • 结果
com.caozz.demo2.thread.ClassLayoutTest object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      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)
      8     4                    (object header)                           00 18 c0 00 (00000000 00011000 11000000 00000000) (12589056)
     12     4   java.lang.Object ClassLayoutTest.obj                       (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

-----------------------------------------------
com.caozz.demo2.thread.ClassLayoutTest object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           a2 00 01 d1 (10100010 00000000 00000001 11010001) (-788463454)
      4     4                    (object header)                           c4 01 00 00 (11000100 00000001 00000000 00000000) (452)
      8     4                    (object header)                           00 18 c0 00 (00000000 00011000 11000000 00000000) (12589056)
     12     4   java.lang.Object ClassLayoutTest.obj                       (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  • 分析
    锁状态为对象头第一部分的第一个字节后三位,上述结果第一个为001,第二个为010,根据markword分布,可知分别为无锁状态以及重量级锁状态

Synchronized锁升级

jdk1.6对锁的实现引入了大量的优化,如自旋锁,自适应自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等技术来减少锁操作的开销。锁主要存在四种状态:无锁,偏向锁,轻量级锁,重量级锁,他们会随着锁竞争的激烈程度而逐渐升级。这么设计的目的是减少重量级锁带来的性能开销。

默认情况下偏向锁是开启状态,偏向锁是在锁对象的对象头记录当前获取到该锁的线程ID,线程下次再来就可以直接获取锁了。当有第二个线程过来竞争锁,偏向锁就会升级为轻量级锁。轻量级锁底层是通过自旋来实现的,不会阻塞线程。如果自旋次数过多,则会升级为重量级锁,重量级锁会阻塞线程。
自旋锁是线程通过CAS获取预期的一个目标,如果没有获取到则循环获取,获取到了则表示获取到了锁。这个过程线程一直在运行相对而言没有使用太多的操作系统资源,比较轻量。


image.png

偏向锁的开启有个4秒的延迟,这么设计的原因是因为jvm自己有一些默认启动的线程。如果这时候就使用偏向锁,会在成偏向锁不断的升级和撤销,效率极低。当然,延迟也是可以通过参数设置-XX:BiasedLockingStartupDelay=0

CAS机制

CAS,Compare And Swap,或compare and exchangecompare and set
,比较交换的意思。它可以保证在多线程环境下对于一个变量修改的原子性。
原理如下图:
通过查看源码,可以知道它是一个native方法,然后去查看jvm源码unsafe.cpp:

cmpxchg:compare and exchange

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

cmpxchg的原子性 底层也是通过锁来保证的:atomic_linux_x86.inline.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

Atomic实现原子性

由源码可以知道,他也是一个不断自旋来实现的

    public final int getAndSet(int newValue) {
        //private static final Unsafe U = Unsafe.getUnsafe();
        return U.getAndSetInt(this, VALUE, newValue);
    }
    @IntrinsicCandidate
    public final int getAndSetInt(Object o, long offset, int newValue) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, newValue));
        return v;
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容