IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> volatile(信号量)原理与应用解析 -> 正文阅读

[Java知识库]volatile(信号量)原理与应用解析

????????volatile是Java提供的一种轻量级的同步机制(可以理解为轻量级锁)。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized,volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差,无法保证操作的原子性。

一、volatile的特性

? ? ? ? 先讲解volatile的特性,因为volatile的原理就是对特性的解释。同时说volatile之前,我们先要了解多线程的三大特性:有序性、可见性和原子性(以前已经讲解过可以点击链接进行跳转查看)。这三点是进行多线程开发时首要考虑的问题。而volatile主要解决的就是可见性问题。volatile的俩大特性:

???1.保证可见性

????????当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的volatile变量缓存无效。

? ? ? ? 下面通过代码对比volatile可见性对程序的影响

/**
 * volatile 关键字,使一个变量在多个线程间可见
 * A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
 * 使用volatile关键字,会让所有线程都会读到变量的修改值
 * 
 * 在下面的代码中,running是存在于堆内存的t对象中
 * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
 * 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
 * 
 * 使用volatile,将会强制所有线程都去堆内存中读取running的值
 * 
 * 可以阅读这篇文章进行更深入的理解
 * http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
 * 
 * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
 * @author mashibing
 */
package com.zcm.juc;

import java.util.concurrent.TimeUnit;

public class T01_HelloVolatile {
	/*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
	void m() {
		System.out.println("m start");
		// 模拟服务器直接宕机或者人工关闭
		while(running) {
		}
		System.out.println("m end!");
	}
	
	public static void main(String[] args) {
		T01_HelloVolatile t = new T01_HelloVolatile();
		
		new Thread(t::m, "t1").start();

		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		t.running = false;
	}
	
}


? ? ? ? 注意:volatile修饰引用类型(包括数组)时,只能保证引用本身的可见性,不能保证内部数据的可见性。

/**
 * volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性
 */
package com.zcm.juc;

import java.util.concurrent.TimeUnit;

public class TestVolatileReference1 {

    boolean running = true;

    volatile static TestVolatileReference1 T = new TestVolatileReference1();


    void m() {
        System.out.println("m start");
        while(running) {
			/*
			try {
				TimeUnit.MILLISECONDS.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}*/
        }
        System.out.println("m end!");
    }

    public static void main(String[] args) {
        //lambda表达式 new Thread(new Runnable( run() {m()}
        new Thread(T::m, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 观察false是否起作用
        T.running = false;
    }
}

???2、禁止指令重排序

? ? ? ? 我们首先需要明白什么是指令重排序以及指令重排序需要遵循哪些规则?重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。同时重排序也需要遵守一定规则:

1.重排序操作不会对存在数据依赖关系的操作进行重排序。

2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。

?????????其实指令重排序主要了解实现原理以及运用就行,原理下边讲解,运用的话主要就是在单例模式中的双重检查锁(基本就是在面试中问一下,实际从来没用过,单例饿汉式就很常用)

? ? ? ? 1)双重检查单例模式

? ? ? ? 主要是因为new 一个对象的操作是非原子性的,对象的创建过程在java字节码中主要分为如下4个步骤,这4个步骤后两个有可能会重排序,1234 1243都有可能,造成未初始化完全的对象发布。volatile可以禁止指令重排序,从而避免这个问题。

1、申请内存空间,

2、初始化默认值(区别于构造器方法的初始化),

3、执行构造器方法

4、连接引用和实例。

public class Singleton{   
    // 静态属性,volatile保证可见性和禁止指令重排序,这里主要是禁止重排序
    private volatile static Singleton instance = null; 
    // 私有化构造器  
    private Singleton(){}   

    public static  Singleton getInstance(){   
        // 第一重检查锁定,减少线程对同步锁锁的竞争
        if(instance==null){  
            // 同步锁定代码块 
            synchronized(Singleton.class){
                // 第二重检查锁定,保证单例
                if(instance==null){
                    // 注意:非原子操作
                    instance=new Singleton(); 
                }
            }              
        }   
        return instance;   
    }   
}

? ?3. 不保证原子性

????????volatile是不保证原子性,这是要注意的一点,就是在进行一些复合操作的时候要特别注意,比如;count++操作。代码对比volatile和synchronized对多线程原子性的影响:

/**
 * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
 * 运行下面的程序,并分析结果
 */
package com.zcm.juc;

import java.util.ArrayList;
import java.util.List;

public class T04_VolatileNotSync {
	volatile int count = 0;
	//volatile保证了count的可见性,但是无法保证count++的原子性,比如:线程1修改count为1,线程2,线程3都读取到count=1,同时修改此时少++一次
	void m() {
		for(int i=0; i<10000; i++) count++;
	}
	
	public static void main(String[] args) {
		T04_VolatileNotSync t = new T04_VolatileNotSync();
		
		List<Thread> threads = new ArrayList<Thread>();
		
		for(int i=0; i<10; i++) {
			threads.add(new Thread(t::m, "thread-"+i));
		}
		
		threads.forEach((o)->o.start());
		
		threads.forEach((o)->{
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});
		
		System.out.println(t.count);
		
		
	}
	
}


?

/**
 * 对比上一个程序,可以用synchronized解决,synchronized可以保证可见性和原子性,volatile只能保证可见性
 */
package com.zcm.juc;

import java.util.ArrayList;
import java.util.List;


public class T05_VolatileVsSync {
	/*volatile*/ int count = 0;

	synchronized void m() { 
		for (int i = 0; i < 10000; i++)
			count++;
	}

	public static void main(String[] args) {
		T05_VolatileVsSync t = new T05_VolatileVsSync();

		List<Thread> threads = new ArrayList<Thread>();

		for (int i = 0; i < 10; i++) {
			threads.add(new Thread(t::m, "thread-" + i));
		}

		threads.forEach((o) -> o.start());

		threads.forEach((o) -> {
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});

		System.out.println(t.count);

	}

}

?二、volatile的原理

? ? ? ? 可以从俩方面理解:一:可见性方面,使用的是缓存一致性协议;二:禁止指令重排序,从JVM底层进行理解。

?1.可见性

????????为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。此时为了保证缓存数据的一致性,就要引入缓存一致性协议了。如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。

????????缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

???????

?

? ? 2.禁止指令重排序

?????????在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现(直接使用反编译工具,查看反编译代码),加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

(2)它会强制将对缓存的修改操作立即写入主存;

(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

? ? ? ?

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-09-01 11:47:38  更:2021-09-01 11:48:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 12:55:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码