JAVA 并发与高并发知识笔记(二)

一、并发安全、不安全描述

安全:多个线程操作同一个资源,最后的执行结果与单线程执行结果一致,则说明是线程安全的

不安全:多个线程操作同一个资源,最后执行结果不确定的,则说明不是线程安全的

这里我觉得还是解释一下并发与并行的一点区别比好(并非绝对概念),并发通常是多个线程去竞争相同资源,而并行通常是多个线程之间是协作关系,例如,在秒杀场景下,多个用户(线程)共同争抢某个资源,这个是并发。例如,多个线程统计一个几千万数据的文件,这个时候线程之间是协作关系,每个线程各自统计分配的一段数据,最后汇总给主线程。

二、常见并发模拟工具以及代码模拟并发(工具使用之后再单独学习)

a) Postman :http 请求模拟工具

b) AB (Apache Bench) :Apache 附带的模拟工具,主要用来测试网站性能

c) JMeter : Apache 组织开发的压力测试工具

d) 使用 CountDownLatch、Semaphore 进行并发模拟 (在笔记一中已经提到)

三、代码模拟并发

a) CountDownLatch 介绍:该类为一个计数器,字面意思就是向下减的一个闭锁类


上图解释:

     TA 为主线程,主线程初始化 CountDownLatch 计数器为3,T1~3 为子线程,主线程调用CountDownLatch的 await 后就开始阻塞,直到T1~3 调用 CountDownLatch 的 countDown() 方法将计数器减为0,然后主线程继续运行。

b) Semaphore 介绍:

     字面意思是信号量,它可以控制同一时间内有多少个线程可以执行,比如我们常见的马路,有4车道,8车道,可以把这里的车道比作线程,4车道相当于4个线程同时执行,8车道想相当于8个线程执行。semaphore 就是好比是这个车道,可以指定有多个车道,从对信号量的功能描述,可以想到在实际开发中可以用来限制同一时间请求接口的次数,通常semaphore 会与线程池配合使用。

c) 并发模拟代码(Not thread safe)

/**
 * 并发模拟
 * 
 * @author Aaron
 *
 */
@NotThreadSafe
@Slf4j
public class ConcurrencyTest1 {

	// 模拟 1000个用户请求
	private final static int TotalClient = 1000;

	// 限制同一时间只能有10个线程执行
	private final static int TotalThread = 10;
	// 计数器
	private static int count = 0;

	public static void main(String[] args) throws InterruptedException {
		ExecutorService es = Executors.newCachedThreadPool();
		// 设置信号量,允许同时最多执行的线程数
		final Semaphore sp = new Semaphore(TotalThread);
		final CountDownLatch cdl = new CountDownLatch(TotalClient);
		for (int i = 0; i < TotalClient; i++) {
			es.execute(new Runnable() {
				@Override
				public void run() {
					try {
						sp.acquire();
						add();
						sp.release();
					} catch (InterruptedException e) {
						log.error("A", e);
					}
					cdl.countDown();
				}
			});
		}
		// 中断主线层代码,直至countdownlatch 的计数器变为0
		cdl.await();
		es.shutdown();
		log.info(String.valueOf(count));
	}

	@NotThreadSafe
	public static void add() {
		count++;
	}

}
d ) 并发模拟daim(Thread safe)
/**
 * 并发模拟
 * 
 * @author Aaron
 *
 */
@ThreadSafe
@Recommend
@Slf4j
public class ConcurrencyTest2 {

	// 模拟 1000个用户请求
	private final static int TotalClient = 1000;

	// 限制同一时间只能有10个线程执行
	private final static int TotalThread = 10;

	// 使用原子类
	private final static AtomicInteger count = new AtomicInteger(0);

	public static void main(String[] args) throws InterruptedException {
		ExecutorService es = Executors.newCachedThreadPool();
		// 设置信号量,允许同时最多执行的线程数
		final Semaphore sp = new Semaphore(TotalThread);
		final CountDownLatch cdl = new CountDownLatch(TotalClient);
		for (int i = 0; i < TotalClient; i++) {
			es.execute(new Runnable() {
				@Override
				public void run() {
					try {
						sp.acquire();
						add();
						sp.release();
					} catch (InterruptedException e) {
						log.error("A", e);
					}
					cdl.countDown();
				}
			});
		}
		// 中断主线层代码,直至countdownlatch 的计数器变为0
		cdl.await();
		es.shutdown();
		log.info(String.valueOf(count.get()));
	}

	@ThreadSafe
	public static void add() {
		count.incrementAndGet();
	}

}

四、类的线程安全性定义

      当多个线程同时访问一个类时,不管运行时环境采用何种方式调用或者这些线程如何交替执行, 并且在主调代码中不需要做额外的同步或协同操作,这个类始终表现出正确的行为,那么这个类就是线程安全的。

五、线程安全的主要体现点

a) 原子性:

    原子性可以解释为互斥性访问,既同一时刻,只能有一个线程进行操作

b) 可见性:

   某个线程对主内存的修改,其它线程必须能及时观察到

c) 有序性:

   某个线程观察其它线程中的指令执行顺序,由于指令重排序的存在,通常观察到的是杂乱无序的

六、CAS 原理 (以 AtomicInteger 为参考)

由于学习发现我的 eclipse 看不到 unsafe 源码,其它源码可以看到,所以特意安装了反编译插件(Decompiler

地址:https://www.cnblogs.com/godtrue/p/5499785.html

a ) CAS 是 unsafe 中的 compareAndSwapInt 方法的缩写

b) 原理,在 AtomicInteger 的 incrementAndGet 方法里调用了 unsafe 中的 getAndAddInt 方法,在该方法中,核心的方法是 compareAdnSwapInt 方法,核心原理是通过对象以及值的内存地址取出当前值,然后再进行比较,如果比较是值发生了更改则重新取出最新的值再继续比较,直到比较成功,然后更新值。

// 标记为 native 的方法,说明不是用java实现的,通常是由C、C++ 等等实现的
// 第一个参数是当前对象,第二个参数是值所对应的内存地址
public native int getIntVolatile(Object arg0, long arg1);

// 也是标记为 native 的方法,也是核心方法
// 第一个参数是当前对象,第二个参数是值所对应的内存地址,第三个参数为内存值,第四个参数为准备更新的值
public final native boolean compareAndSwapInt(Object arg0, long arg1, int arg3, int arg4);

// AtomicInteger 中调用的是该方法
// 第一个参数是当前对象,第二个参数是值的内存地址,第三个参数为增加量
public final int getAndAddInt(Object arg0, long arg1, int arg3) {
	int arg4;
	do {
            // 取出当前内存值(预期值)
	     arg4 = this.getIntVolatile(arg0, arg1);
            // 比较当前内存值是否与预期值相等,如果不相等则继续比较,如果相等则返回当前的内存值
            // 如果预期值 与 arg1 指向的值一样,则更新为 arg4 + arg3 
            // 如果不一样则继续循环,直到完成更新
	} while (!this.compareAndSwapInt(arg0, arg1, arg4, arg4 + arg3));

	return arg4;
}

c) CAS 缺点

    分析CAS源码后可以发现,如果大量线程进行CAS操作,那么竞争就会很激烈,导致一部分线程由于总是比较失败而长时间停留在循环体中,可能会有瞬间或一段时间的CPU过载,影响系统性能。

d) (JDK1.8 新增 ) LongAdder 与 DoubleAdder 处理思想

     对于普通类型的 long 或 double 类型的变量,JVM 允许将64位的读操作或写操作拆分成两个32位的读写操作,该处理方式的主要思想是将热点数据分离,将内部 Value 分离成一个Cell 数组,当多个线程访问时通过HASH等算法将线程映射到其中一个Cell 上进行操作,最终的计算结果则是Cell 数组的求和值,当低并发的时候,算法会直接更新变量的值,在高并发的时候通过分散操作Cell 提高性能,当然缺点也是有的,当并发更改以及调用sum操作时,sum统计的值可能不准确,以下是原话。

    /**
     * Returns the current sum.  The returned value is <em>NOT</em> an
     * atomic snapshot; invocation in the absence of concurrent (意思是在非并发的情况下使用)
     * updates returns an accurate result, but concurrent updates that
     * occur while the sum is being calculated might not be
     * incorporated.
     *
     * @return the sum
     */
    public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

七、java.util.concurrent.atomic 包



八、AtomicReference 与 AtomicIntegerFieldUpdater

a) AtomicReference 基本使用

该类提供一个泛型参数,用于对多种对象的原子操作(注意是对象的操作,如果传入原子类,则是对这个原子类本身的原子操作,并非是原子类中数据的原子操作),以下为简单的示例

@ThreadSafe
@Slf4j
public class AtomicReferenceTest {

	private static AtomicReference<Integer> ar = new AtomicReference<Integer>(0);

	public static void main(String[] args) {
		// 比较更新方法,如果是值是0,则更新为1
		log.info("{} -> {} - {}", 0, 1, ar.compareAndSet(0, 1));
		// 获取原先的值,并设置为指定的新值
		log.info("{} -> {}", ar.getAndSet(3), ar.get());
		
		// 以下是源码实现,核心还是使用的Unsafe类的方法
		// /**
		// * Atomically sets the value to the given updated value
		// * if the current value {@code ==} the expected value.
		// * @param expect the expected value
		// * @param update the new value
		// * @return {@code true} if successful. False return indicates that
		// * the actual value was not equal to the expected value.
		// */
		// public final boolean compareAndSet(V expect, V update) {
		// native 原子方法
		// return unsafe.compareAndSwapObject(this, valueOffset, expect,
		// update);
		// }
	}
}

b) AtomicIntegerFieldUpdater
基本使用

该类提供一个泛型参数,用于对对象内部的成员变量进行原子操作,以下为简单的示例

@Slf4j
public class AtomicIntegerFieldUpdaterTest {
	private static AtomicIntegerFieldUpdater<AtomicIntegerFieldUpdaterTest> a = AtomicIntegerFieldUpdater
			.newUpdater(AtomicIntegerFieldUpdaterTest.class, "value");
	// 变量必须是 int 基本类型,不能是对象类型
	// 变量必须有 volatile 关键字修饰
	// 以下是源码中的判断
	// if (field.getType() != int.class)
	// throw new IllegalArgumentException("Must be integer type");
	//
	// if (!Modifier.isVolatile(modifiers))
	// throw new IllegalArgumentException("Must be volatile type");
	@Getter
	// 未初始化默认是0
	private volatile int value;

	public static void main(String[] args) {
         AtomicIntegerFieldUpdaterTest aifu = new AtomicIntegerFieldUpdaterTest();
          // 比较&设置
	 a.compareAndSet(aifu, 0, 2);
         log.info("{}", aifu.getValue());
}
}九、解决 CAS 的ABA问题

a) ABA问题解释:

     当多个线程操作一个资源时,T1取出值A,T2线程也取出值A,这个时候T2执行过程中将A变为B又变回A,然后T1继续执行发现与自己的值相同,然后进行了更新操作,操作虽然成功,但是这个过程却是有隐患的,比如对一个单项链表操作,T1 取出栈顶A与下一个栈B,想用CAS替换栈顶A为B,在T1执行CAS操作之前,这时候T2取出A和B,然后push了A、C、D,这个时候B属于独立的链表,处于游离状态(当前有两个链表 A->C->D->NULL ,还有一个游离的 B->NULL),然后T1开始执行CAS操作,发现ACD栈顶还是A,然后开始处理,由于之前已经取出B,当时的B->NULL 这样的,最后结果是把T2的 CD 给丢了。

b) 为了解决ABA问题,有了 AtomicStampedReference 这个类

    该类的核心思想是,每次操作都有个一 version 来记录,例如 T2 取出A时版本是 1,更新为B后版本号变为2,再更新为A时版本号变为3,此时由于有版本号控制,T1 再来更新A时发现自己的版本号1 与 3 不一致,最后CAS操作失败。

示例:

@Slf4j
public class ABATest {
	// 普通原子类
	private static AtomicInteger atomicInt = new AtomicInteger(100);
	// 有版本号的实现(参数是初始值与初始版本号)
	private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);

	public static void main(String[] args) throws InterruptedException {
		// 模拟 B->A
		Thread intT1 = new Thread(new Runnable() {
			@Override
			public void run() {
				// A->B
				atomicInt.compareAndSet(100, 101);
				// B->A
				atomicInt.compareAndSet(101, 100);
			}
		});
		// 模拟 A->B
		Thread intT2 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					// 线程休眠,给 T1 执行
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// A->B
				boolean c3 = atomicInt.compareAndSet(100, 101);
				log.info("一般 CAS={}", c3);// 操作成功
			}
		});

		intT1.start();
		intT2.start();
		intT1.join();
		intT2.join();

		Thread refT1 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// A-B 版本号 1
				atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
				// B-A 版本号 2
				atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
			}
		});

		Thread refT2 = new Thread(new Runnable() {
			@Override
			public void run() {
				// T2取出版本号
				int stamp = atomicStampedRef.getStamp();
				log.info("有版本号,线程休眠之前:stamp={}", stamp);
				try {
					// 休眠2秒
					TimeUnit.SECONDS.sleep(2);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// T1 的版本号
				log.info("有版本号,线程休眠之后:stamp={}", atomicStampedRef.getStamp());
				// A->B ,T1 将版本号增加到了2,然后执行时由于T2
				// 持有的版本号还是之前的0,与当前的版本号2不一致,最中CAS操作失败
				boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
				log.info("有版本号 CAS={}", c3);
			}
		});

		refT1.start();
		refT2.start();
	}

十、 AtomicBoolean 示例

可以使用该类来保证某项操作只执行一次

@Slf4j
public class AtomicBooleanTest {

	private static AtomicBoolean ab = new AtomicBoolean(true);

	public static void main(String[] args) throws InterruptedException {
		ExecutorService es = Executors.newCachedThreadPool();
		int c = 1000;
		int s = 100;
		final Semaphore sh = new Semaphore(s);
		final CountDownLatch cdl = new CountDownLatch(c);
		for (int i = 0; i < c; i++) {
			es.execute(new Runnable() {
				@Override
				public void run() {
					try {
						sh.acquire();
						init();
						sh.release();
					} catch (InterruptedException e) {
						log.error("Error", e);
					}

					cdl.countDown();
				}
			});
		}

		cdl.await();
		es.shutdown();
	}

	private static void init() {
		// 个人理解这么写会有效率问题,因为每次都要进行CAS比较,应该加一层 if 判断,如 init2
		if (ab.compareAndSet(true, false)) {
			log.info(".......init.....OK");
		}
	}

	private static void init2() {
		if (ab.get()) {
			if (ab.compareAndSet(true, false)) {
				log.info(".......init.....OK");
			}
		}
	}
}



猜你喜欢

转载自blog.csdn.net/keplerpig/article/details/79885760