java并发和锁,自己动手(一)

3 篇文章 0 订阅
2 篇文章 0 订阅

CAS(乐观锁)

cas属于轻量级锁(无锁,自旋锁)
底层实现是lock cmpxchg

CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:
主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
工作内存中共享变量的副本值,也叫预期值:A
需要将共享变量更新到的最新值:B
请添加图片描述
如上图中,主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同,如果不相同证明此时V值已经被其他线程改变,重新将V值赋给A,并重新计算得到B,如果相同,则将B值赋给V。

值得注意的是CAS机制中的这步步骤是原子性的(从指令层面提供的原子操作),所以CAS机制可以解决多线程并发编程对共享变量读写的原子性问题。

AtomicInteger

Unsafe

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
AtomicInteger中使用unsafe
在这里插入图片描述
AtomicInteger 的源码

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            //用于获取value字段相对当前对象的“起始地址”的偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    //返回当前值
    public final int get() {
        return value;
    }

    //递增加detla
    public final int getAndAdd(int delta) {
        //三个参数,1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(value+delta)。
        //增加的时候遇到其他线程使用会自旋,见下文unsafe类的介绍
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    //递增加1
    public final int incrementAndGet() {
    	//增加的时候遇到其他线程使用会自旋,见下文unsafe类的介绍
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
...
}

unsafe中的cas,

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
        	//通过var1和var5找到主内存中的值
            var5 = this.getIntVolatile(var1, var2);
            //比较var5是否改变,如果没变就直接赋值并返回,改变了继续循环
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

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

在cpp中compareAndSwapInt是这样的

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

最终c++层是lock cmpxchg
cmpxchg不是原子性的,因为compare的时候有可能其他cpu修改内存值
lock能够保证原子性

所以说cas属于轻量级锁(无锁,自旋锁),可以说他不是锁,因为确实没有上锁,但是又有锁的效果,说他是自旋锁,是因为他呈现出来的就是自旋的样子

锁升级

在这里插入图片描述

偏向锁

当一个线程使用未被访问过的资源的时候,给资源贴上标签,属于这个线程。而这种情况下也是这个线程访问这个资源,无竞争。而这个贴标签的过程就是使用一个54位的指针指向当前线程

轻量级锁

只要发生任何竞争,就从偏向锁升级成轻量级锁。首先会撤销偏向锁,然后每个线程栈中增加一个LR(Lock Record)指针,每个线程通过自旋的方式去访问资源。具体情况如下

假设线程A和线程B
线程A先获取公共锁到自己的内存中,假设此时锁的指针是指向1
然后A把自己的LR指针赋值到锁中,此时锁指向A
A再去获取一下公共锁的指针,此时1的状态是可改变,就把当前的指向A的锁放回去。否则如果获取的锁改变,就重新来,自旋。
类似于上面的cas图片

自适应自旋锁

竞争的线程太多,或者某个线程占用的时间太长,导致其他锁自旋的时间太长,消耗cpu资源。
jdk1.6之前,如果自旋次数超过10次,或者自旋线程数量超过cpu核数的一半的时候,就变成重量级锁。
jdk1.6之后引入了自适应自旋锁

重量级锁

竞争的线程太多,或者某个线程占用的时间太长,导致其他锁自旋的时间太长,消耗cpu资源。升级成重量级锁。重量级锁需要向操作系统申请

锁消除

public void add(String a, String b){
	StringBuffer sb = new StringBuffer();
	sb.append(a).append(b);
}

例如stringbuffer,是线程安全的,如果在使用他的时候,正常内部会上锁。但是如果JVM判断sb并不会被其他代码引用,则将该代码带默认为线程独有的资源,并不需要同步,所以执行了锁消除操作。

锁粗化

public void test(String a){
	int i = 0;
	StringBuffer sb = new StringBuffer();
	while(i < 100){
		sb.append(a);
		i++
	}
	return sb.toString();
}

锁会加到while上

根据例子学习锁

Object在内存对象中占用多少字节?
在这里插入图片描述
markword -> 8
classpointer -> 4(compressed)
null对齐 -> 4(凑到8的倍数,8字节64位)
o引用本身 -> 4
一共20字节
在这里插入图片描述
classpointer和简单对象指针都进行了压缩,从8变成4
在这里插入图片描述

超线程

JMM(java内存模型)

多线程中就是三个性质1.可见性, 2.原子性,3.有序性。下面的锁对应不同的特性

原理使用场景
final实现了Java内存模型的,有序性一个变量要被多个线程访问(类私有)
volatile实现了Java内存模型的,可见性、有序性一个变量要被多个线程访问(存放在共享内存内,共享)
synchronized实现了Java内存模型的,原子性、可见性、有序性
AtomicCAS一个变量要被多个线程访问

synchronized

synchronized是重量级锁
synchronized实现了多线程的有序性,可见性,原子性
代码块使用锁block,其他代码块需要使用block的时候需要等待锁释放。
在代码块处的锁可以是this当前实例。
可以是某个类AnyClass.class,这样所有的实例公用一把锁。

	synchronized (block) {
            System.out.println("block1锁,我是线程" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("block1锁,"+Thread.currentThread().getName() + "结束");
   }
	synchronized (this) {
		...
   }
   	synchronized (AnyClass.class) {
		...
   }

synchronized用在普通方法上,默认的锁就是this,当前实例,不同的实例可以同时访问。
如果作用在static修饰的方法上那就代表着任何实例访问都有一个公共锁。

    public synchronized void method() {
        System.out.println("我是线程" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "结束");
    }
    public static synchronized void method() {
		...
    }

注意事项

性能问题:多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。
1.锁对象不能为空,因为锁的信息都保存在对象头里
2.作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
3.避免死锁
4.在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错

缺陷

1.效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
2.不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
3.无法知道是否成功获得锁,相对而言,Lock可以拿到状态,如果成功获取锁,…,如果获取失败,…

volatile

volatile实现了可见性,有序性,但是不能保证原子性比如i++在多线程中i使用volatile就不行

线程间可见性

如下代码,如果running不加上volatile,那么程序就会卡住。如果加上了,主线程和t1线程之间就可共享running值。修改会重新写入内存,读取会重新从内存中拿
在这里插入图片描述

有序性(happens-before)

//假设线程A执行writer方法,线程B执行reader方法
class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
    
    public void writer() {
        a = 1;              // 1 线程A修改共享变量
        flag = true;        // 2 线程A写volatile变量
    } 
    
    public void reader() {
        if (flag) {         // 3 线程B读同一个volatile变量
        int i = a;          // 4 线程B读共享变量
        ……
        }
    }
}

根据程序次序规则:1 happens-before 2 且 3 happens-before 4。
根据 volatile 规则:2 happens-before 3。
根据 happens-before 的传递性规则:1 happens-before 4。

上面这个happens-before规则是怎么来的呢,想要深入理解,还得先看下文

DCL例子,禁止重排序

下面的例子为例
多线程下的单例模式(DCL)下的双重检查

public class Singleton {
    public static volatile Singleton singleton;
    /**
     * 构造函数私有,禁止外部实例化
     */
    private Singleton() {};
    public static Singleton getInstance() {
    	//检查1
        if (singleton == null) {
            synchronized (singleton.class) {
            	//检查2
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

可以模拟一下两个线程访问未初始化的资源,最终这种双重检查不会出现问题。

这种可以不使用volatile吗?你看我这个是单例模式,多线程下访问的也只有这一个singleton对象,为什么还要加入volatile呢?
答案是,通过内存屏障防止重排序

Java中的指令重排序有两次,第一次发生在将字节码编译成机器码的阶段,第二次发生在CPU执行的时候,也会适当对指令进行重排。
重排序意思就是,代码执行的顺序可能不是你自己定义的,可能是一个乱序,只不过执行的逻辑不变。单线程没问题,但是多线程就会有问题。
上面的代码重排序可能发生在对象new的时候,字节码编译成机器码时重排序。比如正常的new过程是先半初始化Singleton类为null(0),然后再初始化赋值(4),最后singleton变量和实例关联(7)
在这里插入图片描述
如果4和7被重排了,那么多个线程获取singleton的时候有可能有获取值为null的可能,这是另一个线程在初始化的时候还没执行完,同时被重排指令了。这个时候再看上面的代码,就可能new 两个单例变量

什么是内存屏障呢?
详细文章看看:https://segmentfault.com/a/1190000038355290
前后两个操作之间的屏障。
简单理解就是屏障是墙,阻挡前后。volatile读不可以和下面的任何写重排序。volatile写不可以和下面的读,上面的写重排序。
下面是具体的规则
请添加图片描述
在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

内存屏障说明
StoreStore 屏障禁止上面的普通写和下面的 volatile 写重排序。
StoreLoad 屏障防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。
LoadLoad 屏障禁止下面所有的普通读操作和上面的 volatile 读重排序。
LoadStore 屏障禁止下面所有的普通写操作和上面的 volatile 读重排序。

final

final基础部分比较容易理解

写重排序规则

写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了

其中变量a可以重排序到初始化函数之外,而变量b一定在初始化函数之内,因为构造函数return之前,插入一个storestore屏障。并且在reader都b之前保证先初始化了。

public class FinalDemo {
    private int a;  //普通域
    private final int b; //final域
    private static FinalDemo finalDemo;

    public FinalDemo() {
        a = 1; // 1. 写普通域
        b = 2; // 2. 写final域
    }

    public static void writer() {
        finalDemo = new FinalDemo();
    }

    public static void reader() {
        FinalDemo demo = finalDemo; // 3.读对象引用
        int a = demo.a;    //4.读普通域
        int b = demo.b;    //5.读final域
    }
    public static void main(String[] args){
    	//读写线程
    	Tread(FinalDemo::writer,"t1").start();
    	Tread(FinalDemo::reader,"t2").start();
    }
}

读重排序规则

读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。
还是上面的代码,意思是在执行int b = demo.b的时候一定先执行FinalDemo demo = finalDemo,这个是final变量,读取引用和读取变量之间有一个LoadLoad屏障。
相反int a = demo.a可以窜到FinalDemo demo = finalDemo前面执行(不是很理解)

final是否有线程可见性?

最后想说一点,final无法保证线程间可见性
举例:线程线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法

public class FinalReferenceDemo {
    final int[] arrays;
    private FinalReferenceDemo finalReferenceDemo;

    public FinalReferenceDemo() {
        arrays = new int[1];  //1
        arrays[0] = 1;        //2
    }

    public void writerOne() {
        finalReferenceDemo = new FinalReferenceDemo(); //3
    }

    public void writerTwo() {
        arrays[0] = 2;  //4
    }

    public void reader() {
        if (finalReferenceDemo != null) {  //5
            int temp = finalReferenceDemo.arrays[0];  //6
        }
    }
}

其中可以保证temp读取的内容=1,但是arrays[0] = 2在哪块执行就未必了

参考:
https://segmentfault.com/a/1190000022904663
https://pdai.tech/md/java/thread/java-thread-x-key-volatile.html#volatile-%E7%9A%84-happens-before-%E5%85%B3%E7%B3%BB
https://segmentfault.com/a/1190000038355290

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值