多线程二-同步锁

关于线程安全问题的简述

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

  • 原子性: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对象头

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

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

markword分布

markword分布

通过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获取预期的一个目标,如果没有获取到则循环获取,获取到了则表示获取到了锁。这个过程线程一直在运行相对而言没有使用太多的操作系统资源,比较轻量。
锁升级

偏向锁的开启有个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;
    }
欢迎大家留言,以便于后面的人更快解决问题!另外亦欢迎大家可以关注我的微信公众号,方便利用零碎时间互相交流。共勉!

posted @ 2024-07-06 21:42  东方欲晓_莫道君行早  阅读(2)  评论(0编辑  收藏  举报