Java中的同步

0 cas

翻译一下就好理解了,比较并交换。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。

具体的同步过程
从地址 V 读取值 A,执行多步计算来获得新值 B,然后使用 CAS 将 V 的值从 A 改为 B。

此时存在两种情况

  1. 位置 V 处的值还 A ,那就直接将 A 改成 B 。CAS操作成功。

  2. 位置 V 处的值被其他线程更改成了 C ,那当前线程不会将 C 改成 B ,而是返回 C (在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)。CAS操作失败。

思考一下,C = A 的时候怎么办?

0.1 为什么要用CAS

我们与synchronized关键字来做比较,synchronized是一种独占锁,独占锁属于悲观锁,也就是每次操作都认为线程是不安全的,他会导致其它所有需要锁的线程被挂起,等待持有锁的线程释放锁。相对应的,乐观锁用到的原理就是CAS。所以,CAS操作要比synchronized这种方式性能有很大提升。
举了例子,同样实现线程安全下的i++操作。

public volatile int i;
public synchronized void add(){
  i++;
}
public AtomicInteger i;
public void add(){
  i.getAndIncrement();
}

0.2 (CAS) i++源码解析

包名:package java.util.concurrent.atomic;

//AtomicInteger类
//变量value; 相当于i++中的i
private volatile int value;
//创建Unsafe类的实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//变量value的偏移量, 具体赋值是在下面的静态代码块中中进行的
private static final long valueOffset;
//在静态代码块中获取变量value的偏移量
static {
    try {
        //获取value变量的偏移量, 赋值给valueOffset
        valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

//执行value++操作
public final int getAndIncrement() {
        //this是当前对象
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

// ....

//Unsafe类
//获取内存地址为obj+offset的变量值, 并将该变量值加上delta
public final int getAndAddInt(Object obj, long offset, int delta) {
    int v;
    do {
        //通过对象和偏移量获取变量的值
        //由于volatile的修饰, 所有线程看到的v都是一样的
        v= this.getIntVolatile(obj, offset);
    } while(!this.compareAndSwapInt(obj, offset, v, v + delta));

    return v;
}


静态方法中对valueOffset变量的赋值,应该是获取了value变量相对于对象内存地址的偏移量,得到的结果是value的内存地址,相当于获取了CAS操作数中的-内存位置(V)。这样后面可以根据这个地址中的值来进行比较赋值的操作。

while中的compareAndSwapInt()方法尝试修改v的值,该方法也会通过obj和offset获取变量的值如果这个值和v不一样, 说明其他线程修改了obj+offset地址处的值, 此时compareAndSwapInt()返回false, 继续循环如果这个值和v一样, 说明没有其他线程修改obj+offset地址处的值, 此时可以将obj+offset地址处的值改为v+delta, compareAndSwapInt()返回true, 退出循环。
Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改obj+offset地址处的值的时候不会被其他线程中断。

另外我们可以看出,CAS使用的正是非阻塞算法,即一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

源码大法好。

0.3 CAS原理

CAS是通过调用JNI的代码实现的,而JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言,使用native修饰,如C++。

涉及cpu底层指令等,这里就先不说了,不太懂。

0.4 CAS操作会产生哪些问题

我们在实现某些功能的时候几乎没有一种完美的解决方案,趋利避害的适应业务的实现方式就是好的方案。

CAS虽然能够高效的解决原子操作,但是仍然存在三大问题。ABA问题、循环时间长开销大、只能保证一个共享变量的原子操作。

0.4.1 什么是ABA问题,怎么解决

CAS在执行的过程中,预期原值(A)变成了新值(B),后面又变成了(A),相当于A-B-A。那么这两个A肯定不是同一个A,只不过是值相等罢了。

举个例子,有一个变量 i = 0在主内存中,线程A和B都获取了这个变量 i = 0 到自己的工作内存中。然后线程A先将变量 i 修改成了 1 ,后面又修改成了 0 ,并且线程A执行完毕后将 0 这个值保存到了主内存中。接着,线程B也执行完成了,需要把 i 从工作内存中保存到主内存时,发现主内存中的 i 没有发生变化,这时线程B认为CAS操作成功,并且返回了CAS操作成功的结果。

ABA问题的解决方式就是给变量加一个版本号。

package java.util.concurrent.atomic;包中有一个AtomicStampedReference的类就是用来解决ABA问题的。

上源码

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

compareAndSet方法会检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

0.4.2 循环时间长开销大

上面 i++ 的源码里getAndAddInt方法中有个循环,如果大量线程同时更新的话,所有线程都在循环获取,但是最终只有一个可以成功,会给CPU带来很大的执行开销。

这时候就需要限制自旋次数,防止进入死循环,或者限制循环时间。

从jdk1.8开始LongAdderConcurrentHashMap就是采用的这种方式,这也就是为什么ConcurrentHashMap可以替代HashTable

0.4.3 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。

常见的解决办法是使用AtomicReference类来操作对象,保证对象中被封装的多个共享变量的原子性。

此外,还可以使用锁来保证原子性,但是这种方式感觉有点违背CAS的初衷。

0.5 AtomicStampedReferenceAtomicReference的使用示例

    @Data
    static
    class Dog {
        private String name;
        private int age;

        public Dog(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }
    public static void main(String[] args) {
        AtomicReference<Dog> atomicReference = new AtomicReference<>();
        Dog jinmao = new Dog("糖糖", 2);
        atomicReference.set(jinmao);
        System.out.println(" 1 >>>>>>>>> : "+atomicReference.get());
        Dog labuladuo = new Dog("小七", 3);
        boolean b = atomicReference.compareAndSet(jinmao, labuladuo);
        System.out.println(" 2 >>>>>>>>> : "+b);
        System.out.println(" 3 >>>>>>>>> : "+atomicReference.get());
    }

输出

1 >>>>>>>>> : Test.Dog(name=糖糖, age=2)
2 >>>>>>>>> : true
3 >>>>>>>>> : Test.Dog(name=小七, age=3)

compareAndSet方法其实就是比较原值A是否还是jinmao,如果是,那就将jinmao更改为labuladuo。

我们再来看一个多线程的例子

    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        AtomicReference<Dog> atomicReference = new AtomicReference<>();
        Dog jinmao = new Dog("糖糖", 2);
        atomicReference.set(jinmao);
        System.out.println(" 1 >>>>>>>>> : " + atomicReference.get());
        pool.execute(()->{
            Dog samo = new Dog("旺仔", 4);
            boolean b = atomicReference.compareAndSet(jinmao, samo);
            System.out.println(" 2 >>>>>>>>> : " + atomicReference.get());
            System.out.println(" 2 >>>>>>>>> : " + b);
        });
        pool.execute(()->{
            Dog labuladuo = new Dog("小七", 3);
            boolean b = atomicReference.compareAndSet(jinmao, labuladuo);
            System.out.println(" 3 >>>>>>>>> : " + atomicReference.get());
            System.out.println(" 3 >>>>>>>>> : " + b);
            pool.shutdown();
        });
    }

输出

1 >>>>>>>>> : Test.Dog(name=糖糖, age=2)
2 >>>>>>>>> : Test.Dog(name=旺仔, age=4)
2 >>>>>>>>> : true
3 >>>>>>>>> : Test.Dog(name=旺仔, age=4)
3 >>>>>>>>> : false

boolean b = atomicReference.compareAndSet(jinmao, labuladuo);这一句的jinmao已经变成samo了,所以CAS操作失败。

我们再来看一个改变对象变量的操作,记住,atomicReference对于对象的操作是原子性的。

    public static void main(String[] args) {
        AtomicReference<Dog> atomicReference = new AtomicReference<>();
        Dog jinmao = new Dog("糖糖", 2);
        atomicReference.set(jinmao);
        System.out.println(" 1 >>>>>>>>> : "+atomicReference.get());
        jinmao.setAge(20);
        atomicReference.compareAndSet(jinmao, jinmao);
        Dog labuladuo = new Dog("小七", 3);
        boolean b = atomicReference.compareAndSet(jinmao, labuladuo);
        System.out.println(" 2 >>>>>>>>> : "+b);
        System.out.println(" 3 >>>>>>>>> : "+atomicReference.get());
    }

输出

1 >>>>>>>>> : Test.Dog(name=糖糖, age=2)
2 >>>>>>>>> : true
3 >>>>>>>>> : Test.Dog(name=小七, age=3)

修改成功了,为什么?因为jinmao对象没有变。显然我们要的不是这个结果,怎么办?轮到AtomicStampedReference出场了。

    public static void main(String[] args) {
        int version = 0;
        Dog jinmao = new Dog("糖糖", 2);
        AtomicStampedReference<Dog> atomicReference = new AtomicStampedReference<>(jinmao, version);
        System.out.println(" 1 >>>>>>>>> : "+atomicReference.getReference()+ "  ......  "+ version);
        jinmao.setAge(20);
        atomicReference.compareAndSet(jinmao, jinmao, version, version +1);
        Dog labuladuo = new Dog("小七", 3);
        boolean b = atomicReference.compareAndSet(jinmao, labuladuo, version, version + 1);
        System.out.println(" 2 >>>>>>>>> : "+b);
        System.out.println(" 3 >>>>>>>>> : "+atomicReference.getReference()+ "  ......  "+ version);
    }

输出

1 >>>>>>>>> : Test.Dog(name=糖糖, age=2) ...... 0
2 >>>>>>>>> : false
3 >>>>>>>>> : Test.Dog(name=糖糖, age=20) ...... 0

1 synchronized

被Synchronized修饰过的程序块,在编译前后被编译器生成了monitorenter和monitorexit两个字节码指令。

执行时如下流程

image.png

“锁”的本质其实是monitorenter和monitorexit字节码指令的一个Reference类型的参数,即要锁定和解锁的对象。

synchronized又分为是否明确的指定了锁对象,比如synchronized(this)或是synchronized直接修饰方法。

1.1 指定了锁对象

如下三种方式

    public final Object lock = new Object();
    int i;
    public void thisSync() { //第1种锁
        synchronized (this) {
            i++;
        }
    }
    
    public void objectSync() { //第2种锁
        synchronized (lock) {
            i++;
        }
    }
    
    public void classSync() { //第3种锁
        synchronized (Test.class) {
            i++;
        }
    }
  1. 前两种对于第三种的区别很好理解,1和2是对象锁:只锁当前对象,每个对象各子一把锁,不影响其他对象调用此方法中的代码块;3是类锁:对于该类的所有实体对象都产生影响,每个对象使用同一把锁。

  2. 1和2的区别又是什么呢?既然都是对象锁,那为什么要多此一举创建个对象出来呢?区别就在于锁是不同的。

        static
        class Printer {
            public void print(){
                synchronized (this){
                    System.out.println("打印机打印");
                }
            }
        }
    
        public static void main(String[] args) {
            Printer printer = new Printer();
            ExecutorService pool = Executors.newCachedThreadPool();
    
            pool.execute(()->{
                synchronized (printer){
                    while (true);
                }
            }); // 线程1
            pool.execute(printer::print); // 线程2
    
        }
    

    这段代码将不会有任何输出!这就是死锁。线程1一直在使用this锁对象,线程2获得不到锁一直处于阻塞状态。

    我们修改一下代码

        static
        class Printer {
            private final Object object = new Object();
            public void print(){
                synchronized (object){
                    System.out.println("打印机打印");
                }
            }
        }
    
        public static void main(String[] args) {
            Printer printer = new Printer();
            ExecutorService pool = Executors.newCachedThreadPool();
    
            pool.execute(()->{
                synchronized (printer){
                    while (true);
                }
            }); // 线程1
            pool.execute(printer::print); // 线程2
    
        }
    

    这时有输出了。区别在于,第一个示例中的锁是printer这个对象,两个线程使用的都是这个锁。而第二个示例线程1使用的锁是printer对象,线程2使用的锁是object对,两个线程两把锁,相互之间不会有任何影响。

1.2 修饰方法


    public synchronized static void staticMethod(){
        
    }    
    public synchronized void method(){
        
    }

这个很好理解,类锁和对象锁之间的区别。

注意,当一个对象被锁住时,对象里面所有用synchronized修饰的方法都将产生堵塞,而对象里非synchronized修饰的方法可正常被调用,不受锁影响。

2 ReentrantLock

2.1 为什么使用ReentrantLock

随着Jdk的发展,synchronize和ReentrantLock在性能上的几乎没有什么差别了,

先看例子

    static
    class Printer {
        private Integer counter = 0;
        private final ReentrantLock lock = new ReentrantLock();
        public void print(String threadName){
            System.out.println("线程:--->"+threadName+"等待获取锁");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            System.out.println("线程:--->"+threadName+"第一次加锁");
            counter++;
            System.out.println("线程:--->"+threadName+"做第"+counter+"件事");
            //重入该锁,我还有一件事情要做,没做完之前不能把锁资源让出去
            lock.lock();
            System.out.println("线程:--->"+threadName+"第二次加锁");
            counter++;
            System.out.println("线程:--->"+threadName+"做第"+counter+"件事");
            lock.unlock();
            System.out.println("线程:--->"+threadName+"释放一个锁");
            lock.unlock();
            System.out.println("线程:--->"+threadName+"释放一个锁");

        }
    }

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

推荐阅读更多精彩内容