精华内容
下载资源
问答
  • 原子变量类Atomic*

    2021-01-09 12:16:43
    原子变量类详细介绍

    原子变量最主要的一个特点就是所有的操作都是原子的,synchronized关键字也可以做到对变量的原子操作。只是synchronized的成本相对较高,需要获取锁对象,释放锁对象,如果不能获取到锁,还需要阻塞在阻塞队列上进行等待。而如果单单只是为了解决对变量的原子操作,建议使用原子变量。

    一、原子变量的基本概念

     原子变量保证了该变量的所有操作都是原子的,不会因为多线程的同时访问而导致脏数据的读取问题。我们先看一段synchronized关键字保证变量原子性的代码:

    public class Counter {
        private int count;
    
        public synchronized void addCount(){
            this.count++;
        }
    }

    简单的count++操作,线程对象首先需要获取到Counter 类实例的对象锁,然后完成自增操作,最后释放对象锁。整个过程中,无论是获取锁还是释放锁都是相当消耗成本的,一旦不能获取到锁,还需要阻塞当前线程等等。

    对于这种情况,我们可以将count变量声明成原子变量,那么对于count的自增操作都可以以原子的方式进行,就不存在脏数据的读取了。原原子变量类可以分为 4 组:

    ① 基本类型

    • AtomicBoolean - 布尔类型原子类
    • AtomicInteger - 整型原子类
    • AtomicLong - 长整型原子类

    ② 引用类型

    • AtomicReference - 引用类型原子类
    • AtomicMarkableReference - 带有标记位的引用类型原子类
    • AtomicStampedReference - 带有版本号的引用类型原子类,彻底解决了ABA问题

    ③ 数组类型

    • AtomicIntegerArray - 整形数组原子类
    • AtomicLongArray - 长整型数组原子类
    • AtomicReferenceArray - 引用类型数组原子类

    ④ 属性更新器类型

    • AtomicIntegerFieldUpdater - 整型字段的原子更新器。
    • AtomicLongFieldUpdater - 长整型字段的原子更新器。
    • AtomicReferenceFieldUpdater - 原子更新引用类型里的字段。

    二、AtomicInteger的基本使用及实现原理

    AtomicInteger的基本使用

    首先看它的两个构造函数:

    private volatile int value;
    
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    public AtomicInteger() {
    
    }

    可以看到,我们在通过构造函数构造AtomicInteger原子变量的时候,如果指定一个int的参数,那么该原子变量的值就会被赋值,否则就是默认的数值0。

    也有获取和设置这个value值的方法:

    public final int get()
    public final void set(int newValue) 

    当然,这两个方法并不是原子的,所以一般也很少使用,而以下的这些基于原子操作的方法则相对使用的频繁:

    //基于原子操作,获取当前原子变量中的值并为其设置新值
    public final int getAndSet(int newValue)
    //基于原子操作,比较当前的value是否等于expect,如果是设置为update并返回true,否则返回false
    public final boolean compareAndSet(int expect, int update)
    //基于原子操作,获取当前的value值并自增一
    public final int getAndIncrement()
    //基于原子操作,获取当前的value值并自减一
    public final int getAndDecrement()
    //基于原子操作,获取当前的value值并为value加上delta
    public final int getAndAdd(int delta)
    //还有一些反向的方法,比如:先自增在获取值的等等

    下面我们实现一个计数器的例子,之前我们使用synchronized实现过,现在我们使用原子变量再次实现该问题。

    //自定义一个线程类
    public class MyThread extends Thread {
    
        public static AtomicInteger value = new AtomicInteger();
    
        @Override
        public void run(){
            try {
                Thread.sleep((long) ((Math.random())*100));
                //原子自增
                value.incrementAndGet();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //main函数中启动100条线程并让他们启动
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[100];
        for (int i=0;i<100;i++){
            threads[i] = new MyThread();
            threads[i].start();
        }
    
        for (int j=0;j<100;j++){
            threads[j].join();
        }
    
        System.out.println("value:"+MyThread.value);
    }

    AtomicInteger的内部实现原理

    AtomicInteger的实现原理有点像我们的包装类,内部主要操作的是value字段,这个字段保存就是原子变量的数值。value字段定义如下:

    private volatile int value;

    首先value字段被volatile修饰,即不存在内存可见性问题。由于其内部实现原子操作的代码几乎类似,我们主要学习下incrementAndGet方法的实现。

    在揭露该方法的实现原理之前,我们先看另一个方法:

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

    compareAndSet方法又被称为CAS,该方法调用unsafe的一个compareAndSwapInt方法,这个方法是native,我们看不到源码,但是我们需要知道该方法完成的一个目标:比较当前原子变量的值是否等于expect,如果是则将其修改为update并返回true,否则直接返回false。当然,这个操作本身就是原子的,较为底层的实现。

    在jdk1.7之前,我们的incrementAndGet方法是这样实现的:

    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

    方法体是一个死循环,current获取到当前原子变量中的值,由于value被修饰volatile,所以不存在内存可见性问题,数据一定是最新的。然后current加一后赋值给next,调用我们的CAS原子操作判断value是否被别的线程修改过,如果还是原来的值,那么将next的值赋值给value并返回next,否则重新获取当前value的值,再次进行判断,直到操作完成。

    incrementAndGet方法的一个很核心的思想是,在加一之前先去看看value的值是多少,真正加的时候再去看一下,如果发现变了,不操作数据,否则为value加一。

    但是在jdk1.8以后,做了一些优化,但是最后还是调用的compareAndSwapInt方法。但基本思想还是没变。

    三、AtomicReference的基本使用

    示例:基于 AtomicReference 实现一个简单的自旋锁

    public class AtomicReferenceDemo2 {
    
        private static int ticket = 10;
    
        public static void main(String[] args) {
            threadSafeDemo();
        }
    
        private static void threadSafeDemo() {
            SpinLock lock = new SpinLock();
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 5; i++) {
                executorService.execute(new MyThread(lock));
            }
            executorService.shutdown();
        }
    
        /**
         * 基于 {@link AtomicReference} 实现的简单自旋锁
         */
        static class SpinLock {
    
            private AtomicReference<Thread> atomicReference = new AtomicReference<>();
    
            public void lock() {
                Thread current = Thread.currentThread();
                while (!atomicReference.compareAndSet(null, current)) {}
            }
    
            public void unlock() {
                Thread current = Thread.currentThread();
                atomicReference.compareAndSet(current, null);
            }
    
        }
    
        /**
         * 利用自旋锁 {@link SpinLock} 并发处理数据
         */
        static class MyThread implements Runnable {
    
            private SpinLock lock;
    
            public MyThread(SpinLock lock) {
                this.lock = lock;
            }
    
            @Override
            public void run() {
                while (ticket > 0) {
                    lock.lock();
                    if (ticket > 0) {
                        System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票");
                        ticket--;
                    }
                    lock.unlock();
                }
            }
    
        }
    
    }

    原子类的实现基于 CAS 机制,而 CAS 存在 ABA 问题。正是为了解决 ABA 问题,才有了 AtomicMarkableReference 和 AtomicStampedReference。

    AtomicMarkableReference 使用一个布尔值作为标记,修改时在 true / false 之间切换。这种策略不能根本上解决 ABA 问题,但是可以降低 ABA 发生的几率。常用于缓存或者状态描述这样的场景。

    public class AtomicMarkableReferenceDemo {
    
        private final static String INIT_TEXT = "abc";
    
        public static void main(String[] args) throws InterruptedException {
    
            final AtomicMarkableReference<String> amr = new AtomicMarkableReference<>(INIT_TEXT, false);
    
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 10; i++) {
                executorService.submit(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(Math.abs((int) (Math.random() * 100)));
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        String name = Thread.currentThread().getName();
                        if (amr.compareAndSet(INIT_TEXT, name, amr.isMarked(), !amr.isMarked())) {
                            System.out.println(Thread.currentThread().getName() + " 修改了对象!");
                            System.out.println("新的对象为:" + amr.getReference());
                        }
                    }
                });
            }
    
            executorService.shutdown();
            executorService.awaitTermination(3, TimeUnit.SECONDS);
        }
    
    }

    AtomicStampedReference 使用一个整型值做为版本号,每次更新前先比较版本号,如果一致,才进行修改。通过这种策略,可以根本上解决 ABA 问题。

    public class AtomicStampedReferenceDemo {
    
        private final static String INIT_REF = "pool-1-thread-3";
    
        private final static AtomicStampedReference<String> asr = new AtomicStampedReference<>(INIT_REF, 0);
    
        public static void main(String[] args) throws InterruptedException {
    
            System.out.println("初始对象为:" + asr.getReference());
    
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 3; i++) {
                executorService.execute(new MyThread());
            }
    
            executorService.shutdown();
            executorService.awaitTermination(3, TimeUnit.SECONDS);
        }
    
        static class MyThread implements Runnable {
    
            @Override
            public void run() {
                try {
                    Thread.sleep(Math.abs((int) (Math.random() * 100)));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                final int stamp = asr.getStamp();
                if (asr.compareAndSet(INIT_REF, Thread.currentThread().getName(), stamp, stamp + 1)) {
                    System.out.println(Thread.currentThread().getName() + " 修改了对象!");
                    System.out.println("新的对象为:" + asr.getReference());
                }
            }
    
        }
    
    }

    四、数组类型的原子类

    Java 提供了以下针对数组的原子类:

    • AtomicIntegerArray - 整形数组原子类
    • AtomicLongArray - 长整型数组原子类
    • AtomicReferenceArray - 引用类型数组原子类

    已经有了针对基本类型和引用类型的原子类,为什么还要提供针对数组的原子类呢?

    数组类型的原子类为 数组元素 提供了 volatile 类型的访问语义,这是普通数组所不具备的特性——volatile 类型的数组仅在数组引用上具有 volatile 语义。

    示例:AtomicIntegerArray 使用示例(AtomicLongArray 、AtomicReferenceArray 使用方式也类似)

    public class AtomicIntegerArrayDemo {
    
        private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
    
        public static void main(final String[] arguments) throws InterruptedException {
    
            System.out.println("Init Values: ");
            for (int i = 0; i < atomicIntegerArray.length(); i++) {
                atomicIntegerArray.set(i, i);
                System.out.print(atomicIntegerArray.get(i) + " ");
            }
            System.out.println();
    
            Thread t1 = new Thread(new Increment());
            Thread t2 = new Thread(new Compare());
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            System.out.println("Final Values: ");
            for (int i = 0; i < atomicIntegerArray.length(); i++) {
                System.out.print(atomicIntegerArray.get(i) + " ");
            }
            System.out.println();
        }
    
        static class Increment implements Runnable {
    
            @Override
            public void run() {
    
                for (int i = 0; i < atomicIntegerArray.length(); i++) {
                    int value = atomicIntegerArray.incrementAndGet(i);
                    System.out.println(Thread.currentThread().getName() + ", index = " + i + ", value = " + value);
                }
            }
    
        }
    
        static class Compare implements Runnable {
    
            @Override
            public void run() {
                for (int i = 0; i < atomicIntegerArray.length(); i++) {
                    boolean swapped = atomicIntegerArray.compareAndSet(i, 2, 3);
                    if (swapped) {
                        System.out.println(Thread.currentThread().getName() + " swapped, index = " + i + ", value = 3");
                    }
                }
            }
    
        }
    
    }

    五、属性更新器类型

    更新器类支持基于反射机制的更新字段值的原子操作。

    • AtomicIntegerFieldUpdater - 整型字段的原子更新器。
    • AtomicLongFieldUpdater - 长整型字段的原子更新器。
    • AtomicReferenceFieldUpdater - 原子更新引用类型里的字段。

    这些类的使用有一定限制:

    • 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater() 创建一个更新器,并且需要设置想要更新的类和属性。
    • 字段必须是 volatile 类型的;
    • 不能作用于静态变量(static);
    • 不能作用于常量(final);
    public class AtomicReferenceFieldUpdaterDemo {
    
        static User user = new User("begin");
    
        static AtomicReferenceFieldUpdater<User, String> updater =
            AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 5; i++) {
                executorService.execute(new MyThread());
            }
            executorService.shutdown();
        }
    
        static class MyThread implements Runnable {
    
            @Override
            public void run() {
                if (updater.compareAndSet(user, "begin", "end")) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 已修改 name = " + user.getName());
                } else {
                    System.out.println(Thread.currentThread().getName() + " 已被其他线程修改");
                }
            }
    
        }
    
        static class User {
    
            volatile String name;
    
            public User(String name) {
                this.name = name;
            }
    
            public String getName() {
                return name;
            }
    
            public User setName(String name) {
                this.name = name;
                return this;
            }
    
        }
    
    }

    六、参考

    https://www.cnblogs.com/jingmoxukong/p/12109049.html

    https://www.cnblogs.com/yangming1996/p/7709395.html

     

    展开全文
  • 1 解决问题 对于服务器而言,锁发生的场景主要是在多线程或多进程,多任务的操作系统中,避免不了会共享一些资源,就会出现线程a,... 原子操作:当然除了上面的锁能解决这种问题,原子操作也能解决,注意是无锁的 ...

    1 解决问题

    对于服务器而言,锁发生的场景主要是在多线程或多进程,多任务的操作系统中,避免不了会共享一些资源,就会出现线程a,线程b或者进程a,进程b同时操作同一资源(临界区)的问题会产生无法预料的现象,副作用。所以需要加锁。

    2 锁的分类

    按照自己的理解主要分为两大类:posix api锁和分布式锁
    posix api锁:互斥锁,自旋锁,读写锁
    分布式锁:乐观锁,悲观锁等,目前不总结,等后面再分布式技术(redis,mysql,nginx等集群中)总结吧
    原子操作:当然除了上面的锁能解决这种问题,原子操作也能解决,注意是无锁的

    3 锁的经典问题

    程序中定义一个全局变量idx,然后开10个线程对这个全局变量idx++操作10万次,主线程统计idx的大小;按照一般的理解执行完后,idx应该是100万?

    #define THREAD_SIZE 10
    int idx = 0;
    
    void *func(void *arg){
            int *pcount = (int*)arg; // 用指针是对count变量修改
    
            int i = 0;
            while(i++ < 100000){
                    (*pcount)++;
                    usleep(1);
            }
    }
    
    int main(){
            pthread_t threadid[THREAD_SIZE] = {0};
    
            // 开启了十个子线程
            int i = 0;
            int idx= 0;
            for(i = 0; i <THREAD_SIZE; i++){
                    // 线程id:在其它的线程中关闭或者取消当前线程才会用     
                    // NULL:线程的大小
                    int ret = pthread_create(&threadid[i], NULL, func, &idx);
                    if (ret){
                            break;
                    }
            }
    
            // 主线程做统计 
            for(i = 0; i < 100; i++){
                    printf(" idx:%d\n", idx);
                    sleep(1);
            }
            return 0;
    }
    

    结果:程序执行一段时间后;count就会进入一个稳态,无法达到100万。
    在这里插入图片描述
    原因:

    a:count++底层具体操作
    c/c++程序的整个过程:
    在这里插入图片描述
    对于count++在底层汇编会生成三条汇编指令;
    在这里插入图片描述
    【idx】:内存中的idx值;%eax:cup寄存器值
    三条指令:将内存中的idx的值,拷贝到cpu寄存器eax;然后将eax寄存器加1;最后将cup寄存器eax中的值拷贝到内存idx中来即可;

    b:多线程多进程环境下操作idx值流程:
    大部分情况下是正常执行的,但是也有一些情况下会将这三条指令打算执行;如下图:
    在这里插入图片描述
    按照右边的图形,两个线程同时操作可能出现的情况,产生这种现象的原因主要是由于线程切换导致
    c:线程切换的原理流程:
    单核cpu:以x86为例,cpu里面有16个寄存器。寄存器值直接用unsigned long存储。
    在这里插入图片描述
    正常情况下cpu执行线程1时,内核会将线程1的对应的寄存器拷贝到cpu中去计算加1得到51后;当发生线程切换,切换到线程2时候,在线程2中初始值还是50,这个时候将线程2中的寄存器拷贝到cpu寄存器中去,在不发生切换,则将cpu计算好的值拷贝到内存中去,我们应用程序看到的值了。
    对于多核机器来讲,就是在每个cpu核心下面挂载了一个就绪线程队列来处理

    对于线程切换是否消耗性能资源?
    看以只有mov指令操作,实际上还有fd,文件系统相关属性,内存(虚拟内存)消耗比较大,全部要重新计算;正是因为线程切换消耗性能比较大,所以才产生了协程,专门用来解决线程切换的问题的。

    4 经典问题的解法(几种常见的锁)

    互斥锁(mutex_lock),自旋锁(spin_lock),读写锁,原子操作;
    对于互斥锁,自旋锁,读写锁是将三条汇编指令,要么让每次只能让一个线程执行,要么执行成功,要么不能执行成功;一个线程执行的时,另外一个线程不能执行变成休眠状态,一直尝试,或者一直空转等操作;而对于原子操作是将三条指令变成一条指令执行即可。
    1 互斥锁
    a:mutex_lock:
    本质:当线程1在使用mutex_lock锁时;线程2也运行遇到时,此时线程1在使用,线程2则会进行一系列的动作进入线程切换到休眠状态(阻塞状态)
    接口:

    // 1 定义互斥锁
    pthread_mutex_t mutex;
    //2 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);
    // 3 加锁
    pthread_mutex_lock(&mutex); 
    

    具体使用:
    在这里插入图片描述
    b:mutex_trylock 互斥尝试锁
    mutex_trylock是优化过的互斥锁,上面的互斥锁会直接切换线程。
    本质:当线程1在执行的时候,同一时刻线程2也遇到时,尝试获取锁,发现获取不到,则线程2去做其它的事情,不做线程切换的工作;比如在线程回调函数的循环中直接continue也行
    接口:

    // 1 定义互斥锁
    pthread_mutex_t mutex;
    //2 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);
    // 3 加锁
    pthread_mutex_trylock(&mutex)
    

    具体实现:
    在这里插入图片描述
    两者比较
    mutex_lock:线程会休眠产生线程切换现象;
    mutex_trylock:线程可能会产生休眠线程切换,主要是因为应用层的循环判断来处理的,例如在上面的循环中调用系统调用就会发生线程切换了;能快速的知道锁是否释放;
    上面两者的快速差别不是太大。
    疑问?对于mutex_lock锁来讲,当线程1在使用锁,线程2,线程3同时遇到时,会变成睡眠进入线程切换;当线程1使用完后,内核是如何通知通知线程2,线程3(或者说是如何唤醒的)???
    实际工业生产中,还是优先使用mutex_trylock锁,但是他的逻辑比mutex_lock稍微复杂一点

    c:spin_lock:自旋锁
    本质:线程1在使用自旋锁的时候,当线程2遇到时,cpu一直在自旋等待,不会进行线程切换。
    接口:

    // 1 定义自旋锁
    pthread_spinlock_t spinlock;
    //2 初始化自旋锁
    pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);
    // 3 加锁
    pthread_spin_lock(&spinlock);
    // 4 解锁
    pthread_spin_unlock(&spinlock);
    

    使用:
    在这里插入图片描述
    三种锁的区别:
    例子:嘛聪去东莞酒店钊小翠(老熟人)的故事
    lock:嘛聪去找小翠时候,小翠在服务其它的客户端(引起线程切换,挂起);则嘛聪离开酒店,去另外餐厅吃饭
    trylock:嘛聪去找小翠时候,小翠在服务器其它客户;但是嘛聪比较猴急,就在酒店里面坐着,过一段时间就去问一下好了没(可能引起线程切换)
    spinlock:小翠正在服务嘛聪,其它客户端都知道嘛聪时间段,则其它客户都是在门口等;此时cpu一直在空转;线程不会切换。

    三种锁的使用场景
    时间长用mutex_lock;时间短用spin_lock;时间长短是以是否超过线程切换的时间,超过叫长,没有超过就是短。
    红黑树和B树要做成线程安全,对整个树进行加锁就需要用mutex_lock。
    链表添加,删除几行代码就采用spinlock锁;对指令较多,操作复杂的用mutex_lock

    d:读写锁
    本质:一般不推荐使用,逻辑实现起来比较复杂。
    分类:写加锁,读加锁两种;主要应用于读多谢少常见,可采用。
    接口:解锁是同一个接口

    // 1 定义自旋锁
    pthread_rwlock_t rwlock;
     //2 初始化读写锁
     pthread_rwlock_init(&rwlock, NULL);
    // 3 加锁
    pthread_rwlock_wrlock(&rwlock);
    pthread_rwlock_unlock(&rwlock);
     // 3 读加锁
     pthread_rwlock_rdlock(&rwlock);
     pthread_rwlock_unlock(&rwlock);
    

    使用场景写只能一个线程(同一时刻);读可多个线程(同一时刻)

    对于互斥锁,互斥尝试锁,自旋锁,读写锁都是将汇编的三条指令加锁结合在一起,同一时刻只能让一个线程执行。

    展开全文
  • 近几年,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层原子机器指令(例如比较交换指令)代替锁来确保数据在并发访问中的一致性。非阻塞算法被广泛地用于操作系统和JVM中实现线程/进程调度机制、...

    在java.util.concurrent包的许多类中,如Semaphore和ConcurrentLinkedQueue,都提供了比Synchronized机制更高的性能和可伸缩性。
    近几年,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令(例如比较交换指令)代替锁来确保数据在并发访问中的一致性。非阻塞算法被广泛地用于操作系统和JVM中实现线程/进程调度机制垃圾回收机制锁和其他并发数据结构
    与基于锁的方案相比,非阻塞算法尽管在设计和实现上复杂得多,但它们在可伸缩性和活跃性上却拥有巨大的优势。非阻塞算法可使多个线程在竞争相同数据时不会发生阻塞,因此它能在粒度更细的层次上进行协调,并极大地减少调度开销。而且,在非阻塞算法中不存在死锁和其他活跃性问题。在基于锁的算法中,如果一个线程在休眠或自旋的同时持有一个锁,那么其他线程都无法执行下去,而非阻塞算法不会受到单个线程失败的影响。从Java 5.0开始,可以使用原子变量类(如AtomicInteger和AtomicReference)来构建高效的非阻塞算法。此外,原子变量除了用于非阻塞算法的开发,还可用做一种“更好的volatile类型变量”使用。原子变量除了提供与volatile类型变量相同的内存语义外,还支持原子的更新操作。

    锁的劣势

    使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有守护变量的锁,都能采用独占方式来访问这些变量,并且对变量的任何修改对随后所获得这个锁的其他线程都是可见的。
    如果有多个线程同时请求锁,那么JVM就需要借助操作系统的功能。如果出现这种情况,那么这些线程的挂起和恢复等过程会存在很大的开销,并且存在较长时间的中断。如果在基于锁的类中包含细粒度的操作,那么当锁上存在激烈的竞争时,调度开销与工作开销的比值会非常高。
    锁还存在一些其他缺点。当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的情况下被延迟执行。如果被阻塞线程的优先级较高,而持有锁的线程优先级较低,那么这将是一个严重的问题——也被称为优先级反转(Priority Inversion)。如果持有锁的线程被永久地阻塞,那么所有等待这个锁的线程就永远无法执行下去。
    除此之外,锁定方式对细粒度的操作来说仍是一种高开销的机制。

    硬件对并发的支持

    独占锁是一种悲观技术——它假设最坏的情况,并且只有在确保其他线程不会造成干扰的情况下才能执行下去。
    对于细粒度的操作,还有一种更高效的方法,也是一种乐观的方法,通过这种方法可以在不发生干扰的情况下完成更新操作。
    在针对多处理器操作而设计的处理器中提供了一些特殊指令,用于管理对共享数据的并发访问。现在,几乎所有的现代处理器都包含了某种形式的原子读-改-写指令,如比较并交换(Compare-and-Swap)或者关联加载/条件存储(Load-Linked/Store-Conditional)。操作系统和JVM使用这些指令来实现锁和并发的数据结构,但在Java 5.0之前,在Java类中还不能直接使用这些指令

    比较并交换(Compare-And-Swap,CAS)

    在大多数处理器架构中采用的方法是实现一种比较并交换指令。CAS包含3个操作数——需要读写的内存位置V、进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。CAS的含义是:“我认为V的值应该是A,如果是,那么将V的值更新成B,否则不修改并告诉V的值实际是多少”。CAS是一项乐观技术,它希望能成功地执行操作,并且如果有另一个线程在最近一次检查后更新了该变量,那么CAS能检测到这个错误。
    当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。然而,失败的线程并不会被挂起,而是被告知在这次竞争中失败,并可再次尝试。这种灵活性大大减少了与锁相关的活跃性风险。
    CAS的典型使用模式是:首先从V中读取A,并根据A计算新值B,然后再通过CAS以原子方式将V中的值由A变成B(只要在这期间没有任何线程将V的值修改为其他值)。由于CAS能检测到来自其他线程的干扰,因此即使不使用锁也能实现原子的读—改—写操作序列。
    CAS的主要缺点是,它将使调用者处理竞争问题(通过重试,回退,放弃),而在锁定中能自动处理竞争问题(线程在获得锁之前将一直阻塞)。(事实上,CAS最大的缺点在于难以围绕着CAS正确地构建外部算法)
    CAS的性能要进行实测。

    JVM对CAS的支持

    在Java 5.0中引入了底层的支持,在int、long和对象的引用等类型上都公开了CAS操作,并且JVM把它们编译为底层硬件提供的最有效方法。在支持CAS的平台上,运行时把它们编译为相应的(多条)机器指令。在最坏的情况下,如果不支持CAS指令,那么JVM将使用自旋锁。
    在原子变量类(如java.util.conncurrent.atomic中的AtomicXxx)中使用了这些底层的JVM支持为数字类型和引用类型提供一种高效的CAS操作,而且在java.util.concurrent中大多数类在实现时则直接或间接的使用这些原子变量类。

    原子变量类

    原子变量比锁的粒度更细,量级更轻,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的。原子变量将发生竞争的范围缩小到单个变量上,从而获得最细的粒度。在使用基于原子变量而非锁的算法中,线程在执行时更不易出现延迟,并且如果遇到竞争,也更容易恢复过来。
    原子变量类相当于一个泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。以AtomicInteger为例,该原子类表示一个int类型的值,并提供get和set方法,这些volatile类型的int变量在读取和写入上有着相同的语义。它还提供了一原子的compareAndSet方法,以及原子的添加、递增和递减等方法。AtomicInteger直接利用了硬件对并发的支持,因此在发生竞争的情况下,能提供更高的可伸缩性。
    共有12个原子变量类,可分为四组:标量类(Scalar)、更新器类、数组类及复合变量类。所有这些类都支持CAS,此外AtomicInteger和AtomicLong还支持算术运算。
    原子的标量类没有扩展一些基本的包装类,这是因为:基本类型的包装类是不可修改的,而原子变量类是可修改的。在原子变量类中同样没有重新定义hashCode或equals方法,每个实例都是不同的。

    非阻塞算法

    如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法被称为非阻塞算法。如果在算法的每个步骤中都存在某个线程能够执行下去,那么这种算法被称为无锁(Lock-Free)算法。如果在算法中仅将CAS用于协调线程之间的操作,并且能正确地实现,那么它既是一种无阻塞算法,又是一种无锁算法。
    创建非阻塞算法的关键在于,找出如何将原子修改的范围缩小到单位变量上,同时还要维护数据的一致性。

    非阻塞的栈

    暂无

    非阻塞的链表

    暂无

    原子的域更新器

    原子的域更新器表示现有volatile域的一种基于反射的“视图”,从而能够在已有的volatile域上使用CAS。
    ConcurrentLinkedQueue

    ABA问题

    CAS存在ABA问题。在某些算法中,如果V的值首先由A变成B,再由B变成A,那么仍然被认为是发生了变化,并需要重新执行算法中的某些步骤。
    如果在算法中采用自己的方式来管理节点对象的内存,那么可能出现ABA问题。一种相对简单的解决方案是:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值由A变为B,然后又变为A,版本号也将是不同的。
    AtomicStampedReference将更新一个“对象-引用”二元组,通过在引用上加上“版本号”,从而避免ABA问题。

    总结

    非阻塞同步机制比基于锁的阻塞同步机制,尽管设计和实现更复杂,但在可伸缩性和活跃性上却拥有巨大的优势。从Java 5.0开始,可以使用原子变量类来构建高效的非阻塞算法。原子变量除了用于非阻塞算法的开发,还可用做一种“更好的volatile类型变量”使用。原子变量除了提供与volatile类型变量相同的内存语义外,还支持原子的更新操作。
    非阻塞算法在设计和实现上非常困难,但通常能提供更高的可伸缩性,并能更好的防止活跃性故障的发生。

    原创不易,如果本文对您有帮助,欢迎关注我,谢谢 ~_~

    展开全文
  • Java代码在编译后会变成Java...一、volatile的应用如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。1. volatile的定义与实现原理CPU术语表如下所示:那么,volatile是如何...

    Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转换为汇编指令在CPU上执行。

    Java中所使用的并发机制依赖于JVM的实现和CPU的指令。

    一、volatile的应用

    如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

    1. volatile的定义与实现原理

    CPU术语表如下所示:

    cpu_concepts_table.jpg

    那么,volatile是如何保证可见性的呢?

    现在,我们看示例代码,如下所示:

    instance = new Singleton(); //instance是volatile变量

    转变为汇编代码,如下所示:

    0x01a3de1d: movb $0×0,0×1104800(%esi);

    0x01a3de24: lock addl $0×0,(%esp);

    有volatile修饰的共享变量进行写操作时会多出第二行汇编代码。

    lock前缀的指令在多核处理器下会引发两件事:

    将当前处理器缓存行的数据写回到主内存;

    写回主内存操作会使其他CPU里缓存了该内存地址的数据失效。

    在详细介绍lock指令之前,我们需要对计算机存储层次结构有一个简单的认识:

    为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

    volatile的两条实现原则:

    lock指令会引起处理器缓存回写到内存;

    一个处理器的缓存回写到内存会导致其他处理器的缓存失效。

    2. volatile的使用优化

    使用追加字节的方式来优化队列出队和入队的性能!

    为什么追加字节能够提高并发编程的效率呢?

    因为对于Intel Core、Atom和Pentium M处理器,其L1、L2或L3缓存的高速缓存行是64个字节宽,不支持部分填充缓存行。这意味着,如果队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。

    是不是在使用volatile变量时都应该追加到64字节?不是,两种场景下不能:

    缓存行非64字节宽的处理器;

    共享变量不会被频繁地读写。

    二、synchronized的应用

    1. 锁的实现原理

    synchronized实现同步的基础:Java中的每一个对象都可以作为锁。

    具体表现为:

    对于普通同步方法,锁是当前实例对象;

    对于静态同步方法,锁是当前类的Class对象;

    对于同步方法块,锁是synchronized括号中配置的对象。

    JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。monitorenter指令在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

    synchronized所用的锁是存在Java对象头里的。

    2. 锁的对比

    Java中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

    锁的状态会随着竞争情况逐渐升级,锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

    2.1 偏向锁

    背景:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程得到锁的代价更低,故引入了偏向锁。

    方法:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该进程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的MarkWord里是否存储着指向当前线程的偏向锁。

    偏向锁使用了一种等到竞争出现才释放锁的机制,所以,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

    biased_lock_procedure.jpg

    2.2 轻量级锁

    轻量级锁加锁

    线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的MarkWord复制到锁记录中,该过程称之为Displaced MarkWord。然后,线程尝试使用CAS将对象头的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

    轻量级锁解锁

    轻量级锁解锁时,会使用原子CAS操作将Displaced MarkWord替换回对象头,如果成功,则表示竞争没有发生。如果失败,则表示当前锁存在竞争,锁就会膨胀成为重量级锁。

    lightweight_lock_procedure.jpg

    2.3 锁的对比

    jvm_lock_comparation.png

    三、原子操作的实现原理

    原子操作(Atomic Operation):不可被中断的一个或一系列操作。

    1. 术语

    atomic_operation_concepts_table.jpg

    2. 处理器如何实现原子操作

    处理器通过两种方式来实现原子操作:

    使用总线锁保证原子性

    第一个机制是通过总线锁保证原子性。所谓总线锁就是使用处理器提供的一个LOCK信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,则该处理器可以独占共享内存。

    使用缓存锁保证原子性

    第二个机制是通过缓存锁来保证原子性。总线锁开销大,缓存锁开销小。

    两种情况下,处理器不会使用缓存锁:

    当操作的数据不能被缓存在处理器内部或操作的数据跨多个缓存行时,处理器会调用总线锁。

    有些处理其不支持缓存锁。

    3. Java如何实现原子操作

    Java通过两种方式实现原子操作:

    使用循环CAS实现原子操作

    自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

    例如:

    // 使用CAS实现线程安全计数器

    private void safeCount() {

    for (;;) {

    int i = atomicI.get();

    boolean suc = atomicI.compareAndSet(i, ++i);

    if (suc)

    break;

    }

    }

    CAS实现原子操作的三大问题:

    (1)ABA问题

    因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

    解决思路:使用版本号!

    (2)循环时间开销大

    (3)只能保证一个共享变量的原子操作

    使用锁机制实现原子操作

    锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。

    除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想要进入同步块的时候,其使用循环CAS的方式来获取锁,当它退出同步块时,其使用循环CAS的方式来释放锁。

    四、小结

    在本文,我们一起研究了volatile、synchronized和原子操作的实现原理。

    展开全文
  • 前言本文主要介绍关于Redis的五种基本数据结构的底层实现原理,然后来分析我们常用的使用场景。先简单回顾一下知识点。Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件...
  • 而有些操作,底层需要通过多个字节码来完成,这样的操作就不是原子的,因此不是线程安全的。举个例子,a+=1 。反编译这个语句,发现它由4个字节码组成:>>> dis.dis(compile('a+=1', '', 'exec'))1 0 LOAD_...
  • 原子性,也就是要么全部做完,要么全部不做在多进程(线程)访问资源时,能够确保所有其他的进程(线程)都不在同一时间内访问相同的资源。原子操作(atomic operation)是不需要同步,这是Java多线程编程的老生常谈了。...
  • 前面讲线程同步时,我们对多线程容易出现的问题进行了分析,在那个例子中,问题的根源在于c++和c--这两个操作在底层处理的时候被分成了若干步执行。当时我们用的是synchronized关键字来解决这个问题,而从...
  • 进年以来,并发算法领域的重点都围绕在非拥塞算法,该种算法依赖底层硬件对于原子性指令的支持,避免使用锁来维护数据一致性和多线程安全。非拥塞算法虽然在设计上更为复杂,但是拥有更好的可伸缩性和性能,被广泛...
  • 还有其它的解决方案:原子操作 2 volatile关键字 volatile变量:易变的,最早由于高低电平捕获在变,不是通过内存计算而得到的。通过串口,网线来改变,变主要是外界资源变化引起的。 volatile修饰:去优化,就是...
  • CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。 CAS是如何保证线程安全的? 线程在读取数据时不进行加锁,在准备写回数据时,先
  • } } jdk的并发包里提供了很多原子变量,可以在"不加锁"(注:OS底层其实还是有锁的,只不过相对java里的synchronized性能要好很多)的情况下解决这个问题,参考下面的用法: package test; import java.util....
  • 介绍了并发机制的底层实现原理,valatile synchronized的实现原理,CAS 的优缺点,原子性问题等,后面的很多东西,但是要基于这个来实现的,今天就到这了吧 > 因为博主也是一个开发萌新 我也是一边学一边写 我有个...
  • Java原子类及内部原理

    2021-03-03 14:02:57
    一、引入原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++;这个操作实际是a = a + 1;是可分割的,所以他不是一个原子...
  • volatile底层实现原理

    2021-07-21 14:38:15
    Load2 保证Store1的写操作已刷新到主内存后,Load2及之后的读写操作才会执行 StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果 六、volatile的底层原理 volatile 的底层实现原理是内存屏障,...
  • 重量级锁 5、锁消除 6、锁粗化 五、锁升级场景: 转自: java锁升级过程 synchronized底层实现原理及锁优化 一、概述 1、synchronized作用 原子性:synchronized保证语句块内操作是原子的 可见性:synchronized保证...
  • 今天呢我们就手把手来剖析一下ReentrantLock的底层实现。让我们一起来阅读源码吧。 二.ReentrantLock的使用 ReentrantLock的使用我们就不多说了。 //默认非公平锁 ReentrantLock lock = new ReentrantLock(); //...
  • Atomic原子操作在 Java 5.0 提供了java.util.concurrent(简称JUC)包,在此包中增加了在并发...原子变量底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁...
  • 原子操作底层原理,教程操作步骤:引入在Java中实现并发用的最多的就是synchronized关键字了,自从jdk1.6对synchronized进行重大优化后,其广为人诟病的性能问题也得到了改善,与ReentrankLock相比性能方面相差无几...
  • JVM 底层原理总结

    2021-03-13 16:22:50
    分配内存时线程的安全性 对分配内存的动作进行同步处理(实际上虚拟机采用CAS配上失败重试的机制保证了更新操作的原子性)。 把分配内存的动作按照线程划分在不同的空间之中进行(即每个线程在Java堆中预先分配一...
  • 原子操作的实现原理

    2021-08-05 21:03:16
    现在的计算机一般有多个CPU,一个CPU里又有多个核,当多个CPU同时读内存中的某个变量并个性这个变量时,会出现冲突,如两个核同时执行代码 "i++",即两个处理器同时读写同一块内存,出现错误是显然的。 1.2 单...
  • 你心想:看到没我都知道,这种事情难不倒我 这时面试官喝了口水说:小伙子懂得真多 你想这次面试应该问完了吧,可是没想到面试官接着又抛出一个问题:CAS底层实现原理是什么? 靠,没完了是吧,还问底层原理,也怪...
  • 即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的 lock(锁定):作用于主...
  • 因此,Java 中所使用的并发机制其实是依赖于 JVM 的实现和 CPU 的指令,所以了解 Java 并发机制的底层实现原理也是很有必要的 volatile 的应用 volatile 在多处理器开发中保证了共享变量的可见性。可见性的意思是当...
  • ArrayList的底层数据结构是一个数组,数组元素的类型为Object类型,对ArrayList的所有操作底层都是基于数组的。 ArrayList的线程安全性:ArrayList在多线程环境下是线程不安全的,ArrayList添加元素分为两步,第一...
  • 现代操作系统中,一般都提供了原子操作来实现一些同步操作,所谓原子操作,也就是一个独立而不可分割的操作。在单核环境中,一般的意义下原子操作中线程不会被切换,线程切换要么在原子操作之前,要么在原子操作完成...
  • ②可见性:synchronized 保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现)。 ③有序性:synchronized 保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)。 ...
  • 底层通过操作系统原语实现,保证了原子性。在 CAS 中,线程读取数据不用加锁,在准备写回时,比较原值是否修改,若未被修改则写回,若已被修改,则重新执行读取流程,这是一种乐观策略,认为并发冲突并不总会发生。 ...
  • Volatile的作用以及底层实现原理 先来看一段demo的代码: 你会发现,永远都不会输出有点东西这一段代码,按道理线程改了flag变量,主线程也能访问到的呀? 为会出现这个情况呢?那我们就需要聊一下另外一个东西...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 54,276
精华内容 21,710
关键字:

原子变量底层实现