美文网首页
多线程之volatile、原子类、synchronized

多线程之volatile、原子类、synchronized

作者: 俗人浮生 | 来源:发表于2019-01-21 22:15 被阅读0次

在引入volatile、原子类、synchronized前,我们先来说说Java内存模型的三大特性:可见性、原子性和有序性。
可见性:在多线程中,任一线程的修改对其它线程都是可见的。(确保可见性的方法有:volatile、synchronized 和 final)
原子性:指的是一个操作无法再进行分割。(确保原子性的方法有:synchronized、原子类和加lock)
有序性:指代码按程序员编写的顺序串行执行,然而在多线程中会出现指令重排。(确保有序性的方法有: volatile 和 synchronized)
下面,我们举个例子对这多种方案进行一一验证:

    private static final int THREADS_CONUT = 200;
    private static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(() -> {
                for (int i1 = 0; i1 < 100; i1++) {
                    count++;
                    //为了让结果更加明显,此处进行休眠
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("执行结果:"+count);
    }

这是个很常见的线程安全问题的例子,总共有200个线程,然后计数100次,抛开线程安全问题的话,总数应该是200*100=20000,然而某一次的运行结果如下:

执行结果:19687

注意到最后的循环判断是:Thread.activeCount() > 2
这里为什么是大于2而不是1呢?按理来说应该最后只剩下一个主线程才对啊?如果我们把最后一部分进行如下的更改:

  while (Thread.activeCount() > 1) {
      System.out.println("当前线程数:"+Thread.activeCount());
      Thread.currentThread().getThreadGroup().list();
      Thread.yield();
  }

你会发现程序进入了死循环了,不停地循环打印出如下结果:

当前线程数:2
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Thread[Monitor Ctrl-Break,5,main]

其实,从上面的日志我们也可以看出来了,程序运行到最后是包含两个线程的:主线程和守护线程,所以程序才会陷入无限循环中。
再说回线程安全的问题,其实主要的原因在于:count++;这一步并不是原子操作,它可拆分为count=count+1,故在多线程中就会出现上面的线程安全问题。
既然我们今天说的是volatile、原子类、synchronized,那肯定要进行逐一试验的咯:

1、volatile

首先将上述中的count变量用volatile关键字来进行修饰,如下:

private static volatile int count=0;

某次的运行结果如下:

执行结果:19833

事实证明,volatile关键字并不能确保Java内存的原子性,正如上面所说的,volatile关键字能确保的是可见性和有序性,下面来说说volatile关键字是如何确保可见性和有序性的:
我们先来看看多线程中Java内存模型是怎么样的?借用网上的一张图(侵删):


多线程中的内存模型

如上图,我们知道每个线程都有自己独立的工作内存,对于普通变量,线程是不直接操作主内存中的变量,操作的是工作内存从主内存copy过来的副本变量,而使用如果使用了volatile关键字标记的变量,则是直接操作主内存的变量,因此确保了可见性。
另一方面,使用volatile关键字修饰的变量,会禁止指令重排序优化,禁止将后面的指令排到该变量前,相当于建立了一个“内存屏障”,以此来确保有序性。
性能方面,volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

2、原子类

Java 5.0 提供了 java.util.concurrent(简称JUC)包,根据修改的数据类型,可以将JUC包中的原子操作类可以分为4类。

  1. 基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
  2. 数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
  3. 引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
  4. 对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。
    还是回到上面的例子,我们来看看原子类能否解决上面出现的线程安全问题,将代码更改为如下:
    private static final int THREADS_CONUT = 200;
    private  static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(() -> {
                for (int i1 = 0; i1 < 100; i1++) {
                    count.incrementAndGet();
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("执行结果:"+count);
    }

执行结果:20000

事实证明,“原子类”这个名字可不是白叫的,果然能保证其原子性。
那么,原子类实现的原理又是什么呢?其实,原子类是基于CAS实现,而CAS是通过硬件命令保证了原子性,所以在性能方面有一定的优势。
CAS是一种“乐观锁”技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。
CAS存在着ABA问题:在更新前的值是A,但在操作过程中被其他线程更新为B,又更新为 A。此时,按照上面的规则,当前线程判断可执行,但其实发生了不一致现象,需评估是否对程序存在影响(极少极少)。

3、synchronized

与“乐观锁”相对应的是“悲观锁”,而synchronized就是一种悲观锁技术。
乐观锁相信的是在它修改之前,没有其它线程去修改它。
悲观锁则认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。
同样的,我们使用synchronized再次更改上面的例子:

    private static final int THREADS_CONUT = 200;
    private static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(() -> {
                for (int i1 = 0; i1 < 100; i1++) {
                    synchronized (threads){
                        count++;
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(count);
    }

执行结果:20000

正如最开始说的,使用synchronized可以保证Java内存的三大特性,它是一种悲观锁,它只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
所以,如果如上面的例子的话,使用原子类会比synchronized在性能上有更大的优势!

相关文章

网友评论

      本文标题:多线程之volatile、原子类、synchronized

      本文链接:https://www.haomeiwen.com/subject/jtevdqtx.html