java学习总结——volatile关键字(二)

一、前言

上一篇博客,我们为了解决各个线程中数据的可见性问题,添加了volatile关键字,确保了线程二对全局参数的变更,能影响到线程一的操作。同时也一起分析了volatile实现的原理和MESI协议以及MESI协议原子性操作的流程。

我们接下来一起加强volatile关键字理解

二、深入了解volatile

并发编程的三大特性:《并发编程三大特性

1、可见性
2、原子性
3、有序性

大致总结下三大特性的说明:

名称 含义
可见性 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
原子性 一个或者多个操作作为一个整体,要么全部执行,要么都不执行
有序性 程序执行的顺序按照代码的先后顺序执行

再上一篇博客《java学习总结——volatile关键字(一)》中,我们具体了解了可见性(一个线程变更了全局变量,其他线程能够识别到变更并失效掉工作内存中的数据,重新获取信息值),但网络上一直流传着这么一个说法:

volatile保证可见性和有序性,但不能保证原子性

为什么会有不能保证原子性的说法?
我们看一个demo

再这个demo中,我们使用多线程对一个全局变量进行运算操作,判断是否和正常运算的结果有出入。

public class VolatileDemo2 {
	
	public static volatile int num = 0;
	
	public static  void increase(){
		num ++;
	}
	
	public static void main(String[] args) throws InterruptedException {
		//1、创建10个线程
		Thread[] threads = new Thread[10];
		
		for (int i = 0; i < 10; i++) {
			//十个线程都调用普通方法
			threads[i] = new Thread(()->{
				for (int j = 0; j < 1000; j++) {
					//num ++ 操作执行1000次
					increase();
				}
			});
			threads[i].start();
		}
		
		//等待所有线程执行完成  才继续执行下面的代码
		for (Thread thread : threads) {
			thread.join();
		}
		
		System.out.println(num);
	}
}

多执行几次上述代码,我们看结果打印信息
在这里插入图片描述
在这里插入图片描述
统计上述运行结果,我们发现:

运行结果值 会小于等于 10000;

可见,volatile不能保证原子性。
为什么不能保证原子性,我们采取图示分析。

三、图示分析

10个线程,每个线程都能拿到数据并运算。
在这里插入图片描述

由于此时的线程一store操作经过总线,触发MESI缓存一致性协议,导致线程二中处理的结果失效,促使线程二重新获取最新的num值进行运算操作。
在多个线程操作中,store和write操作存在先后操作,且每个线程执行的速率存在差异,则会导致主内存中num数发生变更(如:num=10---->num=8),则会影响到其他线程获取新的数据时,存在问题。

所以会出现计算结果 ≤ 10000 的操作!

如何解决volatile造成的原子性问题呢?

采取 synchronized 关键字,加锁操作数据操作

四、采取 synchronized 保证 volatile 原子性

手写一个demo

public class VolatileDemo2 {
	
	public static volatile int syncnum = 0;
	
	public static synchronized void syncincrease(){
		syncnum ++;
	}
	
	public static void main(String[] args) throws InterruptedException {
		//1、创建10个线程
		Thread[] syncthreads = new Thread[10];
		
		for (int i = 0; i < 10; i++) {
			
			//十个线程都调用同步方法
			syncthreads[i] = new Thread(()->{
				for (int j = 0; j < 1000; j++) {
					syncincrease();
				}
			});
			syncthreads[i].start();
		}
		
		//等待所有线程执行完成  才继续执行下面的代码
		for (Thread syncthread : syncthreads) {
			syncthread.join();
		}
		
		System.out.println(syncnum);
	}
}

运行结果
在这里插入图片描述
为什么加了 synchronized,就能确保获取到的数据保证原子性了?
在这里插入图片描述
针对上面这个图,有朋友给我指出了synchronized 关键字修饰方法,应该不是我的这种说法,应该是在如下所示处进行数据操作加锁。
在这里插入图片描述
存在争议,所以我额外琢磨了下 synchronized关键字加在方法上修饰和加在类(代码块)上修饰的区别。

五、在方法体上和在方法体里使用synchronized的区别

我们看下面这个demo,分别是线程操作同步方法同步代码块

public class SyncDemo2 {
	public static volatile int syncnum1 = 0;
	public static volatile int syncnum2 = 0;
	//同步方法
	public static synchronized void syncincrease1(int ints) throws InterruptedException{
		System.out.println("Thread Name = "+Thread.currentThread().getName()+"  syncincrease  ints = "+ints);
		System.out.println("-->"+System.currentTimeMillis());
		syncnum1 ++;
		System.out.println(syncnum1);
		//延迟五秒  运行查看线程信息  
		Thread.sleep(5000);
	}
	//同步类
	public static void syncincrease2 (int ints) throws InterruptedException{
		System.out.println("Thread Name = "+Thread.currentThread().getName()+"  syncincrease  ints = "+ints);
		System.out.println("-->"+System.currentTimeMillis());
		synchronized(SyncDemo2.class){
			syncnum2 ++ ;
			System.out.println(syncnum2);
			//延迟五秒  运行查看线程信息  
			Thread.sleep(5000);
		}
	}
	
	public static void main(String[] args) {
		Thread[] threads = new Thread[10];
//		for (int i = 0; i < 10; i++) {
//			threads[i] = new Thread(()->{
//				for (int j = 0; j < 20; j++) {
//					try {
//						syncincrease1(j);
//					} catch (Exception e) {
//						// TODO Auto-generated catch block
//						e.printStackTrace();
//					}
//				}
//			});
//			
//			threads[i].start();
//		}
		
		for (int i = 0; i < 10; i++) {
			threads[i] = new Thread(()->{
				for (int j = 0; j < 20; j++) {
					try {
						syncincrease2(j);
					} catch (Exception e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			});
			
			threads[i].start();
		}
	}
}

运行上述代码,我们发现,当使用同步代码块时,出现如下所示的现象:
在这里插入图片描述

出现了我开辟10个线程,都能拿到处理方法。

如果我屏蔽代码块处理方式,采取同步方法处理呢?
在这里插入图片描述
看时间差距,5秒左右获取到一次方法,我设置的5秒间隔。

综上所述,我们得出以下结论:

如果放在方法体上,会阻塞后面的其他线程拿到操作方法。
如果放在代码块上,则不会阻塞其他线程拿到方法,但在数据处理上却会加锁。
上面两个操作获取的结果值是一样的!

参考资料:
在方法体上和在方法体里使用synchronized的区别

猜你喜欢

转载自blog.csdn.net/qq_38322527/article/details/103259095
今日推荐