精华内容
下载资源
问答
  • Volatile关键字与原子性操作

    千次阅读 2021-02-09 10:48:44
    目录什么是原子性操作什么是Volatile关键字为什么需要原子性操作Volatile能不能实现原子性操作?Volatile关键字常用的场景CUDA编程实现原子性操作的方法 什么是原子性操作 原子性操作具有不可分割性。比如 i=0 这个...

    什么是原子性操作

    原子性操作具有不可分割性。比如 i=0 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:

    i++

    这个操作实际是

    i = i + 1

    是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。

    那么x=y;这个操作是不是原子性操作呢?答案也是否定的,该操作可以向下细分为首先读取y的数据,再赋值给x的过程。

    注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。

    什么是Volatile关键字

    volatile 关键字是一种类型修饰符,被它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法如下:

     int volatile vInt;
    

    为什么需要原子性操作

    计算机在执行程序时,每条指令都是在CPU中执行的,而在执行指令过程中,会涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

    也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

    而当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

    同样针对i++; 操作。当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

    这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。

    当同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。在实际运行过程中可能存在下面一种情况:最初,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

    最终结果i的值是1,而不是2。这就导致了结果的出错。通常称这种被多个线程访问的变量为共享变量。

    也就是说,如果一个变量在多个CPU中都存在缓存(主要当多线程时),那么就可能存在缓存不一致的问题。

    针对这类问题从硬件角度讲可以通过在总线加LOCK锁或者通过缓存一致性协议的方式实现。软件方面不同的语言存在着不同的操作。

    Volatile能不能实现原子性操作?

    在并发编程中,多进程的一个共享变量被volatile修饰之后,那么就具备了两层语义:

    1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
    2)禁止进行指令重排序。(保证指令的有序性)

    通过例子来说明第一层语义,假设线程1先执行,线程2后执行:

    //线程1
    boolean stop = false;
    while(!stop){
        doSomething();
    }
    
    //线程2
    stop = true;
    

    针对这个程序存在以下的问题:每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

    但是用volatile修饰之后就变得不一样了:

    首先使用volatile关键字会强制将修改的值立即写入主存;

    之后使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

    最后由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

    那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

    那么线程1读取到的就是最新的正确的值。

    但是Volatile关键字仍然不能保证原子性。

    问题在于volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

    在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致缓存不同步的情况出现。

    Volatile关键字常用的场景

    通常volatile关键字被用于以下的几个地方:
    1、中断服务程序中修改的供其它部分程序检测的共享变量。
    2、多任务环境下各个任务的共享标志应该加volatile。
    3、存储器的硬件寄存器需要加volaile说明,因为对它的每次读写都有不同的含义。

    CUDA编程实现原子性操作的方法

    在这里简单的提供一个使用倒原子性操作的例程,并记录一些注意事项。

    统计计算一组数据并作出直方图:

    CPU版本:

    #define SIZE (100*1024*1024)
    int main(void){
        unsigned char *buffer = (unsigned char*)big_random_block(SIZE);
        unsigned int histo[256];
        for(int i=0;i<256;i++)
            histo[i]=0;
        for(itn i=0;i<SIZE;i++)
            histo[buffer[i]]++;
    

    atomicAdd(addr,y ) 将生成一个原子操作序列,这个操作序列包括读取地址 addr 处的值,将 y 增加到这个值,以及将结果保存回地址 addr. 底层硬件将保证执行这些操作时,任何其他的线程都不会读取写入 addr 上的值。

    CPU版本的kernel函数部分:

    __global__ void histo_kernel(unsigned char *buffer,long size,unsigned int *histo){
        int i = threadIdx.x + blockIdx.x*blockDim.x;
        int stride = blockDim.x * gridDim.x;
        while(x < size){
            atomicAdd(&(histo[buffer[i]]),1);
            i += stride;
        }
    }
    

    这个方法比 cpu 还要慢,因为几千个线程访问少量的内存,将发生大量的竞争,为了确保递增操作的原子性,对相同内存位置的操作都将被硬件串行化。

    使用共享内存原子操作和全局内存原子操作的直方图kernel函数

    __global__ void histo_kernel(char *buffer,long size,unsinged int *histo){
        int i= threadIdx.x +blockDim.x * blockIdx.x;
        int stride = gridDim.x * blockDim.x;
        __share__ unsigned int temp[256];
        temp[threadIdx.x]=0;
        __syncthreads();
        while(i<size){
            atomicAdd(&(temp[buffer[i]]),1);
            i += stride;
        }
        __syncthreads();
        //最后将所用共享内存上的直方图相加到一个直方图上
        atomicAdd(&(histo[threadIdx.x]),temp[threadIdx.x]);
    }
    

    虽然最初将直方图程序修改为GPU版本使性能下降,但同时使用共享内存与原子性操作仍然能使程序加速。实测程序速度提升了大概7倍左右。

    展开全文
  • Java多线程之原子性操作

    千次阅读 2018-03-09 21:59:48
    何谓原子性操作,即为最小的操作单元,比如i=1,就是一个原子性操作,这个过程只涉及一个赋值操作。又如i++就不是一个原子操作,它相当于语句i=i+1;这里包括读取i,i+1,结果写入内存三个操作单元。因此如果操作不...

            何谓原子性操作,即为最小的操作单元,比如i=1,就是一个原子性操作,这个过程只涉及一个赋值操作。又如i++就不是一个原子操作,它相当于语句i=i+1;这里包括读取i,i+1,结果写入内存三个操作单元。因此如果操作不符合原子性操作,那么整个语句的执行就会出现混乱,导致出现错误的结果,从而导致线程安全问题。因此,在多线程中需要保证线程安全问题,就应该保证操作的原子性,那么如何保证操作的原子性呢?其一当然是加锁,这可以保证线程的原子性,比如使用synchronized代码块保证线程的同步,从而保证多线程的原子性。但是加锁的话,就会使开销比较大。另外,可以使用J.U.C下的atomic来实现原子操作。接下来我们就通过对比来说明atomic实现原子操作的功能。

    1.非原子操作

            首先,了解一下若不保证原子操作会出现什么样的情况。代码如下:

    package concurrent;
    import java.util.concurrent.*;
    public class GeneralTest {
        private static final ExecutorService es=Executors.newFixedThreadPool(10);
        private static int count=0;
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		for(int i=0;i<20;i++)
    		{
    			Thread t1=new Thread(){
    				public void run(){
    					
    					count+=1;
    				}
    			};
    			es.execute(t1);
    		}
    		for(int i=0;i<5;i++)
    			System.out.println("累加20次,得到结果是:"+count);
    
    	}
    
    }
    

            以上就是创建20个线程进行对count这个类变量自增一次,预期的值应该是20,但是运行上述程序出现如下结果:

    累加20次,得到结果是:16
    累加20次,得到结果是:19
    累加20次,得到结果是:19
    累加20次,得到结果是:19
    累加20次,得到结果是:19
    

            从以上结果可以看出,输出五次的结果都是没有达到20,可能出现的问题便是count+=1包含了三个操作,可能这个线程读取count的时候,上一个线程还没把更新的count值写入内存,这就是因无法保证操作的原子性而导致的线程安全问题。

    2.通过synchronized加锁

            synchronized称为互斥锁,它的工作原理就是一旦一个线程抢占了得到锁,其它线程便进入等待状态,只有当该线程释放锁,其它线程才有获取锁的机会。因此使用synchronized对count加锁,那么实现的操作便是原子性操作,因为只有当一个线程完成所有操作,然后释放锁,其它线程才会抢占锁。以下还是自增问题,当一个线程在实现++count时,只有当该线程将数据存入内存,释放锁之后,其它内存才会获取锁,读取数据,因此不会出现干涉问题,从而避免线程安全问题。

    package concurrent;
    import java.util.concurrent.*;
    public class GeneralTest {
        private static final ExecutorService es=Executors.newFixedThreadPool(20);
        private static Integer count=0;
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		long start=System.currentTimeMillis();
    		for(int i=0;i<1000;i++)
    		{
    			Thread t1=new Thread(){
    				public void run(){
    					
    					//count+=1;
    					synchronized(count)
    					{
    						System.out.println(Thread.currentThread().getName()+"实现了自增一次,结果为:"+(++count));
    					}
    				}
    			};
    			
    			es.execute(t1);
    		}
    		try
    		{
    			Thread.sleep(5000);
    		}
    		catch(InterruptedException e)
    		{
    			e.printStackTrace();
    		}
    		System.out.println("计算过程的耗时为:"+(System.currentTimeMillis()-start-5000));
    		System.out.println("累加1000次,得到结果"+count);
    	}
    
    }
    

            运行以上程序,得到如下结果,由于累加1000次太多,所以不把每次加的结果显示出来,只显示一部分过程以及最终的结果。

    pool-1-thread-8实现了自增一次,结果为:999
    pool-1-thread-8实现了自增一次,结果为:1000
    pool-1-thread-2实现了自增一次,结果为:679
    pool-1-thread-14实现了自增一次,结果为:678
    pool-1-thread-3实现了自增一次,结果为:677
    pool-1-thread-1实现了自增一次,结果为:675
    pool-1-thread-19实现了自增一次,结果为:674
    pool-1-thread-13实现了自增一次,结果为:673
    pool-1-thread-10实现了自增一次,结果为:882
    pool-1-thread-7实现了自增一次,结果为:881
    pool-1-thread-17实现了自增一次,结果为:715
    pool-1-thread-9实现了自增一次,结果为:714
    pool-1-thread-5实现了自增一次,结果为:713
    pool-1-thread-11实现了自增一次,结果为:712
    pool-1-thread-20实现了自增一次,结果为:706
    pool-1-thread-18实现了自增一次,结果为:698
    pool-1-thread-4实现了自增一次,结果为:697
    pool-1-thread-16实现了自增一次,结果为:696
    pool-1-thread-15实现了自增一次,结果为:695
    pool-1-thread-6实现了自增一次,结果为:694
    pool-1-thread-12实现了自增一次,结果为:693
    计算过程的耗时为:13
    累加1000次,得到结果1000
    

            从上面可以看出,结果和预期一样,并且计算过程花费了13ms。

    3.atomic实现

            atomic系列的类在是J.U.C包下的一系列类。它主要包括四类:基本类型,数组类型,属性原子修改器类型,引用类型。

    基本类型的类主要包括AtomicInteger、AtomicLong、AtomicBoolean等;数组类型主要包括AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;属性原子修改器类型主要包括AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater ;引用类型主要包括AtomicReference、AtomicStampedRerence、AtomicMarkableReference。

    1.基本类型的实现:

    package concurrent;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicInteger;
    public class AtomicTest {
        private static final AtomicInteger at=new AtomicInteger();
        private static final ExecutorService es=Executors.newFixedThreadPool(20);
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		long start=System.currentTimeMillis();
    		for(int i=0;i<1000;i++)
    		{
    			Thread t1=new Thread() {
    				public void run()
    				{
    					System.out.println(Thread.currentThread().getName()+"实现了一次自增原子操作,结果为:"+at.incrementAndGet());
    				}
    			};
    			es.execute(t1);
    		}
    		try
    		{
    			Thread.sleep(5000);
    		}
    		catch(InterruptedException e)
    		{
    			e.printStackTrace();
    		}
    		System.out.println("计算过程的耗时为:"+(System.currentTimeMillis()-start-5000));
    		System.out.println("累加1000次,得到结果"+at);
    		
    
    	}
    
    }
    

    运行结果如下:

    pool-1-thread-13实现了一次自增原子操作,结果为:984
    pool-1-thread-8实现了一次自增原子操作,结果为:983
    pool-1-thread-14实现了一次自增原子操作,结果为:982
    pool-1-thread-20实现了一次自增原子操作,结果为:981
    pool-1-thread-10实现了一次自增原子操作,结果为:980
    pool-1-thread-12实现了一次自增原子操作,结果为:979
    pool-1-thread-3实现了一次自增原子操作,结果为:978
    pool-1-thread-7实现了一次自增原子操作,结果为:977
    pool-1-thread-4实现了一次自增原子操作,结果为:976
    pool-1-thread-18实现了一次自增原子操作,结果为:1000
    pool-1-thread-9实现了一次自增原子操作,结果为:999
    pool-1-thread-17实现了一次自增原子操作,结果为:998
    pool-1-thread-6实现了一次自增原子操作,结果为:997
    pool-1-thread-5实现了一次自增原子操作,结果为:996
    pool-1-thread-2实现了一次自增原子操作,结果为:995
    计算过程的耗时为:9
    累加1000次,得到结果1000
    

            由上面可知使用基本类型的原子操作类进行数字的自增,不仅可以保证操作操作的原子性,而且相对来说花费的时间代价比使用synchronized加锁的时间代价要小。

    2.数组类型

            以下以AtomicIntegerArray为例通过对一个数组内的每个元素进行自增计算,从而来介绍数组类型的原子类的运用。

    package concurrent;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicIntegerArray;
    public class AtomicTest {
        private static final AtomicIntegerArray at=new AtomicIntegerArray(new int[5]);
        private static final ExecutorService es=Executors.newFixedThreadPool(20);
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		long start=System.currentTimeMillis();
    		for(int i=0;i<1000;i++)
    		{
    			Thread t1=new Thread() {
    				public void run()
    				{
    					for(int i=0;i<5;i++)
    						System.out.println(Thread.currentThread().getName()+"实现了对第"+(i+1)+"个元素一次自增原子操作,结果为:"+at.incrementAndGet(i));
    				}
    			};
    			es.execute(t1);
    		}
    		try
    		{
    			Thread.sleep(5000);
    		}
    		catch(InterruptedException e)
    		{
    			e.printStackTrace();
    		}
    		System.out.println("计算过程的耗时为:"+(System.currentTimeMillis()-start-5000));
    		for(int i=0;i<5;i++)
    			System.out.println("第"+(i+1)+"个元素累加1000次,得到结果"+at.get(i));
    		
    
    	}
    
    }
    

            运行程序结果如下,其中运算过程部分给出:

    pool-1-thread-3实现了对第5个元素一次自增原子操作,结果为:980
    pool-1-thread-20实现了对第1个元素一次自增原子操作,结果为:989
    pool-1-thread-20实现了对第2个元素一次自增原子操作,结果为:1000
    pool-1-thread-20实现了对第3个元素一次自增原子操作,结果为:1000
    pool-1-thread-20实现了对第4个元素一次自增原子操作,结果为:1000
    pool-1-thread-20实现了对第5个元素一次自增原子操作,结果为:1000
    pool-1-thread-15实现了对第5个元素一次自增原子操作,结果为:998
    计算过程的耗时为:9
    第1个元素累加1000次,得到结果1000
    第2个元素累加1000次,得到结果1000
    第3个元素累加1000次,得到结果1000
    第4个元素累加1000次,得到结果1000
    第5个元素累加1000次,得到结果1000
    

            从结果可以看出,数组类型的原子类的使用,可以保证每个元素的自增等操作都满足原子性。


    3.属性原子修改器

            以AtomicIntegerFieldUpdater类为例,该类有一个静态方法newUpdater(Class<U> tclass,String fieldName),则这个表示为tclass类的fieldName属性创建一个属性原子修改器,如果需要修改该属性,则只需要调用原子修改器的方法,比如addAndGet(tclass obj,int delta):该方法表示将该修改器对应的属性增加delta。以下通过代码来了解属性原子修改器的作用。

    package concurrent;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
    class Count{
    	public volatile int number;
    }
    public class AtomicTest {
        private static final AtomicIntegerFieldUpdater<Count> aifu=AtomicIntegerFieldUpdater.newUpdater(Count.class, "number");
        private static final ExecutorService es=Executors.newFixedThreadPool(20);
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		final Count count=new Count();
    		long start=System.currentTimeMillis();
    		for(int i=0;i<1000;i++)
    		{
    			Thread t1=new Thread() {
    				public void run()
    				{
    					
    					System.out.println(Thread.currentThread().getName()+"实现了一次自增原子操作,结果为:"+aifu.addAndGet(count, 1));
    				}
    			};
    			es.execute(t1);
    		}
    		try
    		{
    			Thread.sleep(5000);
    		}
    		catch(InterruptedException e)
    		{
    			e.printStackTrace();
    		}
    		System.out.println("计算过程的耗时为:"+(System.currentTimeMillis()-start-5000));
    		for(int i=0;i<5;i++)
    			System.out.println("第"+(i+1)+"个元素累加1000次,得到结果"+count.number);
    		
    
    	}
    
    }
    

    运行以上程序,得到如下结果:

    pool-1-thread-17实现了一次自增原子操作,结果为:993
    pool-1-thread-5实现了一次自增原子操作,结果为:992
    pool-1-thread-19实现了一次自增原子操作,结果为:991
    pool-1-thread-3实现了一次自增原子操作,结果为:990
    pool-1-thread-15实现了一次自增原子操作,结果为:989
    pool-1-thread-20实现了一次自增原子操作,结果为:988
    pool-1-thread-18实现了一次自增原子操作,结果为:987
    pool-1-thread-6实现了一次自增原子操作,结果为:986
    pool-1-thread-7实现了一次自增原子操作,结果为:985
    pool-1-thread-9实现了一次自增原子操作,结果为:984
    计算过程的耗时为:10
    第1个元素累加1000次,得到结果1000
    第2个元素累加1000次,得到结果1000
    第3个元素累加1000次,得到结果1000
    第4个元素累加1000次,得到结果1000
    第5个元素累加1000次,得到结果1000
    

             从上面结果可以看出,属性原子修改器的使用也能达到原子性操作的目的。


    4.引用类型

            以AtomicReference类为例,通过AtomicReference<Count> ar=new AtomicReference<>();ar可以调用set()方法设置初始值,调用compareAndSet(expect,update):这个方法就是将存在ar中的值与expect值作比较,如果两者相等,则更新该值为update;代码如下:

    package concurrent;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicReference;
    class Count{
    	public int count;
    	public Count(int count)
    	{
    		this.count=count;
    	}
    	public String toString()
    	{
    		return "这个对象的位置是:"+count;
    	}
    }
    public class AtomicTest {
        private static final AtomicReference<Count> ar=new AtomicReference();
       
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		Count count=new Count(1001);
    		long start=System.currentTimeMillis();
    		ar.set(count);
    		System.out.println("你好,"+ar.get());
    		Count count1=new Count(1002);
    		ar.compareAndSet(count, count1);//内存值与count是一样的,所以值更新为count1
    		System.out.println("我发生了改变:"+ar.get());
    		Count count2=new Count(1003);
    		ar.compareAndSet(count, count2);//此时内存智为count1,与count不一致,所以无法更新,因此内存值依然为count1
    		System.out.println("我发生了改变:"+ar.get());
    
    	}
    
    }
    

    运行程序,结果如下:

    你好,这个对象的位置是:1001
    我发生了改变:这个对象的位置是:1002
    我发生了改变:这个对象的位置是:1002


    展开全文
  • java并发编程之原子性操作

    千次阅读 2018-07-05 16:43:26
    java中自带了一些原子性操作,比如给一个非long、double基本数据类型变量或者引用的赋值或者读取操作。 为什么强调非 long 、 double 类型的变量?我们稍后看哈~ 那 i++ 这个操作不是一个 原子性操作 么? 答:还真...

    线程风险

    上头一直在说以线程为基础的并发编程的好处了,什么提高处理器利用率啦,简化编程模型啦。但是砖家们还是认为并发编程是程序开发中最不可捉摸、最诡异、最扯犊子、最麻烦、最恶心、最心烦、最容易出错、最不符合社会主义核心价值观的一个部分~ 造成这么多的原因其实很简单:进程中的各种资源,比如内存和I/O,在代码里以变量的形式展现,而某些变量在多线程间是共享、可变的,共享意味着这个变量可以被多个线程同时访问,可变意味着变量的值可能被访问它的线程修改。围绕这些共享、可变的变量形成了并发编程的三大杀手:安全性活跃性性能,下边我们来详细唠叨这些风险~

    共享变量的含义

    并不是所有内存变量都可以被多个线程共享,在一个线程调用一个方法的时候,会在栈内存上为局部变量以及方法参数申请一些内存,在方法调用结束的时候,这些内存便被释放。不同线程调用同一个方法都会为局部变量和方法参数拷贝一个副本(如果你忘了,需要重新学习一下方法的调用过程),所以这个栈内存是线程私有的,也就是说局部变量和方法参数是不可以共享的。但是对象或者数组是在堆内存上创建的,堆内存是所有线程都可以访问的,所以包括成员变量、静态变量和数组元素是可共享的,我们之后讨论的就是这些可以被共享的变量对并发编程造成的风险~ 如果不强调的话,我们下边所说的变量都代表成员变量、静态变量或者数组元素。

    安全性

    原子性操作内存可见性指令重排序是构成线程安全性的三个主题,下边我们详细看哈~

    原子性操作

    我们先拿一个例子开场:

    public class Increment {

       private int i;

       public void increase() {
           i++;
       }

       public int getI() {
           return i;
       }

       public static void test(int threadNum, int loopTimes) {
           Increment increment = new Increment();

           Thread[] threads = new Thread[threadNum];

           for (int i = 0; i < threads.length; i++) {
               Thread t = new Thread(new Runnable() {
                   @Override
                   public void run() {
                       for (int i = 0; i < loopTimes; i++) {
                           increment.increase();
                       }
                   }
               });
               threads[i] = t;
               t.start();
           }

           for (Thread t : threads) {  //main线程等待其他线程都执行完成
               try {
                   t.join();
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }

           System.out.println(threadNum + "个线程,循环" + loopTimes + "次结果:" + increment.getI());
       }

       public static void main(String[] args) {
           test(20, 1);
           test(20, 10);
           test(20, 100);
           test(20, 1000);
           test(20, 10000);
           test(20, 100000);
       }
    }

    其中,increase方法的作用是给成员变量i增1,test方法接受两个参数,一个是线程的数量,一个是循环的次数,每个线程中都有一个将成员变量i增1给定循环次数的任务,在所有线程的任务都完成之后,输出成员变量i的值,如果没有什么问题的话,程序执行完成后成员变量i的值都是threadNum*loopTimes。大家看一下执行结果:

    20个线程,循环1次结果:20
    20个线程,循环10次结果:200
    20个线程,循环100次结果:2000
    20个线程,循环1000次结果:19926
    20个线程,循环10000次结果:119903
    20个线程,循环100000次结果:1864988

    咦,貌似有点儿不对劲唉~再次执行一遍的结果:

    20个线程,循环1次结果:20
    20个线程,循环10次结果:200
    20个线程,循环100次结果:2000
    20个线程,循环1000次结果:19502
    20个线程,循环10000次结果:100157
    20个线程,循环100000次结果:1833170

    这就更令人奇怪了~~ 当循环次数增加时,执行结果与我们预期不一致,而且每次执行貌似都是不一样的结果,这个是个什么鬼?

    答:这个就是多线程的非原子性操作导致的一个不确定结果。

    啥叫个原子性操作呢?就是一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作,也就是说这些操作是不可分割的,线程不能在这些操作上交替执行。java中自带了一些原子性操作,比如给一个非long、double基本数据类型变量或者引用的赋值或者读取操作。

    为什么强调非longdouble类型的变量?我们稍后看哈~

    i++这个操作不是一个原子性操作么?

    答:还真不是,这个操作其实相当于执行了i = i + 1,也就是三个原子性操作:

    1. 读取变量i的值

    2. 将变量i的值加1

    3. 将结果写入i变量中

    由于线程是基于处理器分配的时间片执行的,在这个过程中,这三个步骤可能让多个线程交叉执行,为简化过程,我们以两个线程交叉执行为例,看下图:

    这个图的意思就是:

    1. 线程1执行increase方法先读取变量i的值,发现是5,此时切换到线程2执行increase方法读取变量i的值,发现也是5

    2. 线程1执行将变量i的值加1的操作,得到结果是6,线程二也执行这个操作。

    3. 线程1将结果赋值给变量i,线程2也将结果赋值给变量i。

    在这两个线程都执行了一次increase方法之后,最后的结果竟然是变量i5变到了6,而不是我们想象中的7。。。

    另外,由于CPU的速度非常快,这种交叉执行在执行次数较低的时候体现的并不明显,但是在执行次数多的时候就十分明显了,从我们上边测试的结果上就能看出

    在真实编程环境中,我们往往需要某些涉及共享可变变量的一系列操作具有原子性,我们可以从下边三个角度来保证这些操作具有原子性。

    共享性解决

    如果一个变量变得不可以被多线程共享,不就可以随便访问了呗哈哈,大致有下面这么两种改进方案。

    尽量使用局部变量解决问题

    因为方法中的局部变量(包括方法参数和方法体中创建的变量)是线程私有的,所以无论多少线程调用某个不涉及共享变量的方法都是安全的。所以如果能将问题转换为使用局部变量解决问题而不是共享变量解决,那将是极好的哈~。不过我貌似想不出什么案例来说明一下,等想到了再说哈,各位想到了也可以告诉我哈。

    使用ThreadLocal

    为了维护一些线程内可以共享的数据,java提出了一个ThreadLocal类,它提供了下边这些方法:

    public class ThreadLocal<T> {

       protected T initialValue() {
           return null;
       }

       public void set(T value) {
           ...
       }

       public T get() {
           ...
       }

       public void remove() {
            ...
        }
    }

    其中,类型参数T就代表了在同一个线程中共享数据的类型,它的各个方法的含义是:

    • T initialValue():当某个线程初次调用get方法时,就会调用initialValue方法来获取初始值。

    • void set(T value):调用当前线程将指定的value参数与该线程建立一对一关系(会覆盖initialValue的值),以便后续get方法获取该值。

    • T get():获取与当前线程建立一对一关系的值。

    • void remove():将与当前线程建立一对一关系的值移除。

    我们可以在同一个线程里的任何代码处存取该类型的值

    public class ThreadLocalDemo {

       public static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>(){
           @Override
           protected String initialValue() {
               return "调用initialValue方法初始化的值";
           }
       };

       public static void main(String[] args) {
           ThreadLocalDemo.THREAD_LOCAL.set("与main线程关联的字符串");
           new Thread(new Runnable() {
               @Override
               public void run() {
                   System.out.println("t1线程从ThreadLocal中获取的值:" + ThreadLocalDemo.THREAD_LOCAL.get());
                   ThreadLocalDemo.THREAD_LOCAL.set("与t1线程关联的字符串");
                   System.out.println("t1线程再次从ThreadLocal中获取的值:" + ThreadLocalDemo.THREAD_LOCAL.get());
               }
           }, "t1").start();

           System.out.println("main线程从ThreadLocal中获取的值:" + ThreadLocalDemo.THREAD_LOCAL.get());
       }
    }

    执行结果是:

    main线程从ThreadLocal中获取的值:与main线程关联的字符串
    t1线程从ThreadLocal中获取的值:调用initialValue方法初始化的值
    t1线程再次从ThreadLocal中获取的值:与t1线程关联的字符串

    从这个执行结果我们也可以看出来,不同线程操作同一个 ThreadLocal 对象执行各种操作而不会影响其他线程里的值。这一点非常有用,比如对于一个网络程序,通常每一个请求都分配一个线程去处理,可以在ThreadLocal里记录一下这个请求对应的用户信息,比如用户名,登录失效时间什么的,这样就很有用了。

    虽然ThreadLocal很有用,但是它作为一种线程级别的全局变量,如果某些代码依赖它的话,会造成耦合,从而影响了代码的可重用性,所以设计的时候还是要权衡一下子滴。

    可变性解决

    如果一个变量可以被共享,但是它自打被创建之后就不能被修改,那么随意哪个线程去访问都可以哈,反正又不能改变它的值,随便读啦~

    再强调一遍,我们写的程序可能不仅我们自己会用,所以我们不能靠猜、靠直觉、靠信任其他使用我们写的代码的客户端程序猿,所以如果我们想通过让对象不可变的方式来保证线程安全,那就把该变量声明为 final 的吧 

    public class FinalDemo {
       private final int finalField;

       public FinalDemo(int finalField) {
           this.finalField = finalField;
       }
    }

    然后就可以随便在多线程间共享finalField这个变量喽~

    加锁解决

    锁的概念

    如果我们的需求确实是需要共享并且可变的变量,又想让某些关于这个变量的操作是原子性的,还是以上边的increase方法为例,我们现在面临的困境是increase方法其实是由下边3个原子性操作累积起来的一个操作:

    1. 读变量i

    2. 运算;

    3. 写变量i

    针对同一个变量i,不同线程可能交叉执行上边的三个步骤,导致两个线程读到同样的变量i的值,从而导致结果比预期的小。为了让increase方法里的操作具有原子性,也就是在一个线程执行这一系列操作的同时禁止其他线程执行这些操作,java提出了的概念。

    我们拿上厕所做一个例子,比如我们上厕所需要这几步:

    1. 脱裤子

    2. 干正事儿

    3. 擦屁股

    4. 提裤子

    上厕所的时候必须把这些步骤都执行完了,才能圆满的完成上厕所这个事儿,要不然执行到擦屁股环节被别人赶出来岂不是贼尴尬

    展开全文
  • Java线程安全中的原子性操作

    千次阅读 2020-04-02 23:13:46
    原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,...一个是JDK中已经提供好的Atomic包,他们均使用了CAS完成线程的原子性操作。 另一个是使用锁的机制来处理线程之间的原子性。锁包括synch...

    原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割,而只执行其中的一部分(不可中断性)。将整个操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特征。

    说到原子性,在Java中一共有两个方面需要学习和掌握

    1. 一个是JDK中已经提供好的Atomic包,他们均使用了CAS完成线程的原子性操作。
    2. 另一个是使用锁的机制来处理线程之间的原子性。锁包括synchronized、Lock。

    什么是CAS?

    CAS(Compare and swap)比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。CAS操作需要输入两个数值,一个旧值(期望操作前的值)A和一个新值B,在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换,交换失败后进行自旋,直到成功或者线程失败结束。Java中的sun.misc.Unsafe类,提供了compareAndSwapint()和compareAndSwapLong()等几个方法实现CAS。

    操作系统修改内存中的值通常是经过找到变量在内存中的内存地址,然后修改变量的值,但是作为开发人员,我们是在JVM下编写代码,Java的API是不允许我们像操作系统一样去通过内存地址去找到变量,修改变量的值。通常我们是使用上面所提到的sun.misc.Unsafe类,Unsafe知道到每个对象在内存中的内存区域是怎样的,并且可以得到对应字段的offset,也就是我们所说的偏移量,偏移量的类型是Long类型。下面是通过Unsafe,简单的实现CAS操作

    public class CounterUnsafe {
     
        volatile int i = 0;
     
        private static Unsafe unsafe = null;
        //偏移量
        private static Long valueOffset;
     
        static {
            try {
                //通过反射得到Unsafe类
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                unsafe = (Unsafe) field.get(null);
                //获取i字段的偏移量
                Field fieldi = CounterUnsafe.class.getDeclaredField("i");
                valueOffset = unsafe.objectFieldOffset(fieldi);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
     
        public void add() {
     
            for (;;){
                //通过偏移量取到变量值
                int current = unsafe.getIntVolatile(this,valueOffset);
                //若CAS操作成功,则停止自旋
                if(unsafe.compareAndSwapInt(this, valueOffset, current, current+1)) {
                    break;
                }
            }
     
        }
    }
    

    Atomic包

    从java1.5开始,jdk提供了java.util.concurrent.atomic包,这个包中的原子操作类(均使用CAS机制完成原子性操作),提供了一种用法简单,性能高效,线程安全的更新一个变量的方式。

    atomic包里面一共提供了13个类,分为4种类型,分别是:原子更新基本类型,原子更新数组,原子更新引用,原子更新属性,这13个类都是使用Unsafe实现的包装类。

    在这里插入图片描述

    一、原子更新基本类型

    Atomic包提供了3个类用于原子更新基本类型:分别是AtomicInteger原子更新整型,AtomicLong原子更新长整型,AtomicBoolean原子更新bool值。由于这三个类提供的方法几乎是一样的,以AtomicInteger为例进行说明。

    AtomicInteger

    public class CountExample {
    
        //请求总数
        public static int clientTotal  = 5000;
        //同时并发执行的线程数
        public static int threadTotal = 200;
        //变量声明:计数
        public static AtomicInteger count = new AtomicInteger(0);
    
        public static void main(String[] args) throws InterruptedException {
            ExecutorService executorService = Executors.newCachedThreadPool();//创建线程池
            final Semaphore semaphore = new Semaphore(threadTotal);//定义信号量,给出允许并发的数目
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);//定义计数器闭锁
            for (int i = 0;i<clientTotal;i++){
                executorService.execute(()->{
                    try {
                        semaphore.acquire();//判断进程是否允许被执行
                        add();
                        semaphore.release();//释放进程
                    } catch (InterruptedException e) {
                        log.error("excption",e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();//保证信号量减为0
            executorService.shutdown();//关闭线程池
            log.info("count:{}",count.get());//变量取值
        }
    
        private static void add(){
            count.incrementAndGet();//变量操作
        }
    }
    

    对count变量的+1操作,采用的是incrementAndGet方法,此方法的源码中调用了一个名为unsafe.getAndAddInt的方法:

    public final int incrementAndGet() {
         return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    

    而getAndAddInt方法的具体实现为:

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }
    

    代码中的count可以理解为JMM中的工作内存,而这里的底层数值即为主内存

    getAndAddInt调用了Unsafe的native方法:getIntVolatile和compareAndSwapInt,在do-while循环中先取得当前值,然后通过CAS判断当前值是否和current一致,如果一致意味着值没被其他线程修改过,把当前值设置为当前值+var4,如果不相等程序进入新的CAS循环。

    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
    

    通过代码,我们发现Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现它是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新char、float和double变量也可以用类似的思路来实现。

    AtomicLong 与 LongAdder对比

    LongAdder是java8为我们提供的新的类,跟AtomicLong有相同的效果。首先看一下代码实现:

    //AtomicLong:
    //变量声明
    public static AtomicLong count = new AtomicLong(0);
    //变量操作
    count.incrementAndGet();
    //变量取值
    count.get();
    
    //LongAdder:
    //变量声明
    public static LongAdder count = new LongAdder();
    //变量操作
    count.increment();
    //变量取值
    count
    

    那么问题来了,为什么有了AtomicLong还要新增一个LongAdder呢?
    原因是:CAS底层实现是在一个死循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈的时候,修改成功率很高,否则失败率很高。在失败的时候,这些重复的原子性操作会耗费性能。

    对于普通类型的long、double变量,JVM允许将64位的读操作或写操作拆成两个32位的操作。

    LongAdder类的实现核心是将热点数据分离,比如说它可以将AtomicLong内部的内部核心数据value分离成一个数组,每个线程访问时,通过hash等算法映射到其中一个数字进行计数,而最终的计数结果则为这个数组的求和累加,其中热点数据value会被分离成多个单元的cell,每个cell独自维护内部的值。当前对象的实际值由所有的cell累计合成,这样热点就进行了有效地分离,并提高了并行度。这相当于将AtomicLong的单点的更新压力分担到各个节点上。在低并发的时候通过对base的直接更新,可以保障和AtomicLong的性能基本一致。而在高并发的时候通过分散提高了性能。

    public void increment() {
        add(1L);
    }
    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }
    

    如果在统计的时候,如果有并发更新,可能会有统计数据有误差。实际使用中在处理高并发计数的时候优先使用LongAdder。在线程竞争不是很激烈的情况下,使用AtomicLong会简单效率更高一些。比如序列号生成(准确性)

    二、原子更新数组

    Atomic包里提供了三个类用于原子更新数组里面的元素,分别是:AtomicIntegerArray:原子更新整型数组里的元素;AtomicLongArray:原子更新长整型数组里的元素;AtomicReferenceArray:原子更新引用数组里的元素;因为每个类里面提供的方法都一致,因此以AtomicIntegerArray为例来说明。

    AtomicIntegerArray

    public class AtomicIntegerArrayTest {
    
        private static int[] value = new int[]{1,2,3};
        private static AtomicIntegerArray atomicInteger = new AtomicIntegerArray(value);
    
        public static void main(String[] args){
            atomicInteger.getAndSet(0,12);
            System.out.println(atomicInteger.get(0));
            System.out.println(value[0]);
        }
    
    }
    

    数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

    三、原子更新引用

    原子更新基本类型的AtomicInteger只能更新一个变量,如果要原子更新多个变量,就需要使用原子更新引用类型提供的类了。原子引用类型atomic包主要提供了以下几个类:AtomicReference:原子更新引用类型;AtomicReferenceFieldUpdater:原子更新引用类型里的值;AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。以上类中提供的方法基本一致,我们以AtomicReference为例说明:

    AtomicReference

    public class AtomicReferenceTest {
    
        private static AtomicReference<User> reference = new AtomicReference<User>();
    
        public static void main(String[] args){
            User user = new User("tom",23);
            reference.set(user);
            User updateUser = new User("ketty",34);
            reference.compareAndSet(user,updateUser);
            System.out.println(reference.get().getName());
            System.out.println(reference.get().getAge());
        }
    
    
        static class User{
    
            private String name;
            private int age;
    
            public User(String name, int age) {
                this.name = name;
                this.age = age;
            }
    
            public String getName() {
                return name;
            }
    
            public int getAge() {
                return age;
            }
        }
    }
    

    上述代码中首先创建一个user对象,然后把user对象设置进AtomicReference中,最后通过compareAndSet做原子更新操作,运行结果如下:

    ketty
    34
    

    四、原子更新属性

    如果需要原子更新某个对象的某个字段,就需要使用原子更新属性的相关类,Atomic中提供了一下几个类用于原子更新属性:AtomicIntegerFieldUpdater:原子更新整型属性的更新器;AtomicLongFieldUpdater:原子更新长整型的更新器;AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。

    想要原子的更新字段,需要两个步骤:

    1. 因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性
    2. 更新类的字段(属性)必须使用public volatile修饰符

    AtomicIntegerFieldUpdater与AtomicLongFieldUpdater方法基本一致,下面用AtomicIntegerFieldUpdater为例说明。

    AtomicIntegerFieldUpdater

    public class AtomicExample5 {
    
        //原子性更新某一个类的一个实例
        private static AtomicIntegerFieldUpdater<AtomicExample5> updater
                = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");
    
        @Getter
        public volatile int count = 100;//必须要volatile标记,且不能是static
    
        public static void main(String[] args) {
            AtomicExample5 example5 = new AtomicExample5();
    
            if(updater.compareAndSet(example5,100,120)){
                log.info("update success 1,{}",example5.getCount());
            }
    
            if(updater.compareAndSet(example5,100,120)){
                log.info("update success 2,{}",example5.getCount());
            }else{
                log.info("update failed,{}",example5.getCount());
            }
        }
    }
    
    此方法输出的结果为:
    [main] INFO com.superboys.concurrency.example.Atomic.AtomicExample5 - update success 1,120
    [main] INFO com.superboys.concurrency.example.Atomic.AtomicExample5 - update failed,120
    
    由此可见,count的值只修改了一次。
    

    AtomicStampReference

    什么是ABA问题?

    CAS操作的时候,其他线程将变量的值A改成了B,但是随后又改成了A,本线程在CAS方法中使用期望值A与当前变量进行比较的时候,发现变量的值未发生改变,于是CAS就将变量的值进行了交换操作。但是实际上变量的值已经被其他的变量改变过,这与设计思想是不符合的。

    原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。

    private static class Pair<T> {
            final T reference;
            final int stamp;
            private Pair(T reference, int stamp) {
                this.reference = reference;
                this.stamp = stamp;
            }
            static <T> Pair<T> of(T reference, int stamp) {
                return new Pair<T>(reference, stamp);
            }
        }
    
    private volatile Pair<V> pair;
    
    private boolean casPair(Pair<V> cmp, Pair<V> val) {
            return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
        }
    
    public boolean compareAndSet(V   expectedReference,
                                     V   newReference,
                                     int expectedStamp,
                                     int newStamp) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                expectedStamp == current.stamp &&
                ((newReference == current.reference &&   //排除新的引用和新的版本号与底层的值相同的情况
                  newStamp == current.stamp) ||
                 casPair(current, Pair.of(newReference, newStamp)));
    }
    

    解释一下上边的源码:
    类中维护了一个volatile修饰的Pair类型变量current,Pair是一个私有的静态类,current可以理解为底层数值。
    compareAndSet方法的参数部分分别为期望的引用、新的引用、期望的版本号、新的版本号。
    return的逻辑为判断了期望的引用和版本号是否与底层的引用和版本号相符,并且排除了新的引用和新的版本号与底层的值相同的情况(即不需要修改)的情况(return代码部分3、4行)。条件成立,执行casPair方法,调用CAS操作

    AtomicStampReference的处理思想是,每次变量更新的时候,将变量的版本号+1,之前的ABA问题中,变量经过两次操作以后,变量的版本号就会由1变成3,也就是说只要线程对变量进行过操作,变量的版本号就会发生更改。从而解决了ABA问题。

    synchronized

    在synchronized锁中,Java默默在临界区前后加上了lock和unlock方法,好处是,加锁和解锁一定是成对出现了,毕竟如果忘记unlock解锁,意味着其他线程只能死等下去了。

    依赖于JVM去实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程对其进行操作的。
    synchronized是java中的一个关键字,是一种同步锁。它可以修饰的对象主要有四种:

    一、synchronized 修饰一个代码块

    被修饰的代码称为同步语句块,作用的范围是大括号括起来的部分。作用的对象是调用这段代码的对象

    public class SynchronizedExample {
        public void test(int j){
            synchronized (this){
                for (int i = 0; i < 10; i++) {
                    log.info("test - {} - {}",j,i);
                }
            }
        }
        //使用线程池方法进行测试:
        public static void main(String[] args) {
            SynchronizedExample example1 = new SynchronizedExample();
            SynchronizedExample example2 = new SynchronizedExample();
            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.execute(()-> example1.test(1));
            executorService.execute(()-> example2.test(2));
        }
    }
    

    不同对象之间的操作互不影响

    二、synchronized 修饰一个方法

    被修饰的方法称为同步方法,作用的范围是大括号括起来的部分,作用的对象是调用这段代码的对象

    public class SynchronizedExample 
        public synchronized void test(int j){
            for (int i = 0; i < 10; i++) {
                log.info("test - {} - {}",j,i);
            }
        }
        //验证方法与上面相同
        ...
    }
    

    结果:不同对象之间的操作互不影响

    如果当前类是一个父类,子类调用父类的被synchronized修饰的方法,不会携带synchronized属性,因为synchronized不属于方法声明的一部分

    三、synchronized 修饰一个静态方法

    作用的范围是synchronized 大括号括起来的部分,作用的对象是这个类的所有对象

    public class SynchronizedExample{
        public static synchronized void test(int j){
            for (int i = 0; i < 10; i++) {
                log.info("test - {} - {}",j,i);
            }
        }
        //验证方法与上面相同
        ...
    }
    

    结果:同一时间只有一个线程可以执行

    四、synchronized 修饰一个类

    public class SynchronizedExample{
        public static void test(int j){
            synchronized (SynchronizedExample.class){
                for (int i = 0; i < 10; i++) {
                    log.info("test - {}-{}",j,i);
                }
            }
        }
        //验证方法与上面相同
        ...
    }
    
    

    结果:同一时间只有一个线程可以执行

    原子性操作方法对比

    • Atomic:竞争激烈时能维持常态,比Lock性能好,每次只能同步一个值
    • synchronized:不可中断锁,适合竞争不激烈,可读性好
    • Lock:可中断锁,多样化同步,竞争激烈时能维持常态

    参考链接

    https://www.cnblogs.com/senlinyang/p/7856339.html

    https://blog.csdn.net/jesonjoke/article/details/79837508#原子性

    https://blog.csdn.net/weixin_39267363/article/details/97569304

    https://www.jianshu.com/p/7188fe52e9b9

    关注公众号 ,专注于java大数据领域离线、实时技术干货定期分享!个人网站 www.lllpan.top

    在这里插入图片描述

    展开全文
  • redis使用lua脚本实现原子性操作案例

    千次阅读 2020-04-10 14:25:37
    例 redis里面的命令是这样 127.0.0.1:6379[11]> set age 12 OK 127.0.0.1:6379[11]> 那么在lua脚本里面就是这样写,后面的0表示要操作的KEY的个数,关于redis使用lua脚本怎么传参数可以看上面的那个连接。...
  • 并发编程之原子性操作

    千次阅读 2018-04-20 10:59:07
    原子性操作:一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作,也就是说这些操作是不可分割的,线程不能在这些操作上交替执行。i++为什么不是原子性操作?它相当于三个原子性操作:1.读取...
  • 概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。 引出问题(代码示例) class VolatileAtomicThread implements ...
  • java多线程并发-原子性操作类的应用

    万次阅读 2019-04-11 15:47:22
    在java5以后,我们接触到了线程原子性操作,也就是在修改时我们只需要保证它的那个瞬间是安全的即可,经过相应的包装后可以再处理对象的并发修改,本文总结一下Atomic系列的类的使用方法,其中包含: 基本类型 ...
  • CAS原子性操作的原理: 1. CAS(Compare And Swap):比较并交换 在CAS中,有这样三个值: V:要更新的变量(var) E:预期值(expected) N:新值(new) 比较并交换的过程如下: 判断V是否等于E,如果等于,将V的值设置...
  • 分布式Redis原子操作示例,近期项目中遇到分布式项目中多节点大并发操作redis同一个key。此案例利用java调用LUA脚本实现redis操作原子性。分享出来大家参考。
  • 主要给大家介绍了关于JDK8中新增的原子性操作类LongAdder的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面跟着小编来一起学习学习吧。
  • 什么是原子性,什么是原子性操作

    千次阅读 2018-03-23 21:14:11
    在这个事务里,要做如下操作: 1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。 2. 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。如果在A的...
  • 在单线程的模式下,我们针对某个变量的修改是不会产生数据的脏读和脏写的,因为它只有一个操作来对变量进行读写操作,但是在多...简单说明下原子操作就是一个步骤要么操作全部成功,要么失败。我们看下i++这个操作...
  • 为什么i++不是原子性操作

    千次阅读 2020-11-30 21:00:35
    为啥i++并不是原子性操作 在编写多线程程序时,对于不是原子操作的,需要引起程序员的额外注意。 一定要确保其对数据的操作是同步的,否则会引发数据安全问题。 i++不是原子操作 先来看一个例子,多线程下出现的数据...
  • Java实现i++原子性操作

    千次阅读 2020-03-14 15:32:43
    Java实现i++原子性操作1、cas 实现i++原子性 i++在并发过程中是属于线程不安全的,因为i++不具有原子性。只要解决了i++的原子性问题,那么它就是属于线程安全的了。(废话一大堆……) 1、cas 实现i++原子性 ...
  • JDK8中新增原子性操作类LongAdder

    千次阅读 2018-01-16 12:49:46
    LongAdder类似于AtomicLong是原子性递增或者递减类,AtomicLong已经通过CAS提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说性能已经很好了,但是JDK开发组并不满足,因为在非常高的并发请求下AtomicLong的...
  • java——保证原子性操作的CAS算法

    千次阅读 2017-12-14 10:34:47
    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享...
  • 原子性操作函数

    千次阅读 2019-05-11 19:53:50
    如果对一个变量进行自加自减操作,一般这样写: int global_count = 0; global_count ++; 如果是多线程操作,一般都需要加锁处理: pthread_mutex_t count_lock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_...
  • java原子性操作,volatile

    2018-03-02 02:42:09
    可是虚拟机规范中规定的assign,store,write才是原子性操作,万一这三步执行中间时刻,有其他线程读取了数据,那怎么办? 还有一点,对于多线程来说:比如线程A和B,同时执行了对全局变量c的write原子操作,那怎么处理?谁会先...
  • 基于redis的分布式锁(原子性操作

    千次阅读 2018-10-31 18:44:01
    1.互斥。在任意时刻,只有一个客户端能持有锁。 2.不会发生死锁。即使有一个客户端在持有锁期间挂掉没有主动释放锁,也要保证后续其他客户端可以加锁。 3.具有容错。只要大部分的redis节点正常运行,客户端就...
  • 原子性操作 原子性是指事务的一个完整性操作。对一件事务进行操作,操作成功则提交,失败则回滚。得以保障事务的完整性,要么做完,要么什么也不做。在进行该事务的过程中不能被其它进程(线程)中断。
  • 原子性操作(atomic operation)

    千次阅读 2017-04-13 23:30:16
    原子操作排除了任何抢占的可能,是不会被线程调度机制打断的操作;也就是说这种操作一旦开始就一直运行到结束,中间不会有任何context switch(进程切换)。 原子操作可以是一个步骤,也可以是多个操作步骤,但是...
  • (Java学习笔记)i++不是原子性操作

    千次阅读 2018-10-03 20:42:41
    public class CasStudy01 {...这是因为count++这一行代码并不是原子操作,这一行代码实际在运行时,被分为取值,修改,存储三步操作,所以1,2两个线程同时取出值a,并且自增1修改为a+1,再存储的话,两次自增实际上只自增了1
  • 程序的原子性操作

    万次阅读 2020-08-14 00:21:09
    作为开发人员经常会用到锁, 因为锁通常来说具有原子性,那原子性又是什么? 有些人说,原子性就是同一个代码块在任何时刻只能被一个线程访问执行,其他线程则等待(阻塞). 细想一下这句话应该属于排它性. 也有人说不可...
  • Atomic: Atomic包是java.util.concurrent下的另一个专门为线程安全设计的java的包,包含多个原子性操作的类。基本特性就是在多线程情况下,当多个线程想要同时操作这些类的某些实例方法时,具有排他性,也就是当...
  • i++ 是线程安全的吗?不是!(经典的内存不可见问题) { int i = 1; int j1 = i++; System.out.println("j1=" + j1...)使用支持原子性操作的类,如 java.util.concurrent.atomic.AtomicInteger,它使用的是 CAS 算法
  • GIL保证字节码级别的原子性和线程安全性,因此当个字节码执行一定是安全的,执行结果一定是一致的。而有些操作,底层需要通过多个字节码来完成,这样的操作就不是原子的,因此不是线程安全的。举个例子,a+=1 。反...
  • 【java并发】原子性操作类的使用

    千次阅读 2016-06-01 21:42:37
    在java5以后,我们接触到了线程原子性操作,也就是在修改时我们只需要保证它的那个瞬间是安全的即可,经过相应的包装后可以再处理对象的并发修改,本文总结一下Atomic系列的类的使用方法,其中包含: 类型 Integer...
  • 什么是原子性,什么是原子性操作

    千次阅读 2018-06-28 08:25:16
    转自:https://blog.csdn.net/android_mrchen/article/details/77866490点击...在这个事务里,要做如下操作: 1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。 2. 在B的帐户里...
  • 关于Java原子性操作的理解(赋值和自增自减) 文章目录关于Java原子性操作的理解(赋值和自增自减)赋值操作是否是原子性?自增自减操作是否是原子性的? 赋值操作是否是原子性? 不是,不管是基础数据类型还是引用数据...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 341,879
精华内容 136,751
关键字:

原子性操作

友情链接: 111857150142493.rar