java内存模型及线程案例分析

java 内存模型

学习目的

  • 了解更深层次内存的使用和读取实现,方便日后分析多线程内存相关问题
  • 工作中遇到的并发问题,并不好复现,需要对理论知识掌握得足够深刻,才能更好分析

操作系统内存模型

  • 现代CPU都存在多级缓存,用来缓存CPU经常使用的数据,提供数据的读写、处理速度,分为L1(高速缓存,保存极为常用的数据,容量小),L2(高速缓存,缓存经常使用的数据),Shared L3 Cache(共享缓存,线程间共享),主存(内存)
    现代CPU多级缓存
  • 其中L1,L2是CPU私有,能够命中CPU用到的数据 的80%,剩余的是L3缓存
    L1-L2命中率达80,极大提高CPU处理效率
  • Shared L3 Cache是共享缓存,多个CPU之间可以共享- 内存也是多个CPU共享

java内存模型

  • 每个线程都有自己的工作内存
  • 工作内存包含线程本地局部变量和主内存的副本拷贝
  • 线程间共享变量通过主内存在各线程间进行同步(通过去主内存去拷贝)
  • 内存模型图示
    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     

sellTickets

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只保证线程的内存可见性,不具备锁的特性,无法保证修饰对象的原子性
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值