java多线程高级篇(详解)
1、Java内存模型
Java内存模型是每个java程序员必须掌握理解的,这是Java的核心基础,对我们编写代码特别是并发编程时有很大帮助。由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。
1.1.Java程序执行流程回顾
如图所示
首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),
然后由JVM中的类加载器加载各个类的字节码文件,
加载完毕之后,交由JVM执行引擎执行。
Java内存模型指的就是Runtime Data Area(运行时数据区),即程序执行期间用到的数据和相关信息保存区。
1.2、Java内存模型
根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。结构如下图:
PC程序计数器:
- 每个线程对应有一个程序计数器。
- 各线程的程序计数器是线程私有的,互不影响,是线程安全的。
- 程序计数器记录线程正在执行的内存地址,以便被中断线程恢复执行时再次按照中断时的指令地址继续执行
Java栈JavaStack(虚拟机栈JVM Stack):
- 每个线程会对应一个Java栈;
- 每个Java栈由若干栈帧组成;
- 每个方法对应一个栈帧;
- 栈帧在方法运行时,创建并入栈;方法执行完,该栈帧弹出栈帧中的元素作为该方法返回值,该栈帧被清除;
- 栈顶的栈帧叫活动栈,表示当前执行的方法,才可以被CPU执行;
- 线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
- 栈扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;
方法区MethodArea
- 方法区是Java堆的永久区(PermanetGeneration)
- 方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,
- 方法区是被Java线程共享的
- 方法区要使用的内存超过其允许的大小时,会抛出OutOfMemoryError: PremGen space的错误信息。
常量池ConstantPool:
- 常量池是方法区的一部分。
- 常量池中存储两类数据:字面量和引用量。
字面量:字符串、final变量等。
引用量:类/接口、方法和字段的名称和描述符, - 常量池在编译期间就被确定,并保存在已编译的.class文件中
本地方法栈Native Method Stack:
- 本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。
- 本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
Java内存模型工作示意图
- 首先类加载器将Java代码加载到方法区
- 然后执行引擎从方法区找到main方法
- 为方法创建栈帧放入方法栈,同时创建该栈帧的程序计数器
- 执行引擎请求CPU执行该方法
- CPU将方法栈数据加载到工作内存(寄存器和高速缓存),执行该方法
- CPU执行完之后将执行结果从工作内存同步到主内存
线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。
当个多个线程同时读写某个内存数据时,就会产生多线程并发问题,要解决这些问题就涉及到多线程编程三个特性:原子性,有序性,可见性。
2、多线程特性
多线程编程要保证满足三个特性:原子性、可见性、有序性。
原子性
原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。显然,对于单线程来说,可见性问题是不存在的。
有序性
有序性即程序执行的顺序按照代码的先后顺序执行
3、多线程控制类
为了保证多线程的三个特性,Java引入了很多线程控制机制,下面介绍其中常用的几种:
- ThreadLocal
- 原子类
- Lock类
- Volatile关键字
3.1、ThreadLocal
作用
ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本。
当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal,比如数据库连接Connection,每个请求处理线程都需要,但又不相互影响,就是用ThreadLocal实现。
示例
两个线程分别转账
package com.liuchunming.ThreadLocal;
import java.lang.annotation.Target;
/**
* @author liuchunming
* @site www.liuchunming.com
* @company xxx公司
* @create 2021-01-06 11:31
*/
public class ThreadLocalDemo {
//1、创建银行对象:钱,取款、存款
static class Bank{
private ThreadLocal<Integer> threadLocal =new ThreadLocal<Integer>(){
protected Integer initialValue(){
return 0;
}
};
public Integer get(){
return threadLocal.get();
}
public void set(Integer monry){
threadLocal.set(threadLocal.get()+monry);
}
}
//2、创建转账对象、从银行值取钱、保存到账户银行
static class Transfer implements Runnable{
private Bank bank;
public Transfer(Bank bank){
this.bank=bank;
}
public void run() {
for (int i=0; i<10; i++){
bank.set(10);
System.out.println(Thread.currentThread().getName()+"账户余额"+bank.get());
}
}
}
//3、main方法中使用两个对象模拟转账
public static void main(String[] args) {
Bank bank =new Bank();
Transfer transfer =new Transfer(bank);
Thread thread1 =new Thread(transfer,"客户1");
Thread thread2 =new Thread(transfer,"客户2");
thread1.start();;
thread2.start();
}
}
打印结果:
分析
- 在ThreadLocal类中定义了一个ThreadLocalMap,
- 每一个Thread都有一个ThreadLocalMap类型的变量threadLocals
- threadLocals内部有一个Entry,Entry的key是ThreadLocal对象实例,value就是共享变量副本
- ThreadLocal的get方法就是根据ThreadLocal对象实例获取共享变量副本
- ThreadLocal的set方法就是根据ThreadLocal对象实例保存共享变量副本
3.2、原子类
Java的java.util.concurrent.atomic包里面提供了很多可以进行原子操作的类,分为以下四类:
- 原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong
- 原子更新数组:AtomicIntegerArray、AtomicLongArray
- 原子更新引用:AtomicReference、AtomicStampedReference等
- 原子更新属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
提供这些原子类的目的就是为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题。
3.2.1、非原子性操作问题演示
非原子性的操作会引发什么问题呢?下面以i++为例演示非原子性操作问题。
i++并不是原子操作,而是由三个操作构成:
tp1 = i;
tp2 = tp1+1;
i = tp2;
所以单线程i的值不是有问题,但多线程下就会出错,多线程示例代码如下:
package com.liuchunming.ThreadAtomic;
/**
* @author liuchunming
* @site www.liuchunming.com
* @company xxx公司
* @create 2021-01-06 18:56
*/
public class AtomicDemo {
static private int n;//执行n++操作的变量
public static void main(String[] args) throws InterruptedException {
int j=0;
while (j<100){
n=0;
Thread thread1=new Thread(new Runnable() {
public void run() {
for(int i=0; i<1000;i++){
n++;
}
}
});
Thread thread2=new Thread(new Runnable() {
public void run() {
for(int i=0; i<1000;i++){
n++;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
j++;
System.out.println("打印n的值是:"+n);
}
}
}
执行结果如下:发现n的最终值可能不是2000
3.2.2、原子类解决非原子性操作问题
AtomicInteger类可以保证++炒作的原子性;
getAndIncrement方法:对应n++
incrementAndGet方法:对应++n
decrementAndGet方法:对应–n
getAndDecrement方法:对应n–
以上代码修改如下:
package com.liuchunming.ThreadAtomic;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author liuchunming
* @site www.liuchunming.com
* @company xxx公司
* @create 2021-01-06 18:56
*/
public class AtomicDemo2 {
// static private int n;//执行n++操作的变量
static AtomicInteger atomicInteger; //代替变量n
public static void main(String[] args) throws InterruptedException {
int j=0;
while (j<100){
// n=0;
atomicInteger =new AtomicInteger(0);//创建原子整数,初始值是0
Thread thread1=new Thread(new Runnable() {
public void run() {
for(int i=0; i<1000;i++){
// n++;
atomicInteger.getAndIncrement();//代表n++操作
}
}
});
Thread thread2=new Thread(new Runnable() {
public void run() {
for(int i=0; i<1000;i++){
// n++;
atomicInteger.getAndIncrement();//代表n++操作
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
j++;
System.out.println("打印n的值是:"+atomicInteger.get());
}
}
}
3.2.3、原子类CAS原理分析
3.2.4、CAS的ABA问题及解决
ABA问题分析
当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。如下图:
ABA问题解决
我们在判断栈顶元素A是不是原来的值以外,还要加入一个时间搓。比如T1第一次判断时间搓是1,T2进行变更后时间搓为2。这时T1第二次判断就可以判断时间搓的比对,这样就解决了ABA的问题
AtomicStampedReference解决ABA问题的方法
AtomicStampedReference初始时间戳:构造函数设置初始值和初始时间戳
getStamp:获取时间戳
getReference:获取预期值
compareAndSet:(预期值.更新值.预期时间戳.更新时间戳):实现CAS时间戳和预期值的比对
package com.liuchunming.ThreadAtomic;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author liuchunming
* @site www.liuchunming.com
* @company xxx公司
* @create 2021-01-06 18:56
*/
public class AtomicStamped {
// static private int n;//执行n++操作的变量
// static AtomicInteger atomicInteger; //代替变量n
static AtomicStampedReference<Integer> atomicInteger;
public static void main(String[] args) throws InterruptedException {
int j=0;
while (j<100){
// n=0;
atomicInteger =new AtomicStampedReference(0,0);//初始变量值、时间戳值
Thread thread1=new Thread(new Runnable() {
public void run() {
for(int i=0; i<1000;i++){
int stamp;
Integer reference;
do{
stamp = atomicInteger.getStamp();
reference = atomicInteger.getReference();
}while (!atomicInteger.compareAndSet(reference,reference+1,stamp,stamp+1));
}
}
});
Thread thread2=new Thread(new Runnable() {
public void run() {
for(int i=0; i<1000;i++){
int stamp;
Integer reference;
do{
stamp = atomicInteger.getStamp();
reference = atomicInteger.getReference();
}while (!atomicInteger.compareAndSet(reference,reference+1,stamp,stamp+1));
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
j++;
System.out.println("打印n的值是:"+atomicInteger.getReference());
}
}
}
执行效果如下:执行结果也是2000
注意:采用AtomicStampedReference会降低性能,慎用。
3.3、Lock类
3.3.1.Lock接口关系图
Lock和ReadWriteLock是两大锁的根接口
Lock 接口支持重入、公平等的锁规则:实现类 ReentrantLock、ReadLock和WriteLock。
ReadWriteLock 接口定义读取者共享而写入者独占的锁,实现类:ReentrantReadWriteLock。
3.3.2、可重入锁
不可重入锁,即线程请求它已经拥有的锁时会阻塞。
可重入锁,即线程可以进入它已经拥有的锁的同步代码块。
package com.liuchunming.ThreadLock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author liuchunming
* @site www.liuchunming.com
* @company xxx公司
* @create 2021-01-07 20:39
*/
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock lock=new ReentrantLock();
for (int i=0;i<10;i++){
lock.lock();
System.out.println("加锁次数:"+(i+1));
}
for (int i=0;i<10;i++){
try {
System.out.println("解锁次数:"+(i+1));
}finally {
lock.unlock();
}
}
}
}
3.3.3、读写锁
读写锁,即可以同时读,读的时候不能写;不能同时写,写的时候不能读。
示例代码:
package com.liuchunming;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author liuchunming
* @site www.liuchunming.com
* @company xxx公司
* @create 2021-01-08 19:27
*/
public class ReadWriteLockDemo {
private Map<String, Object> map = new HashMap<String, Object>();
//创建一个读写锁实例
private ReadWriteLock rw = new ReentrantReadWriteLock();
//创建一个读锁
private Lock r = rw.readLock();
//创建一个写锁
private Lock w = rw.writeLock();
/**
* 读操作
*
* @param key
* @return
*/
public Object get(String key) {
r.lock();
System.out.println(Thread.currentThread().getName() + "读操作开始执行......");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
return map.get(key);
} finally {
r.unlock();
System.out.println(Thread.currentThread().getName() + "读操作执行完成......");
}
}
/**
* 写操作
*
* @param key
* @param value
*/
public void put(String key, Object value) {
try {
w.lock();
System.out.println(Thread.currentThread().getName() + "写操作开始执行......");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
} finally {
w.unlock();
System.out.println(Thread.currentThread().getName() + "写操作执行完成......");
}
}
public static void main(String[] args) {
final ReadWriteLockDemo d = new ReadWriteLockDemo();
d.put("key1", "value1");
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
}
}
3.4、Volatile关键字
一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(注意:不保证原子性)
- 禁止进行指令重排序。(保证变量所在行的有序性)
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
3.4.1、应用场景
基于volatile的作用,使用volatile必须满足以下两个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
常见应用场景如下:
状态量标记:
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
双重校验:
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}