在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);
}
}
例子中用new了10个线程,分别去调用1000次increase()方法,每次运行结果都不一致,都是一个小于10000的数字。自增操作不是原子操作,volatile 是不能保证原子性的。回到文章一开始的例子,使用volatile修饰int型变量i,多个线程同时进行i++操作。比如有两个线程A和B对volatile修饰的i进行i++操作,i的初始值是0,A线程执行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()方法时读取到inc值0,B线程也同时执行increase()方法读取到该值0,然后都进行自增操作,由于依赖了原值0,最后A线程将inc值自增为1,B线程将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字段和用于存储值的HashEntry的value。
Get方法只需要读的,不需要写,由于使用volatile修饰了共享变量,这样根据java内存模型的happenbefore原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。
ConcurrentHashMap的其他涉及到操作变量的方法都有加入了锁,来保证线程安全,采用分段锁来提高并发的效率参考文档
http://ifeve.com/concurrenthashmap/
https://blog.csdn.net/seu_calvin/article/details/52370068