Java并发中的可见性和原子性

16 篇文章 1 订阅

目录

一、可见性

        1、实例讲解 

        2、如何理解Java线程中的不可见性?

        3、那么如何实现可见性呢?

二、原子性 

        1、实例讲解 

        2、a++的本质

        3、使用Atomicxxxx保证原子性:

        4、使用synchronized同步代码段强制实现原子性和可见性


 

一、可见性

        1、实例讲解 

先看这样一段代码:

public class Test {

	static boolean a = true;
	
	public static void main(String[] args) {
		a = false;					//对a执行写操作
		System.out.println(a); 		//对a执行读操作
	}
}

 

我们在单线程中,对a执行了写操作,并且读取到了最新写的值,也就是说,单线程中对a的写操作时可见的。

那么我们再开启一个线程 :

public class Test {

	static boolean a = true;
	
	public static void main(String[] args) throws InterruptedException {
		new Thread(()-> {
			while(a) {}	//死循环
		}).start();
		
		Thread.sleep(1000);	//为了保证不会影响,停一秒再写		
		a = false;					//对a执行写操作
		System.out.println(a); 		//对a执行读操作
	}
}

 

 

 可以看到,虽然a仍然打印出为false,但是程序没有结束,就说明在我们新开启的线程中a的值始终为true,他才可以一直执行while循环。换句话说,我们的主线程对a的写操作对于新开的线程的读操作来说是不可见的。

为什么这么长时间了,新开线程中的a还是true呢?那是因为新线程中一直在执行循环,使得线程没有机会去拿到主存中a的最新值,而是一直读取缓存中a的值。

那么,我们让循环沉睡一会儿,给他去读最新值的机会:

public class Test {

	static boolean a = true;
	
	public static void main(String[] args) throws InterruptedException {
		new Thread(()-> {
			while(a) {
				try {
					Thread.sleep(1);	//睡1ms,给线程去读新值的机会
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}	//死循环
		}).start();
		
		Thread.sleep(1000);	//为了保证不会影响,停一秒再写		
		a = false;					//对a执行写操作
		System.out.println(a); 		//对a执行读操作
	}
}

 

 

可以看到,程序很快就结束了,说明新线程已经读到了a的新值为false,结束了循环。 

 

        2、如何理解Java线程中的不可见性?

        简单来说:线程1读,线程2写,而线程1读不到线程2写的值,这就是不可见性。

        3、那么如何实现可见性呢?

就需要用到volatile关键字了:

public class Test {

	static volatile boolean a = true;
	
	public static void main(String[] args) throws InterruptedException {
		new Thread(()-> {
			while(a) {}	//死循环
		}).start();
		
		Thread.sleep(1000);	//为了保证不会影响,停一秒再写		
		a = false;					//对a执行写操作
		System.out.println(a); 		//对a执行读操作
	}
}

 

 

 

二、原子性 

        保证可见性可以保证一个线程写之后,另一个线程可以读到。 
       那假如一个进程既读取变量,又依赖读到的变量进行写操作呢?我们来看下面的例子 :

        1、实例讲解 

        用两个线程分别执行10000次a++的操作,按道理来说,a的结果应该会增加20000 

public class Test {

	static int a = 0;
	
	public static void main(String[] args) throws InterruptedException {
		
		for(int i=0; i<10000; i++) {
			new Thread(()-> {
				a++;
			}).start();
			new Thread(()-> {
				a++;
			}).start();
		}		
		
		Thread.sleep(1000);	//为了保证不会影响,停一秒再写		
		System.out.println(a); 		//对a执行读操作
	}
}

 

可以看到,结果和我们的预期对不上,那我们加上volatile关键字试一下: 

 

 

结果还是对不上,这是为什么呢?

这就要探究a++的本质了 

        2、a++的本质

        a++可以拆分为三个操作:1、读取a; 2、a+1; 3、将加之后的值赋给a 

        有可能会出现这种情况:
        1、当a=0时,线程1读取a值,线程2也读取a值;
        2、线程1将它读到的a值+1,此时为1,线程2也将它读到的a值+1,此时为1;
        3、线程1将1这个值刷入主存,此时主存中的a=1;线程2也将1这个值刷入主存,此时为1

        这显然是不对的,两个线程各执行一次a++,a的值应该+2才对。刚刚我们得到的值为19998,可能就是有两个线程在其他线程+之前读取到了a值。

        那加上volatile关键字之后呢?

        volatile关键字只能保证可见性,即一个线程写过之后,另一个线程能够立马读到。而假如线程2操作在线程1操作写之前就已经读了,那还是没办法改变这个情况。这两个线程都需要读取主存的值,并且每个线程都依赖自己读取的值进行写操作。这就需要保证原子性了。

        

        3、使用Atomicxxxx保证原子性:

public class Test {

	static AtomicInteger a = new AtomicInteger(0);
	
	public static void main(String[] args) throws InterruptedException {
		
		for(int i=0; i<10000; i++) {
			new Thread(()-> {
				a.getAndAdd(1);
			}).start();
			new Thread(()-> {
				a.getAndAdd(1);
			}).start();
		}		
		
		Thread.sleep(1000);	//为了保证不会影响,停一秒再写		
		System.out.println(a); 		//对a执行读操作
	}
}

 

        可以看到,a++的操作改成了getAndAdd(),读和写是一起执行的,这就不会在读值之后写值之前被其他线程插一杠子。 

 

        需要注意的一点是,原子性和可见性并不是相互独立的,保证原子性的前提是保证可见性,那为什么我们没有再用volatile修饰a来保证可见性呢?这就需要去看看AtomicInteger的源码了:

其实它的内部也使用了volatile关键字。 

        4、使用synchronized同步代码段强制实现原子性和可见性

除了Atomic,也可以使用synchronized同步代码段强制实现原子性。

public class Test {

	static AtomicInteger a = new AtomicInteger(0);
	static int b = 0;
	
	public static void main(String[] args) throws InterruptedException {
		
		for(int i=0; i<10000; i++) {
			new Thread(()-> {
//				a.getAndAdd(1);
				synchronized(Test.class) {
					b++;
				}
			}).start();
			new Thread(()-> {
//				a.getAndAdd(1);
				synchronized(Test.class) {
					b++;
				}
			}).start();
		}		
		
		Thread.sleep(1000);	//为了保证不会影响,停一秒再写		
//		System.out.println(a); 		//对a执行读操作
		System.out.println(b); 		//对b执行读操作
	}
}

        相比较Atomic,synchronized就更加重量级了。

        另外:volatile不具有传染性,用volatile修饰的对象的内部属性不具有可见性,反之用volatile修饰的内部属性也不能保证所在对象的可见性。

参考:【java】并发之可见性与原子性_哔哩哔哩_bilibili 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值