JAVA并发编程(二)volatile与synchronized

时间:2021-03-27 20:50:28

在java多线程知识中有很多关键字,常用的就是volatile和synchronized两个,这两个关键字分别保证了字段数据的可见性临界区的同步性。这篇文章对所有关于此两个关键字的作用和用法作一个总结。

数据的可见性


什么叫做数据的可见性?

想这样一种情况,线程A对共享属性a进行修改之后,线程B读取属性a,但是读取到的是A修改之前的旧数据,发生了错误。这种错误的发生不是因为线程B和线程A并行造成的,线程A完成了所有操作之后,线程B读到的属性a仍然是修改前的旧数据。发生这种问题是因为我们没有保证属性a具有良好的可见性。

为什么会发生可见性问题?

听了上面的例子你可能会感觉很魔幻,你会反驳我:“这种情况根本不可能出现”。诚然我们平时的小程序很少出现这种情况,但是不能确保一个庞大的系统不会发生这种问题,而且这种问题的出现不是我们主观臆断的,是有存在的理由的。

如果你熟悉JVM的运行时数据区,你会记得在JVM的内存堆中有这样一块区域叫做TLAB,全程是Thread Local Allocation Buffer即多线程私有分配缓冲区。这个区域分配给各个线程作为私有存储空间,当线程创建对象需要分配内存时直接在TLAB中给出空间,这样的好处在于会更快速,因为堆中的数据存放是很复杂的,TLAB能够解决分配速度的问题。但是可见性的问题也出现了,各个线程都需要使用的数据存放在公共的堆空间中,线程需要读取数据a时到公共堆空间中去读取数据a,把a读取到自己的缓冲区中进行处理,当线程将a读取到缓冲区中时,有可能公共空间中的a已经发生了变化,这样导致线程读到的数据是旧数据。

volatile


volatile能做什么?

为了解决这个问题volatile应运而生,volatile使得我们读取数据时直接到公共空间的读取最新的数据,并直接将数据写入到自己的线程的程序中,省去了中间的缓冲区的部分,用这种方法确保数据的可见性。当一个属性被volatile修饰时,我们也就说:这个属性具有可见性,对于任何线程操作都能保证获取这个属性是最新值。

volatile不能保证操作的原子性

我们解释了可见性的概念,但这和原子性不同。原子性主要指于多步操作不会被其他线程打断或干扰的操作。比如i++,自增的操作分成三个步骤。实际上一个线程在执行这三个步骤的时候,是可能被其他线程干扰的,所以i++不是原子性操作。我们不能用volatile修饰i来希望i++变为原子操作。这根本不是一个概念。

volatile只能保证那些关于它修饰的属性的操作不会被重排序,保持此属性的可见性。这是一种轻量级的同步概念,但是volatile能做的太少了,实际上我们也不必为每个处于多线程共享的属性都加上volatile,对于那些我们必须要保证可见性的操作而言volatile是需要的,比如:

package Main;

/**
* Title: NoVisibility
* Description:
* Company: www.QuinnNorris.com
*
* @date: 2018/1/24 下午5:32 星期三
* @author: quinn_norris
* @version: 1.0
*/

public class NoVisibility {

private volatile static boolean flag;
private static int number;

public static void main(String[] args) {
new SubThread().start();
number = 11;
flag = true;
}

private static class SubThread extends Thread {

@Override
public void run() {
while (!flag)
Thread.yield();
System.out.println(number);
}
}
}

这段代码中flag作为条件判断的属性就有必要加上volatile修饰,保证当它发生变化的时候能够在其他线程及时跳出while循环。

synchronized


java是一门面向对象的编程语言,所以这样说起来synchronized看起来就有些奇怪,因为他是加在代码段上使用的,这就给我们一种面向过程编程的感觉,实际上并不是这样的,synchronized的使用方法也有很多说法在里面。

synchronized一共有四种用法:

  1. 修饰代码段
  2. 修饰方法
  3. 修饰静态方法
  4. 修饰类

synchronized修饰代码段

synchronized修饰代码段时表示在代码段内部的部分是临界区,一次只能被同一个类的一个线程访问

/**
* Title: SyncThread
* Description:
* Company: www.QuinnNorris.com
*
* @date: 2018/1/24 下午5:32 星期三
* @author: quinn_norris
* @version: 1.0
*/

class SyncThread implements Runnable {
private static int count;

public void run() {
synchronized (this) {
//todo
}
}

}

上面的代码中,todo的内容在同一时间在同样的一个对象中只能被一个线程执行。请注意,上面的条件很重要,同一时间是指在一个时间点上,同一对象是指不同的线程用同一个SyncThread对象进行运行时才会起作用,如果不同的线程创建了不同的SyncThread对象,那么syncrhonized并不会起到作用。

synchronized修饰方法

修饰方法和修饰代码段很类似,就是将关键字写在方法名前,并且临界区是整个方法,使用这种方法虽然简单方便,但是需要注意,这样写让整个方法都变成临界区很可能会让效率急剧降低,你的并行程序会慢慢变成“串行程序”,这是我们不愿见到的。

/**
* Title: SyncThread
* Description:
* Company: www.QuinnNorris.com
*
* @date: 2018/1/24 下午5:32 星期三
* @author: quinn_norris
* @version: 1.0
*/

class SyncThread implements Runnable {
private static int count;

public synchronized void run() {
//todo
}

}

synchronized修饰静态方法

/**
* Title: SyncThread
* Description:
* Company: www.QuinnNorris.com
*
* @date: 2018/1/24 下午5:32 星期三
* @author: quinn_norris
* @version: 1.0
*/

class SyncThread implements Runnable {
private static int count;

public synchronized static void abs() {
//todo
}

@Override
public void run() {

}
}

修饰静态的方法和修饰方法类似,只不过锁的对象是所有此类创建的对象。

synchronized修饰类

修饰类和修饰静态方法是完全一样的,只不过换了一种写法,修饰类和修饰静态方法的关系可以看作是,修饰代码段和修饰方法之间的关系。只不过前两者是锁类的所有的对象,而后两者是锁一个实例化的对象。

/**
* Title: SyncThread
* Description:
* Company: www.QuinnNorris.com
*
* @date: 2018/1/24 下午5:32 星期三
* @author: quinn_norris
* @version: 1.0
*/

class SyncThread implements Runnable {
private static int count;

public void method() {
synchronized(SyncThread.class) {
// todo
}
}
@Override
public void run() {

}
}