多线程--锁 详解

多线程–锁 Synchorized

1 CAS

1.1 CAS概念

​ CAS 即 (Compare And Swap / Compare And Set /Compare And Exchange)比较并交换,一种实现并发算法时用到的技术,是 JAVA 很多底层并发包的实现原理。

1.2 案例

先做一个实验,示例如下:

public static volatile int v1 = 0;	//定义一个共享变量
public staic void incease1(){		//自增
    v1++;
}
public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) { 
            Thread thread = new Thread(() -> { //启动10个线程,每个线程循环自增1000次
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            });
            thread.start();
        }
    	//保证是个线程都跑完
    	Thread.sleep(5000);
    	System.out.println("v1:"+v1);
}

执行完后,我们无法得到10000,得出输出结果总是小于 10000,并且每次都不一样;

我们稍作修改,使用 AtomicInteger:

    public static AtomicInteger v2 = new AtomicInteger(0);
    public static void increase2() {
        v2.getAndIncrement();

    }
    public static void main(String[] args) throws InterruptedException {
            Thread thread2 = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increase2();
                }
            });
            thread2.start();
        }
        Thread.sleep(5000);
        System.out.println("v2:" + v2);
    }

上述实验,总能得到正确 值 v2:10000;

点开AtomicInteger.getAndIncrement() 源码, 会发现调用了Unsafe.getAndAddInt();

AtomicInteger:
/**
* Atomically increments by one the current value.
*/
public final int getAndIncrement() {
   return unsafe.getAndAddInt(this, valueOffset, 1);
}


Unsafe:
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

注意 this.compareAndSwapInt();既是 CAS的实现

1.3 CAS流程

在这里插入图片描述

以a++为例,

首选 获取到 a 的值 v1 = 0,

然后执行自增,得到V1 = 1;

接着 再去获取 a 的值 记为 v2 。比较 v1 与 v2是否相等

如果相等,则认为a的值没有被其他线程修改,将a的 值set 为V1 ,即 a = 1;

如果不相等,则可能其他线程对a 进行了操作,修改了a的值,此时不对 a 值进行操作,回到获取 a的v1值。

如果v1与 v2一直不相等,则重复进行循环,即称为自旋。

如此通过没有加锁(添加synchorized)的方式,实现了线程安全。

1.4 java源码追踪分析

结合1.2案例 可知 调用链路如下(入参省略):

AtomicInteger.getAndIncrement() ----> Unsafe.getAndAddInt(); —> Unsafe.compareAndSwapInt();

 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

我们发现 compareAndSwapInt()方法 是 native方法,就是说 该方法是操作cpu的方法了,由C实现。想要查看 具体实现,得查看hotspot源码。追踪到方法如下:

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

看到执行了return (jint)(Atomic::cmpxchg(x, addr, e)) == e;

(cmpxchg 重点)cmpxchg是汇编指令,作用是比较并交换操作数。

继续往下,找到Atomic的linux实现(不同操作系统实现不同):

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


发现 它是一个内联的方法,mp(multi processor)是“os::is_MP()”的返回结果,“os::is_MP()”是一个内联函数,用来判断当前系统是否为多处理器。

  1. 如果当前系统是多处理器,该函数返回1。
  2. 否则,返回0。

如果是多处理器,即为cmpxchg添加lock 指令。 lock cmpxchg

intel手册对lock前缀的说明如下:

1 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
2 禁止该指令与之前和之后的读和写指令重排序。
3 把写缓冲区中的所有数据刷新到内存中。
上面的第1点保证了CAS操作是一个原子操作,第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义。

lock指令详解

1.5 ABA问题

假设有两个及以上线程操作对象 O 的 值 为 A,

Thread1线程 操作对象 O 进行CAS时,Thread2 将 O 的值 改为 B,然后Thread3 将 O的值又改为 A。

此时 Thread1 的值去 比较的时候,发现 O的 值 仍是 A。即 认为 O并没有改动,于是 将 O 的值

CAS的 ABA问题,当设置值的时候,如果有其他线程 将A的值改为B,然后更新O的值。即 Thread1 无法感知到 对象 O 从 A->B->A 的变化,此既是ABA问题。

解决办法,可以通过版本号机制,每次改动都给 O 设置一个版本号,compare 比较时 同时比较O的值与 版本号,若版本号 一致则更新;

若不一致 则自旋,获取最新版本。

Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

2 JOL (Java Obejct Layout)

2.1 Java对象在内存中的布局

在这里插入图片描述

如上图所示,最终 new Object();

对象在内存中由4个部分组成:

​ MarkWord:存储锁的信息,GC的信息,jvm默认大小是 8个字节

​ Class Pointer(类型指针): 存储指向Class的信息,默认4个字节

​ Instance Data(实例数据): 存储对象的数据,例 对象中有一个int类型的成员变量 ,即大小为 4个字节

​ Padding(对齐): 用于保证对象整体字节数能被 8 整除,为了提高读取运行效率

2.2 面试题Object obj = new Object();obj在内存中占几个字节?

详情查看2.1

3 锁升级

4 锁优化

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值