Java内存模型与线程(二)

本文探讨了Java中的volatile关键字,它提供了一种轻量级的同步机制。volatile保证了变量对所有线程的可见性以及禁止指令重排序,但不保证原子性。文章指出,在某些特定情况下,volatile可以作为线程安全的解决方案,但在涉及变量运算的并发场景下,仍需要锁来确保原子性。此外,volatile的使用还可能导致性能上的轻微下降,但通常优于锁的性能。
摘要由CSDN通过智能技术生成

时间记录:2020-2-1
上章节了解到了关于内存的一些操作和简单的规则,在其中volatile属于一个比较特殊的内容的操作,而也存在一些特殊的变量的内存操作的特殊性,long,double的操作的特殊性,但是其表现没有什么差异。

Volatile型变量的特殊规则
在java中volatile为一个关键字,是一种轻量级的同步机制,但是其在一些的特定场景下的操作比较合适,不是完全的同步,其只是对所有线程是可见的,其实际上的意义是线程在使用被其修饰的变量的时候一定是先从主内存中进行同步,然后再进行使用。

volatile的几个特性
1:被此修饰的变量对所有线程的可见性,这里的可见性是指当一条线程修改看这个变量的值,新值对于其他线程来说是可以立即得知的。(注意:这里的立即得知其实是对变量的使用定下了规则,必须从主内存中进行获取)
2:被此修饰的变量在发生改变的时候得马上返回到主内存,也就是进行同步的操作
3:禁止指令排序优化(这个在jdk1.5中才完全被修复,关于被修饰的禁止指令排序优化)
指令重排序 是指cpu采用了允许将多条指令不安程序规定的顺序分开发送个各相应电路单元出路(这里应该是指集成的寄存器,个人猜测)

常见的误解
【volatile变量对所有线程是立即可见的,对volatile变量所有的写操作都能立刻反应到其它线程之中,换句话说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的】
在各个线程的工作内存中,volatile变量可以存在不一致的情况,但是由于每次使用之前都要进行刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题,但是java里面的运算并不是一致性的,导致了volatil变量在并发下一样是不安全的。

package com.huo.mem;

public class VolatileTest 
{
	public static volatile int race = 0;
	
	public static void increase() {
		race ++;
	}
	
	private static final int THREAD_COUNT = 20;
	
	public static void main(String[] args)
	{
		Thread[] threads = new Thread[THREAD_COUNT];
		for(Thread thread : threads)
		{
			thread = new Thread(new Runnable()
			{
				@Override
				public void run() 
				{
					for(int i = 0;i<1000;i++)
					{
						increase();
					}
				}
			});
			thread.start();
		}
		while(Thread.activeCount() > 1)
		{
			Thread.yield();
		}
		System.out.println(race);
	}
}

结果为:

18291

并不是我们所期望的那样为200000,所以volatile并不是线程安全的,这里因为在执行increase操作的时候,通过指令将race 对应的值读取到栈顶的时候,这个时候是没有错的,但是其他线程可能已经将值改变了并且推送回到主内存中了,但是这个操作是使用的之前的值,然后此又将值同步回主内存,这个时候就会导致,值不是我们所期望的那个样子了。

从上我们可以知道了,volatile变量只能保证可见性,在不符合其的运算场景下还是需要使用锁的方式来保证其的原子性。
1:运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
2:变量不需要与其他的状态变量共同参与不变约束。

例如下面这种的:利用volatile变量来控制其他的线程操作

package com.huo.mem;

public class VolatileTest2 
{
	public volatile boolean shutDown;
	
	public void shutDown()
	{
		shutDown =  true;
	}
	
	public void doWork()
	{
		while(!shutDown)
		{
			System.out.println("ThreadID  "+Thread.currentThread().getId()+" "+System.currentTimeMillis());
		}
	}
	
	public VolatileTest2() 
	{
		Thread[] threads = new Thread[20];
		for(Thread thread : threads)
		{
			thread = new Thread(new Runnable()
			{
				@Override
				public void run() 
				{
					doWork();
				}
			});
			thread.start();
		}
		
		try 
		{
			Thread.sleep(2000);
			shutDown();
		} catch (Exception e) 
		{
		}
	}
	public static void main(String[] args) 
	{
		new VolatileTest2();
	}
}

指令重排序:
普通变量只能保证在执行的过程中,所有依赖赋值结果的地方都能得到正确的结果,而不能保证变量赋值操作的顺序与程序代码中执行顺序一致。这个就是在java内存模型中描述的所谓的“线程内表现为串行语义”
指令冲排序优化就是指在一些的操作中会将其的汇编代码提前执行。这样就导致了一个问题,有些值是要被其他线程阻塞使用的,就是导致了问题,比如A线程中要处理一个操作并产生结果,然后使用一个boolean的标志位来告诉B线程我弄好了,你可以使用A处理完的结果来做一些事,但是实际上A并没有做完,这样就会导致出问题。

其中最为经典的就是懒汉模式中的指令重排问题了。

package com.huo.mem;

public class Singleton 
{
	private volatile static Singleton instance;
	
	public static Singleton getInstance()
	{
		if(instance == null)
		{
			synchronized (Singleton.class) {
				if(instance == null)
				{
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
	
	public static void main(String[] args)
	{
		Singleton.getInstance();
	}
}

以上为一个标准的DCL单例(双锁检测)
其中的new操作并不是一条指令解决的,其中分为了三部操作,一个是申请内存也就是allocate的操作,还有就是初始化这个内存中的数据,然后将指针指向这个内存操作,但是应为存在指令重排的问题,会导致先指向了这个内存,然后就没有写入初始化的数据,但是我们认为已经初始化好了,然后就会出现没有被初始化的操作,所以单例在多线程中是存在问题的。

而在使用volatile变量的时候添加了一个**lock addl $0x0,(%esp)**的指令,不太懂这个,大致的意思就是添加一个标志,让后面的指令不能够排到这个位置前面。此部分以后可以了解下

性能影响
在添加了这个volatile修饰后对性能有没有什么影响呢
1:volatile的同步机制性能优于锁的
2:volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作会慢一点,优于之前插入的那个指令导致的

总结: 在使用volatile的时候要满足其的场景,否则造成不必要的浪费。

时间记录:2020-2-2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值