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

人生之路不会是一帆风顺的,我们会遇上顺境,也会遇上逆境,在所有成功路上折磨你的,背后都隐藏着激励你奋发向上的动机,人生没有如果,只有后果与结果,成熟,就是用微笑来面对一切小事。

导读:本篇文章讲解 Java并发中的可见性和原子性,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

目录

一、可见性

        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执行读操作
	}
}

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

我们在单线程中,对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执行读操作
	}
}

 

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

 可以看到,虽然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执行读操作
	}
}

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

 

可以看到,程序很快就结束了,说明新线程已经读到了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执行读操作
	}
}

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

 

 

二、原子性 

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

        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执行读操作
	}
}

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

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

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

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

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

这就要探究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执行读操作
	}
}

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

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

 

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

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

其实它的内部也使用了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 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/125030.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!