• Java 多线程 volitile 和 atomic


    Java 多线程 volitile 和 atomic

    volitile关键字

    public class MTester {
        public static class TestKey{
            int x = 0;
        }
        public static TestKey key0 = new TestKey();
        public static void main(String[] args) {
            Thread thread = new Thread(()->{
                while (key0.x == 0){
                }
                System.out.println("key0"+key0.x);
            });
            Thread thread1 = new Thread(()->{
                try {
                    Thread.sleep(1000);
                    key0.x=1;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println("over");
                }
            });
            thread.start();
            thread1.start();
        }
    }
    

    尝试运行以上代码,发现thread永远也无法发现key0的x被改变

    所以这个时候需要加上volitile关键字

    具体原因是java中每个线程都有工作内存,以及主存

    我的理解就是不加volitile,线程读写变量是先在自己的工作内存中处理,然后再写回主存,但是有的线程处理的是工作内存,但是并没有从主存里面读取,加上volitile关键字之后,会通知其他线程,让他们强制从主存中读取数据

    https://www.cnblogs.com/zhengbin/p/5654805.html

    volatile还有一个特性:禁止指令重排序优化。

    可以见这个文章

    https://www.cnblogs.com/chengxiao/p/6528109.html

    但是volitile只能保证可见性,不能保证原子性,也就是说如果多线程操作 i++还是无法保证正确

    ExecutorService service = Executors.newCachedThreadPool();
    for (int i = 0;i<10;i++){
    	service.submit(()->{
    		for (int j = 0;j <10;j++){
    			key0.x++;
    			System.out.println(key0.x);
    			try {
    				Thread.sleep(1000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	});
    }
    service.awaitTermination(10, TimeUnit.SECONDS);
    System.out.println(key0.x);
    

    还是无法保证原子性

    这个时候可以考虑使用 java.util.concurrent.atomic;中的类

    这些类里面的类大部分都是使用CAS算法进行操作的

    CAS compare and set

    public final boolean compareAndSet(int expect, int update) {
    	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    

    这个 unsafe是unsafe类,里面的方法都是native方法

    补充:

    这个valueOffset可以理解为一个AtomicInteger的偏移量然后native方法然后传入native方法,这样就相当于获取了CAS需要比较对象的地址了

    CAS其实就是期望的值进行比较,如果不相等,就证明有其他线程更改过了,然后不执行操作然后返回失败,CAS看起来很麻烦,但是却可以映射一些CPU指令,实际上执行起来还是很快的(参考java核心技术)

    unsafe里面的方法大部分都是native方法

    比如说我们想要对AtomicInteger执行一个 increase操作,就先比较自己跟期望的值,如果不等,那就在下次循环接着尝试更改,直到更改成功

    //AtomicInteger.java
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    //Unsafe.class
    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;
    }
    

    不过这种不断尝试比较,对CPU开销还是比较大,不过相对于synchronized来说更轻量级,因为synchronized需要不断尝试获取锁释放锁,而且只能独占

    在并发量不是特别大的情况下,效率相对于synchronized还是很高的,当自选严重冲突的时候synchronized还是效率更高一些

    CAS 算法属于自旋

    不过CAS算法也有其他的缺点,常见的就是ABA问题

    举个例子

    线程1:从内存位置 1 取回A

    线程2:从内存位置 1取到 A

    线程2:做了一些操作

    线程2:从内存位置 1 写入A

    线程1:发现位置1还是A CAS成功但是却不知道线程2做了什么操作,可能引发一些后果

    解决办法

    AtomicStampedReference
    

    可以用一个timestamp 或者mask来判断是否有其他操作

    自旋锁的简单实现:

    思路:每次只有一个线程进入临界区

    import java.util.concurrent.atomic.AtomicReference;
    
    public class MSpinLock {
        AtomicReference<Thread> reference = new AtomicReference<>();
        public void lock(){
            do {
    
            }while (!this.reference.compareAndSet(null,Thread.currentThread()));
        }
        public void unlock(){
            do {
    
            }while (!this.reference.compareAndSet(Thread.currentThread(),null));
        }
    
        public static void main(String[] args) {
            MSpinLock lock = new MSpinLock();
            Thread thread = new Thread(()->{
                while (true){
                    try{
                        lock.lock();
                        System.out.println("thread had lock");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println("thread will unlock");
                        lock.unlock();
                    }
                }
            });
            Thread thread1 = new Thread(()->{
                while (true){
                    try{
                        lock.lock();
                        System.out.println("thread1 had lock");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println("thread1 will unlock");
                        lock.unlock();
                    }
                }
    
            });
    
            thread.start();
            thread1.start();
        }
    }
    

    每次都是成对出现的

    如果注释掉lock

    显然不对

    不过我们做的自选锁不可重入

    假如有个函数需要递归,那么自旋锁就会发生死锁

    所以我们需要一个Integer来判断一下

        public void lock(){
            if(Thread.currentThread().equals(reference.get())){
                atomicInteger.incrementAndGet();
                return ;
            }
            do {
            }while (!this.reference.compareAndSet(null,Thread.currentThread()));
            atomicInteger.incrementAndGet();
        }
        public void unlock(){
            if(Thread.currentThread().equals(reference.get())){
                int n = atomicInteger.decrementAndGet();
                if(n>0){
                    return;
                }
            }
            do {
            }while (!this.reference.compareAndSet(Thread.currentThread(),null));
    
        }
    

    这样就可重入了

    https://www.cnblogs.com/qjjazry/p/6581568.html

  • 相关阅读:
    在excel实现多级联动
    发送邮件使用html模板的实现的大致思路
    设计模式的定义和分类
    yb课堂之用户登陆校验拦截器开发 《十一》
    CTF中常用的php伪协议利用
    Docker
    从零开始的Wordpress个人博客搭建
    .htaccess文件配置理解
    disable_function绕过--利用LD_PRELOAD
    信息安全实习生面试小结
  • 原文地址:https://www.cnblogs.com/stdpain/p/10659449.html
Copyright © 2020-2023  润新知