java 线程——synchronized 和 volatile 关键字

synchronized 和 volatile 关键字

原子性、可见性、有序性

原子性

可见性

有序性

synchronized 关键字

加在代码块上

加在方法上

加在静态方法上

volatile 关键字

synchronized 和 volatile 的区别

扫描二维码关注公众号,回复: 3355360 查看本文章

原子性、可见性、有序性

在讲这两个关键字之前,我们先来看一下几个概念

原子性

原子性是指一个操作时不可中断的,要么全部执行成功,要么全部执行失败,即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰,我们大致可以认为基本数据类型的访问读写是具备原子性的(long 和 double 例外,这种情况很少,几乎不会发生)。

但是如果在某个场景下需要更大范围的原子性保证,java 内存还提供了lock 和 unlock 操作来满足这种需求,尽管虚拟机未把lock和 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter 和moniterexit 来隐士地使用这两个操作,这两个字节码指令反映到java 代码中就是同步块——synchronized 关键字,因此在synchronized块之间的操作也具备原子性。

可见性

可见性是指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。java内存模型是通过在修改变量后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。

有序性

java 程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序现象”和“工作内存与主内存同步延迟的现象”。

java 语言提供了volatile 和sychronized两个关键字来保证线程之间操作的有序性。volatile 关键字本身就包含了禁止指令重排序的语义,而sychronized 则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。

synchronized 关键字

synchronized 具有原子性、可见性、有序性

synchronized 关键字相当于拿到了一把锁,这保证了它的原子性。根据synchronized 关键字的位置不同,它锁住的对象也不同,具体可以分为以下三类。

加在代码块上

public void add(Object obj){
		/**
		 * 加在代码块上
		 */
		synchronized (obj){
			 //do something……
		}
	}

当synchronized 加在代码块上,它锁住的对象就是括号里面的对象,在上面的例子中指的就是obj 对象,当括号里面为this时,表示锁住的是当前对象。

class test1 implements Runnable{ 
	int count = 0;
	public void run() {
		for(int i = 0;i < 10;i++){		
			synchronized(this){
					try {
						Thread.currentThread().sleep(500);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					count++;
					System.out.println(Thread.currentThread().getName()+":"+count);
				}
			}
	}	
}

我们对这个类来进行反汇编。

synchronized 加在同步代码块上,使用的是monitorenter 和 monitorexit 指令,monitorenter 是同步代码块开始的时候,对象开始尝试获取锁,若对象拿到锁,代码块开始被执行,若锁被其它线程占有,线程进入阻塞状态,直到其它线程释放锁。monitorexit 是在同步代码块结束的时候,进行释放锁的操作,值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

加在方法上

    /**
	 * 加在方法上,锁的是当前对象 
	 */
	public synchronized void update(){
		//do something……
	}

 synchronized 加在方法上,锁住的是当前对象。

加在静态方法上

/**
	 * 加在静态方法上,锁的是类,静态存在方法区
	 */
	public static synchronized void del(){
		//do something……
	}

volatile 关键字

当一个变量定义为volatile 后,它将具备两种特性

  • 可见性:保证此变量对所有线程的可见性
  • 有序性:禁止指令重排序优化

可见性:这里的可见性是指当一个线程修改了这个变量的值,新值对其他线程来说是可以立即得知的。

在写的时候,即修改变量
1. 将修改的变量的副本写入主内存
2. 其它线程的副本置为无效

读的时候
先判断 volatile 关键字修饰的变量副本是否有效,有效直接读取
反之,则到主内存获取最新值 

由于volatile 变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性。

  • 运算结果并不依赖变量当前值,或者能够确保只有单一线程修改变量的值。
  • 变量不需要与其它的状态变量共同参与不变约束

有序性:程序执行的顺序按照代码的先后顺序来执行

我们先来看一下指令重排序是什么,在下面这段代码中,语句1 和语句2 并没有依赖关系,也就是说它们无论哪个先执行对于结果来说都没有什么影响,所以这儿可能会发生指令重排序。

指令重排序就是cpu 为了提高效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

这是在单线程中,在多线程中指令重排序可能会产生错误的结果。

在下面的代码中,线程 1 里面语句1 和语句2 没有依赖关系,所以有可能当语句2 执行完之后语句1 还未执行,但这个时候线程2 检测到了inited为false,退出了sleep状态,执行下面的代码,这个时候就可能会出错。

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

volatile 关键字就避免了指令的重排序,那么它是如何实现这个操作的呢。

volatile 变量在赋值后多执行了一个lock 操作,这个操作相当于设置了一个内存屏障(指令重排序时不能把后面的指令重排序的内存屏障之前的位置)。lock的作用是使得本cpu的Cache 写入了内存,该写入的动作也会引起别的Cpu或者别的内核无效化其Cache。因此,lock指令把修改同步到内存时,意味着所有之前的操作都已经执行完成,这样便形成了“指令重排序无法越过内存屏障”的效果。 

synchronized 和 volatile 的区别

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

猜你喜欢

转载自blog.csdn.net/Alyson_jm/article/details/82808316