JAVA多线程与高并发(二)[volatile,CAS(无锁优化,自旋锁,乐观锁-AtomicLong,LongAdder-分段锁)]

link-JAVA多线程与高并发系列[前言,大纲,目录]

volatile

首先,大佬(马老师)说,这个volatile在工程中能不用就不用,因为这玩意不好掌控,没有什么资料.

1. 保证线程可见性(synchronize也有这效果

设一个变量a,如果没有加volatile,多线程情况下,在线程t1修改了a的值后,另一个线程t2读到的仍然是旧值;如果加了volatile修饰,t2就可以马上读到t1修改后的值.
因为如下:(本质依靠的是MESI,CPU的缓存一致性协议)
首先变量a保存在heap堆内存中,堆内存是各线程共享内存;而且每个线程都有自己的专属工作内存.
当两个线程,t1和t2去访问共享内存的变量a时,他们会各自把a复制一份到自己的专属内存.
如果变量a没有加volatile,这时候如果线程t1修改了变量的值,(t1应该会把变动马上同步回共享内存),但是线程t2什么时候去共享内存再次读取同步变量a,不好控制,如果线程2没有去共享内存再次读取同步变量a,那么就看不见线程1的修改后的结果.
看一个保证线程可见性的例子:
在一秒后,main线程修改了变量running的值,
没有加volatile时,程序会过很久才打印"m end!";
加了volatile后,会在一秒后马上打印"m end!"

import java.util.concurrent.TimeUnit;

public class T01_HelloVolatile {
    // 对比一下有无volatile的运行结果
	/*volatile*/ boolean running = true;
	void m() {
		System.out.println("m start");
		while(running) {
		}
		System.out.println("m end!");
	}
	
	public static void main(String[] args) {
		T01_HelloVolatile t = new T01_HelloVolatile();
		
		new Thread(t::m, "t1").start();

		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		t.running = false;
	}
	
}

2. 禁止指令重排序(也和CPU有关)(synchronize无此效果)

(其底层原理是加了读屏障loadfence和写屏障storefence原语指令)
以前的CPU是"串联"执行指令;现代CPU为了提高效率,当第一个指令执行到中间时,就开始执行第二个指令.(原来像是平铺的水泥板,现在像是楼梯).
为了利用CPU的这种高效架构,编译器(compiler)把源码编译时,可能会将指令重新排序,据说这样会把速度提高很多.
指令重排序可能带来问题的场景举例:DCL单例模式

DCL双重检查锁实现的单例模式(静态实例变量加volatile)

new一个对象的时候,分为三步:

  1. 给这个对象申请内存,给成员变量赋默认值(比如int的默认值为0);
  2. 给这个对象的成员变量初始化(比如我们写的int a=8)
  3. 把申请的内存赋值给对象的"引用"
    正常情况下是1,2,3顺序执行;如果发生指令重排序的话,会1,3,2这样顺序执行.
    重排序后,在特别大的并发量下,可能会在第1,3步后,还没有执行第2步前,这时该引用已经!=null了,但是成员变量还没有初始化,这个时候对象被其他线程使用就会出问题.

DCL单例模式举例:(这个属于懒汉模式,一般用不到;直接用恶汉单例就行啦,没必要用懒汉)

public class Manager {
    private static volatile Manager INSTANCE = null;

	// 私有构造方法
    private Manager() {

    }

    public static Manager getInstance() {
        if (INSTANCE == null) {
            synchronized (Manager.class) {
                if (INSTANCE == null) {
                    // 初始化INSTANCE,加载所需资源等
                    INSTANCE = new Manager();
                }
            }
        }
        return INSTANCE;
    }
}

3. 不能保证原子性

做个测试证明一下,如果volatile可以保证原子性,那么下面这段代码应该输出100000;反之则证明volatile不能保证原子性.
如果想保证原子性,可以在方法m()上加个synchronize,或者把count++这段代码用synchronize包起来.

public class T04_VolatileNotSync {
	volatile int count = 0;
	void m() {
		for(int i=0; i<10000; i++) {
		  count++;
		  /*
		  synchronize(this){
		    count++;
		  }
		  */
		}
	}
	
	public static void main(String[] args) {
		T04_VolatileNotSync t = new T04_VolatileNotSync();
		List<Thread> threads = new ArrayList<Thread>();
		for(int i=0; i<10; i++) {
			threads.add(new Thread(t::m, "thread-"+i));
		}
		threads.forEach((o)->o.start());
		threads.forEach((o)->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		System.out.println(t.count);
	}
}

4.volatile修饰引用类型变量的可见性?

有些文章会写到,volatile如果修饰引用类型变量,那么"引用"的地址的改变对其他线程是可见的,但是引用的对象的属性变化对其他线程不可见.

如果你写一个例子,经过一些尝试,发现普通对象的属性的改变,volatile能保证其变化是可见的.但是!!!
但是!!!大量的测试后,我发现,不是每次都可见,特别是对象的属性变化很多次的时候!
所以,结论是,volatile的确不能保证变量指向的对象的属性的可见性.
又但是!如果修改对象属性的线程,sleep了一下,哪怕一纳秒,那么就能保证可见了,真是奇怪,回头有时间研究下JVM没准能搞明白.(也有可能是sleep的情况下,测试次数不够多)

下面是测试的例子,可以看出,volatile修饰的引用类型变量,如果修改的线程(生产者)没有sleep,其他线程不是每次都可见.

/**
 * @author liweizhi
 * @date 2020/3/4 18:18
 */
public class VolatileObject {
    volatile static Pet pet = new Pet("dahuang", 1);

    public static void main(String[] args) {
        // 多运行几次无论是ageChange还是nameChange,会发现有时候t1不会结束(如果t2不sleep)
        nameChange();
//        ageChange();
    }

    private static void ageChange() {
        new Thread(() -> {
            System.out.println("t1 start "/* + Instant.now()*/);
            while (true) {
                if (pet.getAge() == 5) {
                    break;
                }
            }
            System.out.println("t1 end "/* + Instant.now()*/);
        }, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            Pet myPet = pet;
            for (int i = 1; i <= 100; i++) {
                int age = myPet.getAge();
                myPet.setAge(++age);
                /*try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
            }
            System.out.println("t2 end "/* + Instant.now()*/);
        }, "t2").start();
    }

    private static void nameChange() {
        new Thread(() -> {
            System.out.println("t1 start "/* + Instant.now()*/);
            while (true) {
                if ("xiaobai8".equals(pet.getName())) {
                    break;
                }
            }
            System.out.println("t1 end "/* + Instant.now()*/);
        }, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            Pet myPet = pet;
            for (int i = 1; i <= 10; i++) {
                myPet.setName("xiaobai" + i);
                /*try {
                    TimeUnit.NANOSECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
            }
            System.out.println("t2 end "/* + Instant.now()*/);
        }, "t2").start();
    }

    static class Pet {
        String name;

        int age;

        public Pet(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
}

CAS(也有人称作无锁优化,自旋锁,乐观锁)

首先要明确一点,自旋而"无锁"并不一定就比有锁快,因为自旋是在占用CPU,如果很多个线程一起自旋很久,想想都觉得效率很低…具体情况还是要具体分析.

概念 Compare And Swap/Set

下面是一段说明CAS原理的伪代码:

/**
 * nowValue:当前值,这个值是随时可能被修改的
 * expectedValue:期望值,在调用casMethod前获取到的nowValue
 * newValue:想要修改的新的值
 */
casMethod(nowValue, expectedValue, newValue){
  // 如果当前值和期望值相等,说明本次修改期间没有其他线程修改,则赋值
  if(nowValue == expectedValue) {
    // 问题:此时,已经判定为其他线程没有修改,那么在赋值前会不会被其他线程修改了?
    // 答:不会,cas操作是CPU原语支持,是CPU指令指令级别上的支持,中间不能被打断
    nowValue = newValue;
  } else {
    // 如果当前值和期望值不等,说明本次修改期间有其他线程已经修改了值,
    // 那么就再试一次或者直接返回修改失败的结果
    // todo try again or return fail
}

应用:AtomicInteger等atomic类

简单使用

下面这段代码,用了AtomicInteger后,自增部分(count++)不需要加同步锁,输出结果为100000

public class T01_AtomicInteger {
	/*volatile*/ //int count1 = 0;
	
	AtomicInteger count = new AtomicInteger(0); 

	/*synchronized*/ void m() { 
		for (int i = 0; i < 10000; i++){
			//if count1.get() < 1000
			count.incrementAndGet(); //count1++
		}
	}

	public static void main(String[] args) {
		T01_AtomicInteger t = new T01_AtomicInteger();
		List<Thread> threads = new ArrayList<Thread>();
		for (int i = 0; i < 10; i++) {
			threads.add(new Thread(t::m, "thread-" + i));
		}
		threads.forEach((o) -> o.start());
		threads.forEach((o) -> {
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		System.out.println(t.count);
	}
}

简单的分析一波incrementAndGet()

跟踪下去,count.incrementAndGet():

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

继续跟踪,到了Unsafe.class,这里compareAndSwapInt就是用到了CAS:

	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;
    }

Unsafe.class比较复杂,直接操作JVM中的内存,类似C和C++的操作,比如分配一个对象不用new,而是直接写在内存中;操作对象的属性也可以根据"地址"(or指针)和偏移量定位.

ABA问题

什么是ABA?

在开始使用CAS修改一个值后,在CAS中判断nowValue == expectedValue前,假设有一个线程先把nowValue改成了其他值x,然后又把x改回了nowValue,那么这时候虽然nowValue等于expectedValue,但这个值其实已经被修改过了.

ABA会带来什么问题?

如果是AtomicInteger等数值类型,其实ABA是没有影响的,无所谓.
如果是一个对象引用,多数情况是不允许ABA情况的(比如小黄和其女朋友小白要结婚了,但是突然来了小黑抢走了小白,他们成为恋人,过了段时间又把小白还了回来,这婚多半就结不成了).

怎么避免ABA?

加上版本号version,做任何操作时都把version+1,同时比较nowValue和version
假设nowValue值为A,version为1,如果有ABA情况发生,即nowValue值变为B后又变回A,那么此时version是3,就可以根据version知道值已经被修改过了.
例如java.util.concurrent.atomic.AtomicStampedReference

Atomic的常见问题

高并发实现递增的三种方式?

  1. 同步static long count2 = 0L;
    synchronized (lock) {
    count2++;
    }
  2. CAS原子操作AtomicLong count1 = new AtomicLong(0L);
    count1.incrementAndGet();
  3. 分段锁LongAdder count3 = new LongAdder();
    count3.increment();

下面代码是一个简单的测试(1000个线程,累加10W次),从中可以LongAdder最快,AtomicLong次之,synchronize最慢.
synchronize慢是因为会升级成重量级锁,向OS申请资源加锁.
注意:如果线程数较少,或者累加次数较少,LongAdder比AtomicLong慢.所以实际项目中,还是要看项目中的并发度如何.

public class T02_AtomicVsSyncVsLongAdder {
    static long count2 = 0L;
    static AtomicLong count1 = new AtomicLong(0L);
    static LongAdder count3 = new LongAdder();

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[1000];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int k = 0; k < 100000; k++) {
                    count1.incrementAndGet();
                }
            });
        }
        long start = System.currentTimeMillis();
        for (Thread t : threads) {
            t.start();
        }
        for (Thread t : threads) {
            t.join();
        }
        long end = System.currentTimeMillis();
        //TimeUnit.SECONDS.sleep(10);
        System.out.println("Atomic: " + count1.get() + " time " + (end - start));
        //-----------------------------------------------------------
        Object lock = new Object();
        for (int i = 0; i < threads.length; i++) {
            threads[i] =
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            for (int k = 0; k < 100000; k++) {
                                synchronized (lock) {
                                    count2++;
                                }
                            }
                        }
                    });
        }

        start = System.currentTimeMillis();
        for (Thread t : threads) {
            t.start();
        }
        for (Thread t : threads) {
            t.join();
        }
        end = System.currentTimeMillis();

        System.out.println("Sync: " + count2 + " time " + (end - start));

        //----------------------------------
        for (int i = 0; i < threads.length; i++) {
            threads[i] =
                    new Thread(() -> {
                        for (int k = 0; k < 100000; k++) {
                            count3.increment();
                        }
                    });
        }
        start = System.currentTimeMillis();
        for (Thread t : threads) {
            t.start();
        }
        for (Thread t : threads) {
            t.join();
        }
        end = System.currentTimeMillis();
        //TimeUnit.SECONDS.sleep(10);
        System.out.println("LongAdder: " + count1.longValue() + " time " + (end - start));
    }
    
}

我本地环境输出:

Atomic: 100000000 time 1665
Sync: 100000000 time 3930
LongAdder: 100000000 time 406

为什么高并发下,LongAdder比AtomicLong快?

LongAdder内部实现类似"分段锁"(分段锁也是CAS操作),把值放在数组里,每个元素作为一个相对独立的部分,分散开线程的压力,最后再汇总起来.(有点类似于MapReduce思想)

你可能感兴趣的:(JAVA多线程与高并发)