一、并发编程与高并发
并发编程与高并发是两个不同的概念,并发编程是相对于以往单线程的编程模型,而采用多线程的编程模型,以充分利用CPU资源。高并发是一类场景,应用的并发请求量非常大的情景,为了解决高并发场景的问题,有很多技术手段,其中多线程并发编程模型是一种技术手段,还有包括扩容、缓存、队列、拆分、服务降级与熔断、数据库分库分表等等。
并发的概念:同时拥有两个或更多的线程,如果是单核CPU,则这多个线程交替获取CPU资源,轮流执行,同一时刻只有一个处于执行状态,其它线程处于其它状态(JVM模型中,线程都有哪些状态?初始状态、就绪状态、运行中状态、阻塞、等待、超时等待、终止);如果是多核CPU,则核心数个线程将同时获取CPU资源,同时执行,多余的线程仍然需要和这些执行中的线程交替获取CPU资源。
谈到并发,主要是考虑多个线程操作相同的资源,保证线程安全,合理调度和使用共享资源,保证正确性。
谈到高并发,主要是考虑能够通过各种技术使得服务器能够同时处理很多请求,提高系统性能。
二、并发编程知识点
线程安全
线程封闭
线程调度
同步容器/并发容器
AQS
J.U.C
三、多线程实现计数器累加
要保证多线程计数器累加的线程安全,关键是++操作的原子性及操作变量的数值可见性,一般情况下,维护一个变量的线程安全,可以考虑Atomic类,该类保证单调用其方法是原子操作,且保证可见性(其内部保存数据的变量由volatile修饰)
加锁也可以实现但是太重,性能不好
public class Counter {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
try {
for (int i = 0; i < 100; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
int number = Counter.count.incrementAndGet();
System.out.println(Thread.currentThread().getId() + ", " + number);
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
private static AtomicInteger count = new AtomicInteger(0);
}
四、CPU缓存
为什么要有CPU缓存?
原本CPU是直接和主内存交互,但是随着CPU频率的不断提高,其与主内存的速度差异越来越大,CPU需要等内存读取,这就浪费了CPU资源。为了缓解二者之间的速度差异,加入了高速缓存(小而快)。
而高速缓存中毕竟只能保存一小部分数据,那么为什么缓存的内容还是有效的呢?
根据局部性原理:时间局部性和空间局部性,目前读取的数据往往在将来还会使用;使用某个位置的数据往往会使用其周围的数据。因此缓存内容是有效的,其内容不会总是失效。
而随着发展,缓存出现多级缓存,为了实现各个CPU缓存之间数据的一致性,引入了MESI协议,缓存中的内容状态有四种,并在这四种之间不断转换,通过该协议保证各个缓存中的数据是一致的。
- Java内存模型(JMM)
Java内存模型规范规定了,一个线程如何以及何时能够看到其它线程修改过的共享变量的值,以及在必要时如何同步地访问共享变量。
Java内存模型划分为主内存(存储共享变量值,各个线程共享),工作内存(每个线程独有,将主内存中的值读取副本到工作内存,然后进行操作,操作后再写回主内存)
由于各个线程都有自己的工作内存,因此在从主内存读取值后进行操作,可能会出现各个线程操作的结果错乱的问题。为了保证数据的安全,规定了八种同步操作:
Lock、unlock
Read、load、use
Assign、store、write
而且针对上述八种操作,有一些规定,比如:
Lock只能作用于主内存,只能有一个线程对该变量进行lock,多次lock必须对应多次unlock才释放该变量
Read必须在load之前,read和load必须成对出现,write和store与之类似
Assign到工作内存的值必须在某一时刻通过store和write到主内存,不能随意丢弃
等等很多的规定
五、线程安全性
线程安全性最核心的定义就是正确性。正确性的含义是某个类的行为与其设计完全一致。因此线程安全性的定义是:当多个线程并发访问某个类时,不论运行时环境如何调度或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步措施,这个类始终都能够表现出正确的行为,那么就称这个类是线程安全的。
线程安全性体现在三点:
原子性:提供了互斥访问,一段时间内只能有一个线程来对某个共享变量进行操作
可见性:一个线程对主内存内共享变量的修改能够及时被其它线程看到
有序性:一个线程观察其它线程中指令的执行顺序,由于指令的重排序,其观察结果往往是杂乱无序的
原子性:
总结:需要学习的有Atomic包、CAS算法、synchronized关键字、Lock包
Atomic包中有以下的类:
重点学习以下几个:
AtomicInteger类
AtomicBoolean类
AtomicLong类与LongAdder类的区别
AtomicLongArray类
AtomicReference类、AtomicReferenceFieldUpdater类
AtomicStampReference类:解决CAS的ABA问题
- AtomicInteger类
示例代码:
public static AtomicInteger count = new AtomicInteger(0);
private static void add(){
count.incrementAndGet();//变量操作
}
示例中,对count变量的+1操作,采用的是incrementAndGet方法,此方法的源码中调用了一个名为unsafe.getAndAddInt的方法。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
而Unsafe类中的getAndAddInt方法的具体实现为:
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;
}
在此方法中,方法参数为要操作的对象Object var1、底层当前的数值为var2、要增加的值为var4。定义的var5为从底层取出来的值。采用do..while循环的方式去比较底层当前值与之前获取的是否一致,比较为一致才将值进行修改,否则认为CAS操作失败进行重试。而这个比较再进行修改的方法就是compareAndSwapInt就是我们所说的CAS操作,它是一系列位于Unsafe中的native修饰方法,由底层实现。CAS取的是compareAndSwap三个单词的首字母.
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
另外,示例代码中的count位于方法中的操作时使用的,可以理解为JMM中的工作内存,而这里的底层数值即为主内存。
- AtomicBoolean类
与AtomicInteger和AtomicLong类似,只不过维护一个布尔类型的变量,而且通过其方法compareAndSet可以实现控制一段代码只让一个线程执行,AtomicBoolean源码如下,只有AtomicBoolean维护的值等于expect时才会修改为update
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
通过上述方法实现一段代码只有一个线程能执行:
//是否发生过
private static AtomicBoolean isHappened = new AtomicBoolean(false);
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
//通过信号量控制同时并发的线程数最大为threadTotal
final Semaphore semaphore = new Semaphore(threadTotal);
//通过向下计数闭锁来控制一共有clientTotal个线程执行
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
test();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
//等待闭锁值减到0
countDownLatch.await();
executorService.shutdown();
log.info("isHappened:{}", isHappened.get());
}
private static void test() {
//控制某有一段代码只执行一次
//只有一个线程能将false置为true,其它线程均发现为true无法赋值成功
if (isHappened.compareAndSet(false, true)) {
log.info("execute");
}
}
结果是log只会打印一次,即只有一个线程能够执行test方法中if块中的代码
[main] INFO com.superboys.concurrency.example.Atomic.AtomicExample6 - isHappened:true
- AtomicLong类与LongAdder类
先来看一看这两个类的操作
AtomicLong:
//变量声明
public static AtomicLong count = new AtomicLong(0);
//变量操作
count.incrementAndGet();
//变量取值
count.get();
LongAdder:
//变量声明
public static LongAdder count = new LongAdder();
//变量操作
count.increment();
//变量取值
count
AtomicLong与AtomicInteger类似,只是表示Long类型的值,那为何还要有LongAdder类呢?
通过对AtomicInteger的分析我们得知,其内部通过Unsafe的方法来进行线程安全的写入操作,而Unsafe中通过不断CAS尝试直到写入成功,如果并发量比较大,则很多的线程需要进行很多次的CAS尝试,这样效率比较低。(对于基本类型long、double,jvm允许将64位的读写操作变成两个32位的读写操作来完成)
LongAdder类为了保证在并发比较大的情况下有比较好的性能,内部通过cell数组来保存一个long数值,即将long数值拆分成很多个值分别保存在数组中的每一个位置上,对该数值的加减操作只需要对数组中的一个值进行加减即可,而如果需要获得该long值则需要将数组中的所有值进行求和。该数组默认初始化长度为2。每个线程访问该值时,通过hash算法映射到其中一个数字进行操作。这其实采用了分段锁的思想,提高了并发度,减少冲突的发生。获取其值的方法源码如下
public String toString() {
return Long.toString(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;
}
从源码中可以看出,求数组的和时并没有任何加锁操作,直接将数组中的值相加求和,如果此时有并发的修改操作,将直接读取到修改的结果。实际中在处理并发度高的时候用LongAdder,而并发度低的时候用实现简单的AtomicLong效率更高。
- AtomicLongArray类
这个类实际上和AtomicLong区别不大,只是内部维护了一个Long值的数组,而不是一个Long值
- AtomicReference类、AtomicReferenceFieldUpdater类
AtomicReference类与AtomicInteger、~Long、~Boolean类似,只不过其内部维护一个通过泛型指定的引用类型,比如Integer等等,而AtomicReferenceFieldUpdater可以将一个用户自定义的类中的一个字段封装为原子,并且这个字段必须用volatile修饰、且不能static修饰,而且需要添加get方法
@Slf4j
public class AtomicExample5 {
//原子性更新某一个类的一个实例
private static AtomicIntegerFieldUpdater<AtomicExample5> updater
= AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");
@Getter
public volatile int count = 100;//必须要volatile标记,且不能是static
public static void main(String[] args) {
AtomicExample5 example5 = new AtomicExample5();
if(updater.compareAndSet(example5,100,120)){
log.info("update success 1,{}",example5.getCount());
}
if(updater.compareAndSet(example5,100,120)){
log.info("update success 2,{}",example5.getCount());
}else{
log.info("update failed,{}",example5.getCount());
}
}
}
此方法输出为success 1
[main] INFO com.superboys.concurrency.example.Atomic.AtomicExample5 - update success 1,120
[main] INFO com.superboys.concurrency.example.Atomic.AtomicExample5 - update failed,120
- AtomicStampReference类
以上介绍的Atomic类都是通过CAS操作来保证线程安全,但是这些类都没有解决CAS操作的ABA问题,而AtomicStampReference解决了ABA问题,其解决方法是内部维护一个版本号,只要是修改操作,就将版本号增加,这样当出现ABA问题时,线程就能通过版本号发现,从而放弃此次修改操作,也就防止了ABA问题造成影响。
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
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)));
通过上面的源码可以看出,通过stamp来保存版本号,观察compareAndSet方法可以看到其会对期望值和期望版本号进行比较,不同则会返回(&&是短路与),而且如果新值和新版本号均与旧值旧版本号相等,则直接返回(||也是短路操作),否则通过CAS操作casPair对值和版本号进行赋值。
Synchronized关键字是通过JVM层面的锁实现互斥操作,不可以中断的锁,适合竞争不激烈,可读性好;
Lock是类库层面的锁实现互斥操作,可中断,功能多,多样化同步,竞争激烈时优于synchronized;
AtomicXxx类也是类库层面实现的互斥操作,竞争激烈时效果优于Lock,但是功能单一,只能维护单个值
synchronized关键字:
修饰代码块,可以指定一个实例对象作为锁,也可以指定一个类(将该类对应的类对象作为锁)作为锁
修饰普通方法,默认调用该方法的实例作为锁,子类继承父类中的方法,方法的synchronized关键字不会继承到
修饰静态方法,默认该方法所在类对应的类对象作为锁
可见性:
总结:synchronized、volatile
导致共享变量在多个线程之间不可见的原因:
- 线程交替执行
- 指令重排序结合线程交替执行
- 线程对共享变量的修改没有及时从工作内存更新到主内存
Synchronized保证可见性:
JMM对于synchronized有两条规定,加锁时会将线程的工作内存中保存的该共享变量清空,从主内存读取一次,从而得到最新的值;解锁时必须将该线程工作内存中的值更新到主内存
Volatile保证可见性:
通过加入内存屏障来禁止指令重排序,以及强制规定线程对volatile变量的读必须从主内存读取(防止工作内存与主内存内容不一致)、对volatile变量的写必须直接将工作内存的值更新回主内存(防止工作内存的值没有及时更新回去,导致内容不一致)来实现。
由以上及java内存模型可以看出,要保证可见性其实很简单,只需要保证工作内存的读写与主内存读写绑定即可,即要读工作内存则先从主内存读最新值再操作,操作后要写工作内存则一定直接写回主内存。
有序性:
总结:happens-before原则
JMM中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
可以通过volatile、synchronized、lock实现有序性
Java本身具有一定的有序性,可以通过happens-before原则来判断两个指令是否本身就有序,无需通过其它方式保证,如果不满足该原则,那么虚拟机可能会对其进行重排序
Happens-before原则:
程序次序规则,一个线程内,按照代码书写顺序,书写在前面的操作先行发生于书写在后面的操作。(怎么理解,单线程程序内指令不也会重排序吗?从两个角度考虑,如果两行代码具有先后执行依赖关系,则不会对其进行指令重排序,因此满足上述规则;如果两行代码没有依赖关系,那么可能会进行指令重排序,但是重排序的执行结果和不重排序的执行结果是一致的,因此完全可以认为书写在前的还是先执行的,仍然满足上述规则)
锁定规则,一个unlock操作先行发生于后面对同一个锁的lock操作
Volatile规则,对volatile变量的写操作一定会将工作内存中的值及时更新回主内存,因此后面的读操作一定能读到写操作后的最新值,可以说写先行发生于读
传递规则,A先行发生于B,B先行发生于C,立即推:A先行发生于C
前四条比较重要,后四条比较显而易见
线程启动原则,一个线程的start方法先行发生于该线程中的其它操作
线程中断原则,一个线程interrupt方法先行发生于该线程检测到中断事件的发生
线程终结规则,线程中所有的操作都先行发生于线程的终止检测
对象终结规则,一个对象的初始化先行发生于该对象finalize方法的开始