Java 并发编程——volatile与synchronized

时间:2021-12-19 01:02:06

一、Java并发基础


多线程的优点


  • 资源利用率更好
  • 程序设计在某些情况下更简单
  • 程序响应更快

这一点可能对于做客户端开发的更加清楚,一般的UI操作都需要开启一个子线程去完成某个任务,否者会容易导致客户端卡死。例如一个网络请求,可以在回调接口中进行处理,而不是等待网络请求结束了再去做其它的UI操作(UI不会卡死)

多线程的代价


  • 设计更加复杂

特别是共享数据的访问,多线程之间的数据通信等。

  • 上下文切换开销

当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。

  • 增加资源消耗

除了CPU,线程还需要一些内存来维持它本地的堆栈。它也需要占用操作系统中一些资源来管理线程。我们可以尝试编写一个程序,让它创建100个线程,这些线程什么事情都不做,只是在等待,然后看看这个程序在运行的时候占用了多少内存。

内存可见性与原子性


  • 内存可见性:

下面是一段简单的代码,在单线程环境下执行不会出现问题:

复制代码
public class TestVolatile {
boolean status = false; /**
* 状态切换为true
*/
public void changeStatus(){
status = true;
} /**
* 若状态为true,则running。
*/
public void run(){
if(status){
System.out.println("running....");
}
}
}

上面这个例子,在多线程环境里,假设线程A执行changeStatus()方法后,线程B运行run()方法,可以保证输出"running....."吗?

答案是NO!

这个结论会让人有些疑惑,可以理解。因为倘若在单线程模型里,先运行changeStatus方法,再执行run方法,自然是可以正确输出"running...."的;但是在多线程模型中,是没法做这种保证的。因为对于共享变量status来说,线程A的修改,对于线程B来讲,是"不可见"的。也就是说,线程B此时可能无法观测到status已被修改为true。那么什么是可见性呢?

所谓可见性,是指当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。很显然,上述的例子中是没有办法做到内存可见性的。

Java内存模型

为什么出现这种情况呢,我们需要先了解一下JMM(java内存模型)

 java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。

  JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如下

需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存。当然如果是出于理解的目的,这样对应起来也无不可。

Java 并发编程——volatile与synchronized

  • 原子性

要么不执行,要么执行到底。原子性就是当某一个线程修改i的值的时候,从取出i到将新的i的值写给i之间不能有其他线程对i进行任何操作。也就是说保证某个线程对i的操作是原子性的,这样就可以避免数据脏读。 通过锁机制或者CAS(Compare And Set 需要硬件CPU的支持)操作可以保证操作的原子性。

指令重排序


重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。

但是重排序也需要遵守一定规则:

a) 重排序操作不会对存在数据依赖关系的操作进行重排序。

比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。

b)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。

 重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了,来开个例子,我们对第一个TestVolatile的例子稍稍改进,再增加个共享变量a。

public class TestVolatile {
int a = 1;
boolean status = false; /**
* 状态切换为true
*/
public void changeStatus(){
a = 2;//1
status = true;//2
} /**
* 若状态为true,则running。
*/
public void run(){
if(status){//3
int b = a+1;//4
System.out.println(b);
}
}
}

假设线程A执行changeStatus后,线程B执行run,我们能保证在4处,b一定等于3么?

答案依然是无法保证!也有可能b仍然为2。上面我们提到过,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。

  使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

  

voaltile


a)当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;这个操作会导致其他线程中的缓存无效,从而保证变量对其它线程的可见性。

b)禁止指令重排序优化。

volatile禁止指令重排序也有一些规则,简单列举一下:

  • 当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
  • 当地一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
  • 当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序

synchronized


synchronized

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

具体可参考:http://www.importnew.com/21866.html

下面的例子中,当调用get方法时需要等待set运行结束:

public class SyncTestEntity {

    private int flag = 0;

    public int getFlag() {
synchronized (this) {
return flag;
}
} public void setFlag(int a) {
synchronized (this) {
System.out.println("into lock");
try {
Thread.sleep(2000);
flag = a;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} public static void main(String[] args) {
final SyncTestEntity entity = new SyncTestEntity();
new Thread(new Runnable() {
@Override
public void run() {
entity.setFlag(10);
}
}).start(); try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result=" + entity.getFlag());
}
}

synchronized demo

运行结果:

into lock
result=10

原子类(atomic)


作用: java中自增/自减操作实际上是由多个原子操作构成(read i; inc; write i),当多个线程同时对一个共享变量进行复合操作时就会出现多线程问题。

java jdk中提供了原子类,提供的所有接口都能保证对基础元素操作的原子性。

基本类型

  • AtomicBoolean:  原子更新布尔类型。
  • AtomicInteger:    原子更新整型。
  • AtomicLong:       原子更新长整型。

以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:

  • int addAndGet(int delta): 以原子的方式将输入的数值与实例中的值相加,并返回结果。
  • boolean compareAndSet(int expect, int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
  • int getAndIncrement(): 以原子的方式将当前值加 1,注意,这里返回的是自增前的值,也就是旧值。
  • void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
  • int getAndSet(int newValue): 以原子的方式设置为newValue,并返回旧值。

原子类中还有其它数组元素等,这里不再介绍。

 

使用实例: 三个线程对同一变量进行自增操作


1) 不做任何线程同步

public class AtomicTest {

    private static String TAG = "AtomicTest";

    private static  int m;
//private static volatile int m; public static void test() { m = 0;
long startTime = System.currentTimeMillis();
final CountDownLatch cdl = new CountDownLatch(3);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m++;
}
cdl.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m++;
}
cdl.countDown();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m++;
}
cdl.countDown();
}
});
t1.start();
t2.start();
t3.start();
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(TAG, "test: result=" + m + " elapsedTime=" + (System.currentTimeMillis()-startTime));
}
}

无任何同步

2) 使用volatile

与1)中代码唯一区别是在定义m时增加volatile修饰符

3)  使用同步锁进行同步

public class AtomicTest {

    private static String TAG = "AtomicTest";

    private static  int m;

    public static void test()  {

        m = 0;
long startTime = System.currentTimeMillis();
final CountDownLatch cdl = new CountDownLatch(3);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
synchronized (AtomicTest.class) {
m++;
}
}
cdl.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
synchronized (AtomicTest.class) {
m++;
}
}
cdl.countDown();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
synchronized (AtomicTest.class) {
m++;
}
}
cdl.countDown();
}
});
t1.start();
t2.start();
t3.start();
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(TAG, "test: result=" + m + " elapsedTime=" + (System.currentTimeMillis()-startTime));
}
}

synchronized

4)使用原子类进行控制

public class AtomicTest {

    private static String TAG = "AtomicTest";

    private static  AtomicInteger m;

    public static void test()  {

        m = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
final CountDownLatch cdl = new CountDownLatch(3);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m.incrementAndGet();
}
cdl.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m.incrementAndGet();
}
cdl.countDown();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
m.incrementAndGet();
}
cdl.countDown();
}
});
t1.start();
t2.start();
t3.start();
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
Log.d(TAG, "test: result=" + m + " elapsedTime=" + (System.currentTimeMillis()-startTime));
}
}

AtomicInteger

四种方式执行结果如下:

  结果 耗时
int 不正确(结果为25000-30000不等) 26ms
volatile 不正确(结果为25000-30000不等) 26ms
synchronized 正确(结果为30000) 300ms
AtomicInteger 正确(结果为30000) 35ms

a)  volatile 对性能基本没有影响

b) volatile不能保证自增操作的原子性

c) synchronized和AtomicInteger都能保证自增操作的原子性,但是AtomicInteger在性能上有绝对的优势。

参考:

http://blog.csdn.net/suifeng3051/article/details/52611233

https://www.jianshu.com/p/beb2c98003c4

https://blog.csdn.net/suifeng3051/article/details/52611310

http://www.cnblogs.com/chengxiao/p/6528109.html