【Java多线程】——volatile关键字

    首先我们先来看一个死循环的问题:

public class RunThread extends Thread{
    private boolean isRunning = true;
	public boolean isRunning() {
		return isRunning;
	}
	
	public void setRunning(boolean isRunning) {
		this.isRunning = isRunning;
	}
	@Override
	public void run() {
		System.out.println("进入run");
		while(isRunning == true) {
		}
		System.out.println("线程停止!");
	}
}
public class Run {
	public static void main(String args[]) {
		try {
			RunThread thread = new RunThread();
			thread.start();
			Thread.sleep(1000);
			thread.setRunning(false);
			System.out.println("赋值为false");
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
	}
}

运行上述程序,可以发现,本应输出“线程停止”这句话,但是实际的输出是如下:

/*

进入run

赋值为false

*/

一、解决不可见问题

    出现这种情况的原因是,虽然程序执行了setRunning方法将变量设置为false,但是,这个false数据被存在了公共堆栈中,而线程在工作的时候,对于数据的操作一般不直接存入公共内存,而是先存放在缓存中或者说是线程的私有堆栈中。这就导致了不可见的问题,也就是说虽然公共堆栈中的isRunning变量已经变为了false,但是由于线程的私有堆栈中的值依旧为true,所以线程会继续执行while循环而陷入无限等待。

    这就涉及到了一个可见性的问题,对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

    另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

二、非原子性

    volatile虽然可以解决不可见的问题,但是不支持原子性,下面将volatile与synchronized做一下比较:

1)volatile是线程同步的轻量级实现,它的效率要比synchronized好,并且volatile只能修饰变量而不能修饰方法、代码块。

2)多线程访问volatile不会发生阻塞,而访问synchronized则会发生阻塞。

3)voaltile保证数据可见性但不保证数据的原子性,synchronized可以保证原子性也可以间接保证可见性,因为它会将私有内存和公共内存中的数据同步。

4)再次划重点,volatile解决的是多个变量在线程之间的可见性,而synchronized解决的是多个线程访问一个互斥资源时的同步性。

    volatile关键字的非原子性操作体现在,假设多个线程共同进行一个变量的自增操作,每个线程增加10次,一共有10个线程共同完成从0到100的增加,将一个int型的变量i用volatile修饰,分别由这10个线程完成自增。但是由于i++或者是i=i+1这样的操作不是原子操作(这里解释一下,i++;这个操作实际上是三次操作,首先取出i的值,然后+1,最后将结果存入内存),想象一下,如果在一个线程取出i并+1时,还没有存入内存,另一个线程从内存中取出了原来的i值也+1,然后这两个任务各自将+1后的i存入了内存中,内存中的i实际上值增加了1,但是原本应该增加2,这就是脏读的一种。虽然volatile保证了每次取出的i都是从内存中取出的,保证了可见性,但是取不能保证操作的原子性。这时,我们可以使用synchronized代码块来限制多个线程对于变量i的访问,从而保证原子性。

三、原子类

    这里额外介绍一下Java中的原子类AtomicInteger,原子类中的一系列方法比如自增,加一个数并返回结果等等操作都是保证原子性的,原子类提供了以下方法:

 //获取当前的值
 
 public final int get()
 
 //取当前的值,并设置新的值
 
  public final int getAndSet(int newValue)
 
 //获取当前的值,并自增
 
  public final int getAndIncrement() 
 
 //获取当前的值,并自减
 
 public final int getAndDecrement()
 
 //获取当前的值,并加上预期的值
 

 public final int getAndAdd(int delta)


PS:  可以用synchronized关键字实现volatile的功能,因为synchronized会使线程在释放锁之前将对变量所做的改变刷新到主存中,从而保证可见性。于是,我们只要将上面的代码改为

public void run() {
		String str = new String();
		System.out.println("进入run");
		while(isRunning == true) {
			synchronized(str) {
			}
		}
		System.out.println("线程停止!");
	}

那么由于不可见性的原因造成的死循环就解决了。

猜你喜欢

转载自blog.csdn.net/u012198209/article/details/80275620