java 内存模型
学习目的
- 了解更深层次内存的使用和读取实现,方便日后分析多线程内存相关问题
- 工作中遇到的并发问题,并不好复现,需要对理论知识掌握得足够深刻,才能更好分析
操作系统内存模型
- 现代CPU都存在多级缓存,用来缓存CPU经常使用的数据,提供数据的读写、处理速度,分为L1(高速缓存,保存极为常用的数据,容量小),L2(高速缓存,缓存经常使用的数据),Shared L3 Cache(共享缓存,线程间共享),主存(内存)
- 其中L1,L2是CPU私有,能够命中CPU用到的数据 的80%,剩余的是L3缓存
- Shared L3 Cache是共享缓存,多个CPU之间可以共享- 内存也是多个CPU共享
java内存模型
- 每个线程都有自己的工作内存
- 工作内存包含线程本地局部变量和主内存的副本拷贝
- 线程间共享变量通过主内存在各线程间进行同步(通过去主内存去拷贝)
- 内存模型图示
线程间数据不一致问题
线程间不可见性
- 由于线程间的不可见性,线程读取了其他未及时写入主内存的变量值
package com.myd.cn;
/** *
* @author dymll
* * @date 2014/3/25
* * */
public class ThreadSharedVariables {
private static int sharedVar = 0;
public static void main(String[] args) {
Thread threadA = new Thread(()->{
System.out.println(Thread.currentThread().getName()+" sharedVar = "+sharedVar);
sharedVar = 1;
System.out.println(Thread.currentThread().getName()+" sharedVar = "+sharedVar);
},"ThreadA");
Thread threadB = new Thread(()->{
System.out.println(Thread.currentThread().getName()+" sharedVar = "+sharedVar);
sharedVar = 1;
System.out.println(Thread.currentThread().getName()+" sharedVar = "+sharedVar); },"ThreadB");
threadA.start();
/ /线程B启动后,获取的值认为0,说明线程间共享变量还没等线程A在CPU缓存中修改后写入到主内存,就被线程B读取了,造成脏读
threadB.start();
}}
- 上述例子,线程间修改不可见导致线程发生脏读
jvm编译器进行指令优化,造成指令重排,发生变量赋值、读取顺序错误
- jvm编译器或CPU编译对指令进行重排,发生读取先于赋值之前,造成数据错误
package com.myd.cn; public class ThreadCommandReorder { private static int a = 0; private static int b = 0; private static int x = 0; private static int y = 0; public static void main(String[] args) throws InterruptedException { Thread threadA = new Thread(()->{ b = 1; x = a; },"threadA"); Thread threadB = new Thread(()->{ a = 1; y = b; },"threadB"); threadA.start(); threadB.start(); //等待ThreadA执行完后,主线程才往下继续执行 threadA.join(); //等待ThreadB执行完后,主线程才往下继续执行 threadB.join(); //此时因为JVM指令编译会对指令进行优化,可能会发生指令重排,而出现错误的赋值,如x = 1 ; y = 0 //合理输出应是 x=1,y=1 System.out.println("x = "+x+" ; y = "+y); }}
- 结果输出
- 上述代码可能发生如下重排
- 指令重排序发生在JVM编译器、CPU处理器中,两个阶段都可能会对指令进行重排,需要使用Happen-Before机制去避免可能出现的错误
Happen-Before解决指令重排问题
- 程序次序规则,在程序中如操作A先于操作B发生,那么线程中操作A也先于操作B发生
- 对象终结规则,一个对象的构造器的完成先行发生于finalize()或cleaner机制。
- 锁规则,对于同一把锁,加锁操作先于释放锁操作
- 传递规则,若操作A先行发生于操作B,而操作B有先行发生于C,则操作A先行发生于操作C
- volatile变量规则,对一个volatile变量的写操作先行发生于对这个变量的读操作
- 线程启动规则,Thread对象start()方法先行发生于此线程中的每个指令操作
- 线程中断规则,一个线程对另一个线程调用interrupt()方法,先行发生于被中断线程检测到中断时间
- 线程结束规则,线程中所有操作都先行发生于线程的终止,如线程结束,Thread.join()返回
synchronized和volatile关键字
- synchronized ,java关键字,用来给方法或代码加锁,控制方法或代码块在同一时间点只有一个线程使用,用来解决多线程同时服务出现的并发问题
- 如使用 synchronized方法保证卖票不会超卖
package com.myd.cn;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SynchronizedSellTicket {
private static int totalTickets = 10000;
private static final Object LOCK = new Object();
public static void main(String[] args) {
sellTickets();
}
/**
* 通过启动线程池,向线程池提交任务,有线程池调度线程执行任务
**/
private static void sellTickets(){
ExecutorService executorService = Executors.newFixedThreadPool(2);
Runnable runnableA = ()->{ i
nt threadAtickets = 0 ;
//如果不低于1张票
while (sellTicketsWithSynchronizedObject(1)) {
threadAtickets ++;
}
System.out.println("程序A卖出火车票: "+threadAtickets);
};
Runnable runnableB = ()->{
int threadBtickets = 0 ;
//如果不低于1张票
while (sellTicketsWithSynchronizedObject(1)) {
threadBtickets ++;
}
System.out.println("程序B卖出火车票: "+threadBtickets);
};
//提交两个卖票程序
executorService.submit(runnableA);
executorService.submit(runnableB);
}
/**
* 在方法加锁,进行买票,保证票不卖超
* @param count
* @return
* */
private static synchronized boolean sellTicketsWithSyncMethod(int count) {
//如果要卖票数小于剩余票数,则不买,退出卖票流程
if(totalTickets - count < 0){ r
eturn false;
}else{
//否则进入卖票流程
totalTickets = totalTickets - count ;
return true;
}
}
/**
* 在对象加锁,降低锁的作用范围,提高并发量
* * @param count
* */
private static boolean sellTicketsWithSynchronizedObject(int count) {
synchronized (LOCK) {
//计算总票数减去申请票数的差值
int flag = totalTickets - count;
//如果为负数,则表明总票数不够申请票数,不进行售卖
if(flag < 0){
return false;
}else{
totalTickets = totalTickets - count;
return true;
}
}
}
}
- 运行结果
- 查看其字节码指令,可看出是通过synchronized机制完成线程间的变量共享
javap -c -p -v SynchronizedSellTicket.class
synchronized关键字与volatile关键字
synchronized使用分类
-
synchronized方法
- 方法使用ACC_SYNCHRONIZED表示,线程获取只有获取该标识,才能进入对应方法
- 如果是static方法,锁是作用在类上,是串行执行,等获取锁的线程执行后,后续线程才能获取锁
1.代码 package com.myd.cn; public class CheckSynchronizedOnDiffObj { public static synchronized void staticSynchronized() throws InterruptedException{ System.out.println(Thread.currentThread().getName()+" is Sleeping"); Thread.sleep(1000L); System.out.println(Thread.currentThread().getName()+" finished Sleeping"); } public static void main(String[] args) { new Thread(()->{ try { CheckSynchronizedOnDiffObj.staticSynchronized(); } catch (InterruptedException e) { e.printStackTrace(); } },"thread1").start();; new Thread(()->{ try { CheckSynchronizedOnDiffObj.staticSynchronized(); } catch (InterruptedException e) { e.printStackTrace(); } },"thread2").start(); } } 2.执行结果,串行执行的 thread1 is Sleeping thread1 finished Sleeping thread2 is Sleeping thread2 finished Sleeping
-
若是作用在实例方法,锁作用在类的实例上,类实例级别是可以同时执行
package com.myd.cn; public class CheckSynchronizedOnDiffObj { public synchronized void ObjSysnchronized() throws InterruptedException { System.out.println(Thread.currentThread().getName()+" is Sleeping"); Thread.sleep(1000L); System.out.println(Thread.currentThread().getName()+" finished Sleeping"); } public static void main(String[] args) { CheckSynchronizedOnDiffObj check1 = new CheckSynchronizedOnDiffObj(); CheckSynchronizedOnDiffObj check2 = new CheckSynchronizedOnDiffObj(); new Thread(()->{ try { check1.ObjSysnchronized(); } catch (InterruptedException e) { e.printStackTrace(); } },"thread1").start();; new Thread(()->{ try { check2.ObjSysnchronized(); } catch (InterruptedException e) { e.printStackTrace(); } },"thread2").start(); } } 2.结果,并行输出 thread1 is Sleeping thread2 is Sleeping thread2 finished Sleeping thread1 finished Sleeping
-
synchronized代码块
- 使用monitorenter和monitorexit指令控制线程进出同步代码块## synchronized与 ReentrantLock区别
- 代码调用
package com.myd.cn; import java.util.concurrent.locks.ReentrantLock; public class SynchronizedAndReentrantLock { private static Object LOCK = new Object(); private static void reetrant(){ synchronized (LOCK) { System.out.println("持有当前锁"); synchronized(LOCK){ System.out.println(" 再次持有锁"); } } } /** * 使用默认非公平(可实现公平)的可重入排它锁 * 可以添加获取锁的等待时间(超时不进行等待),也可不指定超时时间 */ private static void useReentrantLock(){ ReentrantLock lock = new ReentrantLock(); try { //获取后,一定在finally代码块进行释放 lock.lock(); System.out.println("持有当前锁"); } catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } public static void main(String[] args) { //reetrant(); useReentrantLock(); } }
- 相同点
1.都是用于多线程中对资源加锁,控制代码同一时间只有单线程在执行 2.当一个线程获取了锁,其他线程均需要阻塞等待 3.均为可重入锁
- 差异点
1.实现层次 Synchronzied是JVM级别实现的同步机制,由JVM字节码实现,以前并发量比较时,稍逊于ReentrantLock,但是随着JDK对其不断优化,一般场景下由于 ReentrantLockReentrantLock是代码层面实现的锁机制,获取可以设置超时时间,使用完毕后一定要释放。Synchronized无法设置超时时间,但是在获取锁后,使用完毕无需手动释放锁资源 2.中断等待: 获取到Synchronized的线程如果因为外部原因(I/O,网络)等时阻塞,无法处理其他业务 Reentrant在进行中断等待时,可以处理其他业务,进而提高并发量,适用于高竞态的并发场景 3.锁的公平性 ReentrantLock实现了非公平锁(默认)和公平锁(实例化传参为true) synchronized 只有非公平锁
volatile
- 用来保证多线间对变量的内存可见性,将变量强制写入到主存后,及时通知给其他线程
- 使用Happen-Before机制避免对volatile相关对象前后的程序进行指令重排- 无法向synchronizedn那样,不能保证线程安全,不可用于数字的线程安全递增
volatile使用场景
- 修饰状态变量,用于线程间访问该变量,保证个线程可以看到最小的内存值
package com.myd.cn.volatileDemo;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class VolatileSence {
//保证线程间共享变量的可见性,修改可以及时通知到其他线程
private volatile boolean isFinished = false;
public void finish(){
isFinished = true;
}
public void work(){
//当为true时,退出while循环
while (!isFinished) {
//working
System.out.println("i'm working");
}
}
}
- 单例模式下单实例对象构造,避免多线程情况下由于内存不可见而重复多次构造对象
package com.myd.cn.volatileDemo; public class Singleton {
//用时申请,减少开销,提供加载速度
private static volatile Singleton singleton;
//私有化构造器
private Singleton(){
}
//使用double check,减少同步代码块的作用范围
public static Singleton getSingleton(){
if(null == singleton){
//同步机制作用在类上
synchronized (Singleton.class) {
if(null == singleton){
singleton = new Singleton();
}
}
}
return singleton;
}}
synchronzied和volatile区别
- synchronized用于同部署控制,具有原子性,控制同一时间只有一个线程执行一个方法或代码块
- volatile只保证线程的内存可见性,不具备锁的特性,无法保证修饰对象的原子性