JAVA并发与多线程浅析
1. 并发浅析
1.1 并发简介
并发:同时拥有两个或者多个线程,如果程序在单核处理器上运行,多个线程将交替地换入或者换出内存,这些线程是同时“存在”的,每个线程都处于执行过程中的某个状态。如果运行在多核处理器上,此时,程序中的每个线程都将分配到一个处理器核上,因此可以同时运行。
1.2 CPU多级缓存
缓存模式的转换
为什么需要 CPU Cache
:CPU 的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU 常常需要等待主存,浪费资源。所以 Cache 的出现,是为了缓解 CPU 和内存之间速度不匹配的问题(结构:CPU -> Cache -> Memory)
CPU Cache的意义:
- 时间局部性:如果某个数据被访问,那么在不久的将来它可能被再次访问
- 空间局部性:如果某个数据被访问,那么与它相邻的数据很快也可能被访问
CPU 多级缓存的乱序执行优化(重排序)原理:处理器为提高运算速度而做出违背代码原有顺序的优化
1.3 Java内存模型:JMM
JMM:内存模型描述了程序中的各个变量之间的关系,以及在实际计算机系统中将变量储存到内存和从内存中取出变量这样的底层细节。JMM 定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。Java 线程之间的通信由 Java 内存模型控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。
JMM 控制示意图:
Java 内存模型 — 同步的八种操作:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,一遍随后的 load 动作使用
- load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量
- store(储存):作用于工作内存的变量,把工作内存中的变量值传送到主存中,以便以后的 write 操作
- write(写入):作用于主内存的变量,它把 store 操作从工作内存中的变量的值传送到主内存的变量中
1.4 并发的优势与风险
优势:
- 速度:同时处理多个请求,响应更快;复杂的操作可以分成多个进程同时进行
- 设计:程序设计在某些情况下更简单,也可以有更多的选择
- 资源利用:CPU 能够在等待 IO 的时候做一些其他事
风险:
- 安全性:多个线程共享数据时可能会产生与期望不相符的结果
- 活跃性:某个操作无法继续进行下去s时,就会发生活跃性问题,比如:死锁、饥饿等问题
- 性能:线程过多时会使得:CPU 频繁切换,调度时间增多;同步机制;消耗过多内存
2. 并发代码模拟
使用 CountDownLatch
类、Semaphore
类
线程不安全情况测试
package Test3;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* 通过 CountDownLatch 、 Semaphore 测试线程不安全
* @author JJJiker
*
*/
public class ConcurrencyTest2 {
//请求总数
public static int clientTotal = 5000;
//同时并发执行的线程数
public static int threadTotal = 200;
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0;i <clientTotal;i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("count: " + count);
}
public static void add() {
count++;
}
}
可见,每一次运行程序,count 总数始终无法到达请求总数 clientTotal
5000
3. 线程安全性
3.1 简介
定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式,或者这些进程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的
线程安全性三大特征:
1、原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
2、可见性:一个线程对主内存的修改可以及时的被其他线程观察到
3、有序性:一个线程观察其他线程中的指令执行顺序,由于指令的重排序的存在,该观察结果一般杂乱无序
3.2 原子性 — Atomic包
- AtomicXXX:CAS、Unsafe.compareAndSwapInt
- AtomicLong、LongAdder
- AtomicReference、AtomicReferenceFieldUpdater
- AtomicStampReference:CAS 的 ABA 问题
代码演示
1、AtomicInteger CAS(compareAndSet) 保证线程安全
public static AtomicInteger count = new AtomicInteger(0);
public static void add() {
count.incrementAndGet();
}
原理:incrementAndGet 方法将调用 U.getAndAddInt,该方法大致原理是不断尝试将一个比当前值大1的新值赋给自己,如果失败则说明在执行"获取-设置"操作的时已经被其它线程修改过了,于是便再次进入循环下一次操作,直到成功为止
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
2、AtomicReference类 代码测试
AtomicReference函数列表
// 使用 null 初始值创建新的 AtomicReference。
AtomicReference()
// 使用给定的初始值创建新的 AtomicReference。
AtomicReference(V initialValue)
// 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
boolean compareAndSet(V expect, V update)
// 获取当前值。
V get()
// 以原子方式设置为给定值,并返回旧值。
V getAndSet(V newValue)
// 最终设置为给定值。
void lazySet(V newValue)
// 设置为给定值。
void set(V newValue)
// 返回当前值的字符串表示形式。
String toString()
// 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
boolean weakCompareAndSet(V expect, V update)
测试代码
package Test3;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceExample {
private static AtomicReference<Integer> count = new AtomicReference<>(0);
public static void main(String[] args) {
count.compareAndSet(0, 2);
count.compareAndSet(0, 1);
count.compareAndSet(1, 3);
count.compareAndSet(2, 4);
count.compareAndSet(3, 5);
System.out.println(count.get());
}
}
原理:
AtomicReference 是通过 “volatile” 和 "Unsafe“ 提供的CAS函数实现"原子操作。
(01) value是volatile类型。这保证了:当某线程修改value的值时,其他线程看到的value值都是最新的value值,即修改之后的volatile的值。
(02) 通过CAS设置value。这保证了:当某线程池通过CAS函数(如compareAndSet函数)设置value时,它的操作是原子的,即线程在操作value时不会被中断。
3.3 原子性 — 锁
Synchronized
同步锁,修饰对象:
- 修饰代码块:大括号括起来的代码,作用于调用的对象
- 修饰方法:整个方法,作用于调用的对象
- 修饰静态方法:整个静态方法,作用于所有对象
- 修饰类:作用于所有对象
1、修饰代码块:
public void test() {
synchronized (this) {
for (int i = 0;i < 10;i++) {
System.out.println("test: " + i);
}
}
}
同一个对象测试
public static void main(String[] args) {
SynchronizedExample example1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
example1.test();
});
executorService.execute(() -> {
example1.test();
});
}
不同对象测试
public static void main(String[] args) {
SynchronizedExample example1 = new SynchronizedExample();
SynchronizedExample example2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
example1.test();
});
executorService.execute(() -> {
example2.test();
});
}
2、修饰方法
public synchronized void test() {
for (int i = 0;i < 10;i++) {
System.out.println("test: " + i);
}
}
3、修饰静态方法
public static synchronized void test() {
for (int i = 0;i < 10;i++) {
System.out.println("test: " + i);
}
}
4、修饰类
public static void test() {
synchronized (SynchronizedExample.class){
for (int i = 0;i < 10;i++) {
System.out.println("test: " + i);
}
}
}
3.4 原子性对比
- synchronized:不可中断锁,适合竞争不激烈,可读性好
- Lock:可中断锁,多样化同步,竞争激烈时能维持常态
- Atomic:竞争激烈时能维持常态,比 Lock 性能好;只能同步一个值
3.5 可见性
导致共享变量在线程间不可见的原因:
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主存间及时更新
可见性 — volatile
通过加入内存屏障和禁止重排序优化来实现
- 对 volatile 变量写操作时,会在写操作后加入一条 store 屏障指令,将本地内存中的共享变量值刷新到主内存
- 对 volatile 变量读操作时,会在读入前加入一条 load 屏障指令,从主存中读取变量
可见性 volatile 的使用
volatile boolean inited = false;
//线程1
context =loadContext();
inited = true;
//线程2
while( !inited){
sleep();
}
doSomethingWithConfig(context)
注:volatile 不具有原子性,不适用于计数,不能保证线程安全
3.6 有序性
Java 内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行却会影响到多线程程序的并发执行的正确性
有序性 :happens — before 原则
- 程序次序规则:一个线程内,按照代码顺序,书写在前的操作先行发生于书写在后面的操作
- 锁定原则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作
- volatile变量规则:对于一个变量的写操作先行发生于对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C
- 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每一个动作
- 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件发生
- 线程终结规则:线程中所有的操作都先行发生于线程终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到此线程已终止执行
- 对象终结规则:一个对象的初始化完成先行于发生它的 finalize() 方法的开始
时间:2019.6.23 20:09