Java并发编程总结——慎用CAS详解

一、CAS和synchronized适用场景

1、对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

2、对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。以java.util.concurrent.atomic包中AtomicInteger类为例,其getAndIncrement()方法实现如下:

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

如果compareAndSet(current, next)方法成功执行,则直接返回;如果线程竞争激烈,导致compareAndSet(current, next)方法一直不能成功执行,则会一直循环等待,直到耗尽cpu分配给该线程的时间片,从而大幅降低效率。

二、CAS错误的使用场景

public class CASDemo {
  private final int THREAD_NUM = 1000;
  private final int MAX_VALUE = 20000000;
  private AtomicInteger casI = new AtomicInteger(0);
  private int syncI = 0;
  private String path = "/Users/pingping/DataCenter/Books/Linux/Linux常用命令详解.txt";

  public void casAdd() throws InterruptedException {
    long begin = System.currentTimeMillis();
    Thread[] threads = new Thread[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i++) {
      threads[i] = new Thread(new Runnable() {
        public void run() {
          while (casI.get() < MAX_VALUE) {
            casI.getAndIncrement();
          }
        }
      });
      threads[i].start();
    }
    for (int j = 0; j < THREAD_NUM; j++) {
      threads[j].join();
    }
    System.out.println("CAS costs time: " + (System.currentTimeMillis() - begin));
  }

  public void syncAdd() throws InterruptedException {
    long begin = System.currentTimeMillis();
    Thread[] threads = new Thread[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i++) {
      threads[i] = new Thread(new Runnable() {
        public void run() {
          while (syncI < MAX_VALUE) {
            synchronized ("syncI") {
              ++syncI;
            }
          }
        }
      });
      threads[i].start();
    }
    for (int j = 0; j < THREAD_NUM; j++)
      threads[j].join();
    System.out.println("sync costs time: " + (System.currentTimeMillis() - begin));
  }
}

在我的双核cpu上运行,结果如下:

可见在不同的线程下,采用CAS计算消耗的时间远多于使用synchronized方式。原因在于第15行

14           while (casI.get() < MAX_VALUE) {
15             casI.getAndIncrement();
16           }

的操作是一个耗时非常少的操作,15行执行完之后会立刻进入循环,继续执行,从而导致线程冲突严重。

三、改进的CAS使用场景

为了解决上述问题,只需要让每一次循环执行的时间变长,即可以大幅减少线程冲突。修改代码如下:

public class CASDemo {
  private final int THREAD_NUM = 1000;
  private final int MAX_VALUE = 1000;
  private AtomicInteger casI = new AtomicInteger(0);
  private int syncI = 0;
  private String path = "/Users/pingping/DataCenter/Books/Linux/Linux常用命令详解.txt";

  public void casAdd2() throws InterruptedException {
    long begin = System.currentTimeMillis();
    Thread[] threads = new Thread[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i++) {
      threads[i] = new Thread(new Runnable() {
        public void run() {
          while (casI.get() < MAX_VALUE) {
            casI.getAndIncrement();
            try (InputStream in = new FileInputStream(new File(path))) {
                while (in.read() != -1);
            } catch (IOException e) {
              e.printStackTrace();
            }
          }
        }
      });
      threads[i].start();
    }
    for (int j = 0; j < THREAD_NUM; j++)
      threads[j].join();
    System.out.println("CAS Random costs time: " + (System.currentTimeMillis() - begin));
  }

  public void syncAdd2() throws InterruptedException {
    long begin = System.currentTimeMillis();
    Thread[] threads = new Thread[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i++) {
      threads[i] = new Thread(new Runnable() {
        public void run() {
          while (syncI < MAX_VALUE) {
            synchronized ("syncI") {
              ++syncI;
            }
            try (InputStream in = new FileInputStream(new File(path))) {
              while (in.read() != -1);
            } catch (IOException e) {
              e.printStackTrace();
            }
          }
        }
      });
      threads[i].start();
    }
    for (int j = 0; j < THREAD_NUM; j++)
      threads[j].join();
    System.out.println("sync costs time: " + (System.currentTimeMillis() - begin));
  }
}

在while循环中,增加了一个读取文件内容的操作,该操作大概需要耗时40ms,从而可以减少线程冲突。测试结果如下:

可见在资源冲突比较小的情况下,采用CAS方式和synchronized同步效率差不多。为什么CAS相比synchronized没有获得更高的性能呢?

测试使用的jdk为1.7,而从jdk1.6开始,对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。而其中自旋锁的原理,类似于CAS自旋,甚至比CAS自旋更为优化。具体内容请参考 深入JVM锁机制1-synchronized。

四、总结

1、使用CAS在线程冲突严重时,会大幅降低程序性能;CAS只适合于线程冲突较少的情况使用。

2、synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

以上这篇Java并发编程总结——慎用CAS详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

时间: 2016-06-06

Java多线程并发编程(互斥锁Reentrant Lock)

Java 中的锁通常分为两种: 通过关键字 synchronized 获取的锁,我们称为同步锁,上一篇有介绍到:Java 多线程并发编程 Synchronized 关键字. java.util.concurrent(JUC)包里的锁,如通过继承接口 Lock 而实现的 ReentrantLock(互斥锁),继承 ReadWriteLock 实现的 ReentrantReadWriteLock(读写锁). 本篇主要介绍 ReentrantLock(互斥锁). ReentrantLock(互斥锁)

Java 并发编程之线程挂起、恢复与终止

挂起和恢复线程 Thread 的API中包含两个被淘汰的方法,它们用于临时挂起和重启某个线程,这些方法已经被淘汰,因为它们是不安全的,不稳定的.如果在不合适的时候挂起线程(比如,锁定共享资源时),此时便可能会发生死锁条件--其他线程在等待该线程释放锁,但该线程却被挂起了,便会发生死锁.另外,在长时间计算期间挂起线程也可能导致问题. 下面的代码演示了通过休眠来延缓运行,模拟长时间运行的情况,使线程更可能在不适当的时候被挂起: public class DeprecatedSuspendResume

Java 多线程并发编程_动力节点Java学院整理

一.多线程 1.操作系统有两个容易混淆的概念,进程和线程. 进程:一个计算机程序的运行实例,包含了需要执行的指令:有自己的独立地址空间,包含程序内容和数据:不同进程的地址空间是互相隔离的:进程拥有各种资源和状态信息,包括打开的文件.子进程和信号处理. 线程:表示程序的执行流程,是CPU调度执行的基本单位:线程有自己的程序计数器.寄存器.堆栈和帧.同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源. 2.Java标准库提供了进程和线程相关的API,进程主要包括表示进程的jav

Java 并发编程之ThreadLocal详解及实例

Java 理解 ThreadLocal 摘要: ThreadLocal 又名线程局部变量,是 Java 中一种较为特殊的线程绑定机制,用于保证变量在不同线程间的隔离性,以方便每个线程处理自己的状态.进一步地,本文以ThreadLocal类的源码为切入点,深入分析了ThreadLocal类的作用原理,并给出应用场景和一般使用步骤. 一. 对 ThreadLocal 的理解 1). ThreadLocal 概述 ThreadLocal 又名 线程局部变量,是 Java 中一种较为特殊的 线程绑定机制

java并发编程_线程池的使用方法(详解)

一.任务和执行策略之间的隐性耦合 Executor可以将任务的提交和任务的执行策略解耦 只有任务是同类型的且执行时间差别不大,才能发挥最大性能,否则,如将一些耗时长的任务和耗时短的任务放在一个线程池,除非线程池很大,否则会造成死锁等问题 1.线程饥饿死锁 类似于:将两个任务提交给一个单线程池,且两个任务之间相互依赖,一个任务等待另一个任务,则会发生死锁:表现为池不够 定义:某个任务必须等待池中其他任务的运行结果,有可能发生饥饿死锁 2.线程池大小 注意:线程池的大小还受其他的限制,如其他资源池:

Java多线程并发编程 并发三大要素

一.原子性 原子,一个不可再被分割的颗粒.原子性,指的是一个或多个不能再被分割的操作. int i = 1; // 原子操作 i++; // 非原子操作,从主内存读取 i 到线程工作内存,进行 +1,再把 i 写到朱内存. 虽然读取和写入都是原子操作,但合起来就不属于原子操作,我们又叫这种为"复合操作". 我们可以用synchronized 或 Lock 来把这个复合操作"变成"原子操作. 例子: private synchronized void increase

java并发编程之cas详解

CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值.这听起来可能有一点复杂但是实际上你理解之后发现很简单,接下来,让我们跟深入的了解一下这项技术. CAS的使用场景 在程序和算法中一个经常出现的模式就是"check and act"模式.先检查后操作模式发生在代码中首先检查一个变量的值,然后再基于这个值做一些操作.下面是一个

深入分析Java并发编程之CAS

在Java并发编程的世界里,synchronized 和 Lock 是控制多线程并发环境下对共享资源同步访问的两大手段.其中 Lock 是 JDK 层面的锁机制,是轻量级锁,底层使用大量的自旋+CAS操作实现的. 学习并发推荐<Java并发编程的艺术> 那什么是CAS呢?CAS,compare and swap,即比较并交换,什么是比较并交换呢?在Lock锁的理念中,采用的是一种乐观锁的形式,即多线程去修改共享资源时,不是在修改之前就加锁,而是乐观的认为没有别的线程和自己争锁,就是通过CAS的

Java并发编程之Semaphore(信号量)详解及实例

Java并发编程之Semaphore(信号量)详解及实例 概述 通常情况下,可能有多个线程同时访问数目很少的资源,如客户端建立了若干个线程同时访问同一数据库,这势必会造成服务端资源被耗尽的地步,那么怎样能够有效的来控制不可预知的接入量呢?及在同一时刻只能获得指定数目的数据库连接,在JDK1.5 java.util.concurrent 包中引入了Semaphore(信号量),信号量是在简单上锁的基础上实现的,相当于能令线程安全执行,并初始化为可用资源个数的计数器,通常用于限制可以访问某些资源(物

Java并发编程之Condition源码分析(推荐)

Condition介绍 上篇文章讲了ReentrantLock的加锁和释放锁的使用,这篇文章是对ReentrantLock的补充.ReentrantLock#newCondition()可以创建Condition,在ReentrantLock加锁过程中可以利用Condition阻塞当前线程并临时释放锁,待另外线程获取到锁并在逻辑后通知阻塞线程"激活".Condition常用在基于异步通信的同步机制实现中,比如dubbo中的请求和获取应答结果的实现. 常用方法 Condition中主要的

Java并发编程-volatile可见性详解

前言 要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸.最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍. 有什么用? volatile主要对所修饰的变量提供两个功能 可见性 防止指令重排序 <br>本篇博客主要对volatile可见性进行探讨,以后发表关于指令重排序的博文. 什么是可见性? 把JAVA内存模型(JMM)展示得很详细了,简单概括一下 1.每个Thread有一个属于自己的工作内存(可以理解为每个

浅谈Java并发编程之Lock锁和条件变量

简单使用Lock锁 Java 5中引入了新的锁机制--java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接口有3个实现它的类:ReentrantLock.ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁.读锁和写锁.lock必须被显式地创建.锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例

python 编程之twisted详解及简单实例

python 编程之twisted详解 前言: 我不擅长写socket代码.一是用c写起来比较麻烦,二是自己平时也没有这方面的需求.等到自己真正想了解的时候,才发现自己在这方面确实有需要改进的地方.最近由于项目的原因需要写一些Python代码,才发现在python下面开发socket是一件多么爽的事情. 对于大多数socket来说,用户其实只要关注三个事件就可以了.这分别是创建.删除.和收发数据.python中的twisted库正好可以帮助我们完成这么一个目标,实用起来也不麻烦.下面的代码来自t

jsp 编程之@WebServlet详解

编写好Servlet之后,接下来要告诉Web容器有关于这个Servlet的一些信息.在Servlet 3.0中,可以使用标注(Annotation)来告知容器哪些Servlet会提供服务以及额外信息.例如在HelloServlet.java中: @WebServlet("/hello.view") public class HelloServlet extends HttpServlet { 只要在Servlet上设置@WebServlet标注,容器就会自动读取当中的信息.上面的@We

实例讲解Java并发编程之ThreadLocal类

ThreadLocal类可以理解为ThreadLocalVariable(线程局部变量),提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回当前执行线程在调用set时设置的最新值.可以将ThreadLocal<T>视为 包含了Map<Thread,T>对象,保存了特定于该线程的值. 概括起来说,对于多线程资源共享的问题,同步机制采用了"以时间换空间"的方式,而ThreadLocal采用了"以空间