• Android多线程研究(8)——Java中的原子性理解


    一、什么是原子性

    原子性是世界上最小单位,具有不可分割性。比如a=0;(a非long和double类型)这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++;这个操作实际上是a=a+1;是可分割的,所以他不是一个原子操作。

    二、原子操作的作用

    非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们就称它具有原子性。java.util.concurrent.atomic包下提供了一些原子类如下:

    三、理解AtomicInteger类

    下面是AtomicInteger类中几个常用的方法:

    可能很多人和我一样存在着这样一个疑问,在AtomicInteger中是如何实现原子性的呢?有前辈已经对AtomiInteger的源码进行了分析《对 AtomicInteger 源码 的理解》我在这里仅仅是引用一下,做个记录。先来看看源码:
    	public final int getAndIncrement() {
    		for (;;) {
    			int current = get();
    			int next = current + 1;
    			if (compareAndSet(current, next))
    				return current;
    		}
    	}
    看这段代码和上面对原子性的分析,从int next  = current + 1可以看到,这里很难保证操作的原子性,重点在于compareAndSet(current, next)方法

    该函数 只有两个参数,可操作的确实三个值 ,即 value ,expect, update. 他使用了 由硬件保证其原子性的指令 CAS (compare and swap)。

    compareAndSet  函数保证了 比较,赋值这两步操作可以通过一个原子操作完成。

    然后看整个函数, 所有代码被放到了一个循环里面, 如果compareAndSet()执行失败,则说明 在int current = get(); 以后,其他线程对value进行了更新, 于是就循环一次,重新获取当前值,直到compareAndSet()执行成功为止。

    综上,getAndIncrement() 方法并不是原子操作。 只是保证了他和其他函数对 value 值得更新都是有效的。他所利用的是基于冲突检测的乐观并发策略。 可以想象,这种乐观在线程数目非常多的情况下,失败的概率会指数型增加。

    四、理解volatile修饰符

    在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 
    一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。以下例子展现了volatile的作用: 

    package com.codeing.snail.test;
    
    public class TestAtomic extends Thread {
    
    	private volatile boolean pleaseStop;
    
    	public void run() {
    		
    		while (!pleaseStop) {
    			// do some stuff...
    		}
    	}
    
    	public void tellMeToStop() {
    		pleaseStop = true;
    	}
    }
    假如pleaseStop没有被声明为volatile,线程执行run的时候检查的是自己的副本,就不能及时得知其他线程已经调用tellMeToStop()修改了pleaseStop的值。 
    Volatile一般情况下不能代替sychronized,因为volatile不能保证操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

    volatile关键字用于声明简单类型变量,如int、float、 boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的。但这有一定的限制。例如,下面的例子中的n就不是原子级别的: 

    package com.codeing.snail.test;
    
    public class TestAtomic extends Thread {
    	public static volatile int n = 0;
    
    	public void run() {
    		for (int i = 0; i < 10; i++)
    			try {
    				n = n + 1;
    				sleep(3); // 为了使运行结果更随机,延迟3毫秒
    
    			} catch (Exception e) {
    			}
    	}
    
    	public static void main(String[] args) throws Exception {
    
    		Thread threads[] = new Thread[100];
    		for (int i = 0; i < threads.length; i++)
    			// 建立100个线程
    			threads[i] = new TestAtomic();
    		for (int i = 0; i < threads.length; i++)
    			// 运行刚才建立的100个线程
    			threads[i].start();
    		for (int i = 0; i < threads.length; i++)
    			// 100个线程都执行完后继续
    			threads[i].join();
    		System.out.println(" n= " + TestAtomic.n);
    	}
    }
    如果对n的操作是原子级别的,最后输出的结果应该为n=1000,而在执行上面积代码时,很多时侯输出的n都小于1000,这说明n=n+1不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用。

    五、锁和Atomic的使用场景

    JDK的文档中说:“设计原子类主要用作各种块,用于实现非阻塞数据结构和相关基础结构类。compareAndSet()方法不是锁定的常规替换方法。仅当对象的重要更新限于单个变量时才应用它”与锁相比,另外Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代码往往比使用锁的代码更加容易出错。


  • 相关阅读:
    web 开发之酷炫--- 酷炫展示
    攻城狮的体检
    科技发烧友之智能路由
    科技发烧友之3d吉米投影
    科技发烧友之单反佳能700d中高端
    上海
    视频会议
    机器学习之信息
    filter
    centos 20T硬盘(超过16T)分区
  • 原文地址:https://www.cnblogs.com/lanzhi/p/6468797.html
Copyright © 2020-2023  润新知