Java并发——线程同步Volatile与Synchronized详解

volatilesynchronized的区别与联系:

Java,为了保证多线程数据时保证数据的一致性,可以采用两种方式:使用volatile关键字:用一句话概括volatile,它能够使变量在值发生改变时能尽快地让其他线程知道。如用synchronized关键字,表示或者使用锁对象。volatile是一个变量修饰符,而synchronized是一个方法或块的修饰符。所以我们使用这两种关键字来指定三种简单的存取变量的方式。所以volatile只能在线程内存和主内存之间同步一个变量的值,而synchronized则同步在线程内存和主内存之间的所有变量的值,并且通过锁住和释放监听器来实现。显然,synchronized在性能上将比volatile更加有所消耗。

volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

volatile仅能使用在变量级别,synchronized则可以使用在变量,方法。volatile仅能实现变量的修改可见性,但不具备原子特性。volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化。(这里的优化指的是指令重排,用volatile的目的就是禁止指令重排,并在一定场景可以替代锁)

volatile只是保证了多线程环境下当前修饰的变量被其他线程修改后,会尽快地让其他线程知道,其他线程读取的就是修改后的值,但是由于volatile不保证当前变量的操作原子性,也就是当前线程读取这个变量后,其他线程也会同时读取当前的变量,并进行操作,这样就造成变量的线程不安全,如果不想用synchronized加锁的方法来处理当前变量,如何解决这个问题呢,可以使用线程安全的集合如concurrenthashmap,这样当concurrenthashmap被多个线程同时读取时,其中一个线程对其进行了修改,其他线程是不能修改的,修改完成后,其他线程重新读取修改的值,尽管也使用了锁,但是由于分段锁,并且get读取是不用锁的,不像synchronized锁住的对象会对其他线程阻塞,性能较好点

Volatile的理解

首先它是有两个特性:

1.volatile禁止指令重排

 

指令重排是指处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

程序执行到volatile修饰变量的读操作或者写操作时,在其前面的操作肯定已经完成,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。

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

因为指令重排序,有可能语句2会在语句1之前执行,可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了。

1.volatile修饰的变量具有可见性,但不具有原子性

volatile是变量修饰符,其修饰的变量具有可见性。

可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。

看下面案例:

	class MyThread extends Thread {             
	    private volatile boolean isStop = false;          
	    public void run() {      
	        while (!isStop) {      
	            System.out.println("do something");      
	        }      
	    }      
	    public void setStop() {      
	        isStop = true;      
	    }            
	}  

线程执行run()的时候我们需要在线程中不停的做一些事情,比如while循环,那么这时候该如何停止线程呢?如果线程做的事情不是耗时的,那么只需要使用一个标志即可。如果需要退出时,调用setStop()即可。这里就使用了关键字volatile,这个关键字的目的是如果修改了isStop的值,那么在while循环中可以立即读取到修改后的值。

Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。

定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值,如果写的操作依赖于原值,会造成线程不安全,参考下面的案例)

之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

多线程环境下使用volatile修饰变量替换锁,以便提高性能:

前提条件是是否依赖原值,如果依赖原值,就只能保证多线程读,写的操作必须是单线程,即要加锁,不能多线程同时写,如果不依赖原值,则可以多线程的写了。

看下面案例,必须使用synchronized而不能使用volatile的场景,因为在它是多线程,并且increase();写的操作依赖于原值inc

	public class Test {  
	    public volatile int inc = 0;  
	    public void increase() {  
	        inc++;  
	    }  
	       
	    public static void main(String[] args) {  
	        final Test test = new Test();  
	        for(int i=0;i<10;i++){  
	            new Thread(){  
	                public void run() {  
	                    for(int j=0;j<1000;j++)  
	                        test.increase();  
	                };  
	            }.start();  
	        }  
	           
	        while(Thread.activeCount()>1)  //保证前面的线程都执行完  
19.	            Thread.yield();  
20.	        System.out.println(test.inc);  
	    }  
	}  

例子中用new10个线程,分别去调用1000increase()方法,每次运行结果都不一致,都是一个小于10000的数字。自增操作不是原子操作,volatile 是不能保证原子性的。回到文章一开始的例子,使用volatile修饰int型变量i,多个线程同时进行i++操作。比如有两个线程ABvolatile修饰的i进行i++操作,i的初始值是0A线程执行i++时刚读取了i的值0,就切换到B线程了,B线程(从内存中)读取i的值也为0,然后就切换到A线程继续执行i++操作,完成后i就为1了,接着切换到B线程,因为之前已经读取过了,所以继续执行i++操作,最后的结果i就为1了。同理可以解释为什么每次运行结果都是小于10000的数字。
但是使用synchronized对部分代码进行如下修改,就能保证同一时刻只有一个线程获取锁然后执行同步代码。运行结果必然是10000

	public  int inc = 0;  
	public synchronized void increase() {  
	        inc++;  
	}  

分析:使用volatile修饰的变量不具有原子性,当A线程执行increase()方法时读取到inc0B线程也同时执行increase()方法读取到该值0,然后都进行自增操作,由于依赖了原值0,最后A线程将inc值自增为1B线程将inc值自增为1,导致inc的值不符合我的预期2,最后10个线程运行1000次下来,得到的不是我想要的10000,而是小于10000.

 

如果使用synchronized 修饰increase()方法的话,自增操作就会加锁变成单线程的写A线程执行increase()方法时,B线程是阻塞的,inc自增,由0变为1,当A线程执行完毕,B线程执行increase()方法,1变为2,这样inc的值就不会相冲突,最后的结果也是稳定的10000

synchronized可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。


可见性体现在:通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中。

原子性表现在:要么不执行,要么执行到底。

ConcurrentHashMap的get方法里将要使用的共享变量就是使用的volatile来修饰

如用于统计当前Segement大小的count字段和用于存储值的HashEntryvalue

Get方法只需要读的,不需要写,由于使用volatile修饰了共享变量,这样根据java内存模型的happenbefore原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

ConcurrentHashMap的其他涉及到操作变量的方法都有加入了锁,来保证线程安全,采用分段锁来提高并发的效率

参考文档

http://ifeve.com/concurrenthashmap/

https://blog.csdn.net/seu_calvin/article/details/52370068



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值