精华内容
下载资源
问答
  • 深入理解Java并发之synchronized实现原理

    万次阅读 多人点赞 2017-06-04 17:44:44
    【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) ... 出自【zejian的博客】...深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深...

    【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
    http://blog.csdn.net/javazejian/article/details/72828483
    出自【zejian的博客】

    关联文章:

    深入理解Java类型信息(Class对象)与反射机制

    深入理解Java枚举类型(enum)

    深入理解Java注解类型(@Annotation)

    深入理解Java类加载器(ClassLoader)

    深入理解Java并发之synchronized实现原理

    Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

    深入理解Java内存模型(JMM)及volatile关键字

    剖析基于并发AQS的重入锁(ReetrantLock)及其Condition实现原理

    剖析基于并发AQS的共享锁的实现(基于信号量Semaphore)

    并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue

    本篇主要是对Java并发中synchronized关键字进行较为深入的探索,这些知识点结合博主对synchronized的个人理解以及相关的书籍的讲解(在结尾参考资料),如有误处,欢迎留言。

    线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。

    synchronized的三种应用方式

    synchronized关键字最主要有以下3种应用方式,下面分别介绍

    • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

    • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

    • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    synchronized作用于实例方法

    所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意是实例方法不包括静态方法,如下

    public class AccountingSync implements Runnable{
        //共享资源(临界资源)
        static int i=0;
    
        /**
         * synchronized 修饰实例方法
         */
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            AccountingSync instance=new AccountingSync();
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
        /**
         * 输出结果:
         * 2000000
         */
    }

    上述代码中,我们开启两个线程操作同一个共享资源即变量i,由于i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,如下代码将演示出该现象

    public class AccountingSyncBad implements Runnable{
        static int i=0;
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            //new新实例
            Thread t1=new Thread(new AccountingSyncBad());
            //new新实例
            Thread t2=new Thread(new AccountingSyncBad());
            t1.start();
            t2.start();
            //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }

    上述代码与前面不同的是我们同时创建了两个新实例AccountingSyncBad,然后启动两个不同的线程对共享变量i进行操作,但很遗憾操作结果是1452317而不是期望结果2000000,因为上述代码犯了严重的错误,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法。

    synchronized作用于静态方法

    当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,看如下代码

    public class AccountingSyncClass implements Runnable{
        static int i=0;
    
        /**
         * 作用于静态方法,锁是当前class对象,也就是
         * AccountingSyncClass类对应的class对象
         */
        public static synchronized void increase(){
            i++;
        }
    
        /**
         * 非静态,访问时锁不一样不会发生互斥
         */
        public synchronized void increase4Obj(){
            i++;
        }
    
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            //new新实例
            Thread t1=new Thread(new AccountingSyncClass());
            //new心事了
            Thread t2=new Thread(new AccountingSyncClass());
            //启动线程
            t1.start();t2.start();
    
            t1.join();t2.join();
            System.out.println(i);
        }
    }

    由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。

    synchronized同步代码块

    除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:

    public class AccountingSync implements Runnable{
        static AccountingSync instance=new AccountingSync();
        static int i=0;
        @Override
        public void run() {
            //省略其他耗时操作....
            //使用同步代码块对变量i进行同步操作,锁对象为instance
            synchronized(instance){
                for(int j=0;j<1000000;j++){
                        i++;
                  }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();t2.start();
            t1.join();t2.join();
            System.out.println(i);
        }
    }
    

    从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

    //this,当前实例对象锁
    synchronized(this){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }
    
    //class对象锁
    synchronized(AccountingSync.class){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }

    了解完synchronized的基本含义及其使用方式后,下面我们将进一步深入理解synchronized的底层实现原理。

    synchronized底层语义原理

    Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。

    理解Java对象头与Monitor

    在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:

    • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

    • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

    而对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:

    虚拟机位数 头对象结构 说明
    32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
    32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

    其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构

    锁状态 25bit 4bit 1bit是否是偏向锁 2bit 锁标志位
    无锁状态 对象HashCode 对象分代年龄 0 01

    由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

    其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的,稍后我们会简要分析。这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //记录个数
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL;
        _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
      }

    ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

    由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析),ok~,有了上述知识基础后,下面我们将进一步分析synchronized在字节码层面的具体语义实现。

    synchronized代码块底层原理

    现在我们重新定义一个synchronized修饰的同步代码块,在代码块中操作共享变量i,如下

    public class SyncCodeBlock {
    
       public int i;
    
       public void syncTask(){
           //同步代码库
           synchronized (this){
               i++;
           }
       }
    }

    编译上述代码并使用javap反编译后得到字节码如下(这里我们省略一部分没有必要的信息):

    Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
      Last modified 2017-6-2; size 426 bytes
      MD5 checksum c80bc322c87b312de760942820b4fed5
      Compiled from "SyncCodeBlock.java"
    public class com.zejian.concurrencys.SyncCodeBlock
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
      //........省略常量池中数据
      //构造函数
      public com.zejian.concurrencys.SyncCodeBlock();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 7: 0
      //===========主要看看syncTask方法实现================
      public void syncTask();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=3, args_size=1
             0: aload_0
             1: dup
             2: astore_1
             3: monitorenter  //注意此处,进入同步方法
             4: aload_0
             5: dup
             6: getfield      #2             // Field i:I
             9: iconst_1
            10: iadd
            11: putfield      #2            // Field i:I
            14: aload_1
            15: monitorexit   //注意此处,退出同步方法
            16: goto          24
            19: astore_2
            20: aload_1
            21: monitorexit //注意此处,退出同步方法
            22: aload_2
            23: athrow
            24: return
          Exception table:
          //省略其他字节码.......
    }
    SourceFile: "SyncCodeBlock.java"

    我们主要关注字节码中的如下代码

    3: monitorenter  //进入同步方法
    //..........省略其他  
    15: monitorexit   //退出同步方法
    16: goto          24
    //省略其他.......
    21: monitorexit //退出同步方法

    从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

    synchronized方法底层原理

    方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现:

    public class SyncMethod {
    
       public int i;
    
       public synchronized void syncTask(){
               i++;
       }
    }

    使用javap反编译后的字节码如下:

    Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
      Last modified 2017-6-2; size 308 bytes
      MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
      Compiled from "SyncMethod.java"
    public class com.zejian.concurrencys.SyncMethod
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool;
    
       //省略没必要的字节码
      //==================syncTask方法======================
      public synchronized void syncTask();
        descriptor: ()V
        //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: dup
             2: getfield      #2                  // Field i:I
             5: iconst_1
             6: iadd
             7: putfield      #2                  // Field i:I
            10: return
          LineNumberTable:
            line 12: 0
            line 13: 10
    }
    SourceFile: "SyncMethod.java"

    从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单了解一下Java官方在JVM层面对synchronized锁的优化。

    Java虚拟机对synchronized的优化

    锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段,这里并不打算深入到每个锁的实现和转换过程更多地是阐述Java虚拟机所提供的每个锁的核心优化思想,毕竟涉及到具体过程比较繁琐,如需了解详细过程可以查阅《深入理解Java虚拟机原理》。

    偏向锁

    偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

    轻量级锁

    倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

    自旋锁

    轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

    锁消除

    消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

    /**
     * Created by zejian on 2017/6/4.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     * 消除StringBuffer同步锁
     */
    public class StringBufferRemoveSync {
    
        public void add(String str1, String str2) {
            //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
            //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
            StringBuffer sb = new StringBuffer();
            sb.append(str1).append(str2);
        }
    
        public static void main(String[] args) {
            StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
            for (int i = 0; i < 10000000; i++) {
                rmsync.add("abc", "123");
            }
        }
    
    }

    关于synchronized 可能需要了解的关键点

    synchronized的可重入性

    从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。如下:

    public class AccountingSync implements Runnable{
        static AccountingSync instance=new AccountingSync();
        static int i=0;
        static int j=0;
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
    
                //this,当前实例对象锁
                synchronized(this){
                    i++;
                    increase();//synchronized的可重入性
                }
            }
        }
    
        public synchronized void increase(){
            j++;
        }
    
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();t2.start();
            t1.join();t2.join();
            System.out.println(i);
        }
    }

    正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。

    线程中断与synchronized

    线程中断

    正如中断二字所表达的意义,在线程运行(run方法)中间打断它,在Java中,提供了以下3个有关线程中断的方法

    //中断线程(实例方法)
    public void Thread.interrupt();
    
    //判断线程是否被中断(实例方法)
    public boolean Thread.isInterrupted();
    
    //判断是否被中断并清除当前中断状态(静态方法)
    public static boolean Thread.interrupted();

    当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态),如下代码将演示该过程:

    public class InterruputSleepThread3 {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    //while在try中,通过异常中断就可以退出run循环
                    try {
                        while (true) {
                            //当前线程处于阻塞状态,异常必须捕捉处理,无法往外抛出
                            TimeUnit.SECONDS.sleep(2);
                        }
                    } catch (InterruptedException e) {
                        System.out.println("Interruted When Sleep");
                        boolean interrupt = this.isInterrupted();
                        //中断状态被复位
                        System.out.println("interrupt:"+interrupt);
                    }
                }
            };
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            //中断处于阻塞状态的线程
            t1.interrupt();
    
            /**
             * 输出结果:
               Interruted When Sleep
               interrupt:false
             */
        }
    }

    如上述代码所示,我们创建一个线程,并在线程中调用了sleep方法从而使用线程进入阻塞状态,启动线程后,调用线程实例对象的interrupt方法中断阻塞异常,并抛出InterruptedException异常,此时中断状态也将被复位。这里有些人可能会诧异,为什么不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其实原因很简单,前者使用时并没有明确的单位说明,而后者非常明确表达秒的单位,事实上后者的内部实现最终还是调用了Thread.sleep(2000);,但为了编写的代码语义更清晰,建议使用TimeUnit.SECONDS.sleep(2);的方式,注意TimeUnit是个枚举类型。ok~,除了阻塞中断的情景,我们还可能会遇到处于运行期且非阻塞的状态的线程,这种情况下,直接调用Thread.interrupt()中断线程是不会得到任响应的,如下代码,将无法中断非阻塞状态下的线程:

    public class InterruputThread {
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(){
                @Override
                public void run(){
                    while(true){
                        System.out.println("未被中断");
                    }
                }
            };
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            t1.interrupt();
    
            /**
             * 输出结果(无限执行):
                 未被中断
                 未被中断
                 未被中断
                 ......
             */
        }
    }

    虽然我们调用了interrupt方法,但线程t1并未被中断,因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序,改进后代码如下:

    public class InterruputThread {
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(){
                @Override
                public void run(){
                    while(true){
                        //判断当前线程是否被中断
                        if (this.isInterrupted()){
                            System.out.println("线程中断");
                            break;
                        }
                    }
    
                    System.out.println("已跳出循环,线程中断!");
                }
            };
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            t1.interrupt();
    
            /**
             * 输出结果:
                线程中断
                已跳出循环,线程中断!
             */
        }
    }

    是的,我们在代码中使用了实例方法isInterrupted判断线程是否已被中断,如果被中断将跳出循环以此结束线程,注意非阻塞状态调用interrupt()并不会导致中断状态重置。综合所述,可以简单总结一下中断两种情况,一种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位,另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。有时我们在编码时可能需要兼顾以上两种情况,那么就可以如下编写:

    public void run(){
        try {
        //判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
        while (!Thread.interrupted()) {
            TimeUnit.SECONDS.sleep(2);
        }
        } catch (InterruptedException e) {
    
        }
    }

    中断与synchronized

    事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。演示代码如下

    /**
     * Created by zejian on 2017/6/2.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public class SynchronizedBlocked implements Runnable{
    
        public synchronized void f() {
            System.out.println("Trying to call f()");
            while(true) // Never releases lock
                Thread.yield();
        }
    
        /**
         * 在构造器中创建新线程并启动获取对象锁
         */
        public SynchronizedBlocked() {
            //该线程已持有当前实例锁
            new Thread() {
                public void run() {
                    f(); // Lock acquired by this thread
                }
            }.start();
        }
        public void run() {
            //中断判断
            while (true) {
                if (Thread.interrupted()) {
                    System.out.println("中断线程!!");
                    break;
                } else {
                    f();
                }
            }
        }
    
    
        public static void main(String[] args) throws InterruptedException {
            SynchronizedBlocked sync = new SynchronizedBlocked();
            Thread t = new Thread(sync);
            //启动后调用f()方法,无法获取当前实例锁处于等待状态
            t.start();
            TimeUnit.SECONDS.sleep(1);
            //中断线程,无法生效
            t.interrupt();
        }
    }

    我们在SynchronizedBlocked构造函数中创建一个新线程并启动获取调用f()获取到当前实例锁,由于SynchronizedBlocked自身也是线程,启动后在其run方法中也调用了f(),但由于对象锁被其他线程占用,导致t线程只能等到锁,此时我们调用了t.interrupt();但并不能中断线程。

    等待唤醒机制与synchronized

    所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

    synchronized (obj) {
           obj.wait();
           obj.notify();
           obj.notifyAll();         
     }

    需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。

    ok~,篇幅已比较长了,关于synchronized,我们就暂且聊到这。如有错误,欢迎指正,谢谢~~。

    本篇的主要参考资料:
    《Java编程思想》
    《深入理解Java虚拟机》
    《实战Java高并发程序设计》

    感觉文章给你带来收获就赞一个吧!

    展开全文
  • Java并发

    千次阅读 2016-04-16 15:26:50
    1.定义任务:实现Runnable接口,或者继承Thread,推荐使用实现...并发任务启动推荐以下方式启动:ExecutorService exec = Executors.newCachedThreadPool();exec.execute(Task:实现了Runnable接口)CachedThreadPool
    1.定义任务:实现Runnable接口,或者继承Thread,推荐使用实现Runnable接口而不是继承Thread,因为继承Thread后就不能再继承别的类了
    2.并发任务启动推荐以下方式启动:
    1. ExecutorService exec = Executors.newCachedThreadPool();
    2. exec.execute(Task:实现了Runnable接口)
    CachedThreadPool:缓存线程池,不够就启动新线程,够的话就从已经创建的线程中拿
    FixedThreadPool:固定数量的线程的线程池
    SingleThreadExecutor:线程为1的FixedThreadPool

    3.ExecutorService.submit() 会产生Future对象,它用Callable返回结果的特定类型,可以用isDone返回来查询Future时候已经完成,也可以不加isDone判断,因为Future的get()方法是阻塞的。
    比如:
    Class TaskWithResult implement Callable<String>{
         private int id;
         public TaskWithResult(int id){
              this.id = id;
         }
         public string call(){
              return "result of TaskWithResult " + id;
         }
         public static void main(String[] args){
              ExecutorService exec = Executor.newCachedThreadPool();
              Future<String> results = exec.submit(new TaskWithResult(1));
              try{
                   if(result.isDone()){
                         result.get();
                   }
              }catch(InterruptedException e){
              }
         }
    }
    4.线程优先级:尽管JDK有10个优先级,但是它与多数操作系统不能映射得很好。Sun的Solaris有2的31次方个优先级。唯一可移植的方法是当调整优先级的时候,只使用了MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三种级别
    5.线程让步:线程调度暗示:任务做的差不多了,用yield来让步(只是一种暗示,没有任何机制保证它将会被采纳)。当调用yield()方法时,你也是在建议具有相同优先级的其他线程可以运行。注意:对于任何重要的控制或在调整应用时,都不能依赖于yield()。
    6.后台线程:在线程启动之前将线程SetDaemon(true),就设置成后台线程了。注意:如果前台线程全部执行完了,后台线程会自动中断结束的。
    7.等待该线程终止:调用join,对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法,但是请用try-catch捕获,因为被中断的时候会抛出InterruptedException异常。推荐:Java SE5的java.util.concurrent类库包含CyclicBarrier,它可能比最初的线程类库中的join()更加适合
    8.线程组:最好把线程组看成一次不成功的尝试,你只要忽略他就好了。建议不要使用
    9.解决共享资源竞争:基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。
    注意:
    1.在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突
    2.每个访问临界共享资源的方法都必须被同步,否则他们就不会正确地工作
    10.同步控制:synchronized、Lock
    Lock:对象必须被显示地创建、锁定、释放。注意:记得带上try-finally
    优点:具有更细粒度的控制力,比如:异常清理
    缺点:代码多
    比如:
    private Lock lock = new ReentrantLock();
    public void testLock(){
         lock.lock();
         try{
              System.out.print("hihihi");
         }finally{
              lock.unlock();
         }
    }
    synchronized方法:
    优点:代码简洁
    缺点:如果某些事物失败了,那么就会抛出一个异常。你就没有机会去做任何清理工作了。lock就可以做到清理了

    11.原子性与易变性
    1》原子性可以应用于除了long和double之外的所有基本类型之上的“简单操作”。对于读取和写入除融合double之外的基本类型变量这样操作,可以保证它们会被当作不可分(原子)的操作来操作内存。
    2》当你定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作的)原子性。(注意:Java SE5 之前,volatile一直未能正确地工作)
    3》如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile的
    4》使用volatile而不是synchronized的唯一安全情况是类中只有一个可变域。第一选择应该是synchronized关键字,这是最安全的方式,而尝试其他任何方式都是有风险的。
    5》如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么你就应该将这个域设置为volatile
    12.原子类:Java SE5 引入了诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,它们提供下面形式的原子性条件更新操作:boolean compareAndSet(expectedValue,updateValue);

    13.线程本地存储:
    1》防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。
    2》创建和管理线程本地存储可以用java.lang.ThreadLocal类来实现。
    比如:
    private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
         private Random rand = new Random(47);
         protected synchronized Integer initialValue(){
              return rand.nextInt(10000);
         }
    };
    3》ThreadLocal对象通常当作静态域存储,在创建ThreadLocal时,你只能通过get()和set()方法来访问该对象的内容,get()方法将返回与其线程关联的对象的副本,而set()会将参数插入到为其线程存储的对象中,并返回存储中原有的对象。ThreadLocal能保证不会出现竞争条件。

    14.在阻塞时终结
    1》线程状态:新建、就绪、阻塞、死亡
    2》进入阻塞状态:有如下原因
    ①通过sleep(milliseconds)使任务进入休眠状态
    ②通过调用wait()使线程挂起,直到线程得到了notify()或notifyAll()消息(或者再java SE5java.util.concurrent类库中等价的signal()或signalAll()消息),线程才会进入就绪状态。
    ③任务在等待某个输入/输出完成
    ④任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁
    3》中断:
    第一种方式:Thread.interrupt()终止被阻塞的任务,设置完后线程被设置为中断状态。如果线程已经被阻塞,或者执行阻塞操作,那么设置这个线程中断状态将抛出InterruptedException,抛出异常后中断状态复位。
    第二种方式(推荐使用):Executor上调用ShutdownNow(),它将发送一个interrupt()调用给它启动的所有线程,然后关闭Executor的所有任务。如果想单出关闭某一项任务,那么使用Executor的submit()方法,而不是executor来启动任务,submit()返回一个泛型Future<?>,然后调用Future的cancel(true)来interrupt停止这个线程,cancel()是一种中断由Executor启动的单个线程方式。
    4》SleepBlock可中断示例、IOBlocked和SynchronizedBlocked不可中断阻塞示例。Java SE5  ReetrantLock上阻塞的任务可被中断

    15.检查中断:应该在run()返回中使用它来处在中断状态被设置时,被阻塞和不被阻塞的各种可能
    比如:
    public void run(){
         try{
              while(!Thread.interrupted()){
                   doSomething();
              }
         }catch(InterruptedException e){
              print("Exiting InterruptedException");
         }
    }

    16.线程之间的协作
    1》调用sleep()、yield()的时候锁并没有被释放,这一点值得注意。
    2》在wait()期间对象释放锁。
    3》通过notify()、notifyAll(),或者令时间到期,从wait()中恢复执行。
    4》注意:wait()、notify()、notifyAll() 只能在同步控制方法或同步控制块里面调用。
    5》使用显示的Lock和Condition对象
    ①Condition 调用await()来挂起任务,通过signal来唤醒一个任务、或者signalAll()来唤醒所有在这个Condition上被其自身挂起的任务(与使用notifyAll相比,signalAll()是更安全的方式)
    ②lock()的调用都必须紧跟一个try-finally子句,用来保证在所有情况下都可以释放锁。使用内建版本时,任务调用await()、signal()、signalAll之前,必须拥有这个锁。

    17.生成者-消费者与队列
    1》wait()、notifyAll()是一种非常低级的解决任务互斥操作方法,因为每次交付时都握手。
    2》可以使用同步队列来解决任务协作问题,同步队列任何时刻都只允许一个任务插入或移除元素。(java.util.concurrent.BlockingQueue接口中提供),阻塞队列可以解决非常大量的问题,相比wait()、notifyAll()简单并可靠得多。

    18.任务间使用管道进行输入、输出
    1》PipedWriter:允许任务向管道写
    PipedWriter out = new PipedWriter();
    out.write();
    2》PipedReader:允许不同任务从同一个管道中读取
    PipedReader in = new PipedReader(out);
    3》如果启动了一个没有构造完毕的对象,在不同平台上管道可能会产生不一致的行为(注意:BlockingQueue使用起来更加健壮而容易)

    19.死锁
    1》当以下四条同时满足时,就会发生死锁
    ①互斥条件。任务使用的资源中至少有一个是不能共享。一根Chopstick一次就只能被一个Philosopher使用
    ②至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有资源。也就是说,要发生死锁,Philosopher必须拿着一根Chopstick并且等待另一根。
    ③资源不能被任务抢占,任务必须把资源释放当作普通事件。Philosopher很有礼貌,他们不会从其他Philosopher那里抢Chopstick。
    ④必须有循环等待。一个任务等待其他任务所持有的资源,后者又在等待另外一个任务所持有的资源,这样一直下去,直到最后一个等待第一个任务所持有的资源,使得都被锁住。
    2》解决死锁最容易的方法就是破坏第4个条件。让其不要循环等待资源就可以了,就像5个筷子,都循环的向右拿筷子,最后一个就向左拿筷子,就解决了死锁问题

    20.新类库中的构件:Java SE5的java.util.concurrent引入了大量设计用来解决并发问题的新类
    1》CountDownLatch:用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作完成。(只能触发一次的事件)
    2》CyclicBarrier:适用这种情况,你希望创建一组任务,它们并行地执行工作,然后在进行下一个步骤之前等待,直到所有任务都完成(看起来有些像join())。(CyclicBarrier栅栏队列,可触发多次)比如:赛马,要等所有马手都准备好后,会自动执行CyclicBarrier中的Runnable。
    new CyclicBarrier(nHorses,new Runnable(){
         public void run(){
              //开始赛马
         }
    });
    3》DelayQueue:用于放置实现了Delayed接口的对象,其中的对象只能在其到期才能从队列中取走。DelayQueue可以作为一种优先级队列的变体,根据里面存放的DelayTask(实现Delayed接口)设置的到期时间长度来排序,时间最短的先从队列取出,然后运行。
    4》PriorityBlockingQueue:优先级队列,具有可阻塞的读取操作。如果队列没有元素,将直接阻塞读取者。
    5》使用ScheduledExecutor的温室控制器:温室事件是一个在预定时间运行的任务,ScheduledThreadPoolExecutor可以解决该问题。通过使用schedule()(运行一次任务)或者scheduleAtFixedRate()(每隔规则时间重复执行任务)。
    6》Semaphore:正常的锁(来自concurrent.locks或内建的synchronized锁)在任何时刻都只允许一个任务访问一项资源,而计数信号量允许n个任务同时访问这个资源。
    7》Exchanger:两个任务之间交换对象的栅栏。当这些任务进入栅栏时,他们各自拥有一个对象,当它们离开时,它们都拥有之前由对象持有的对象。应用场景:一个任务创建对象,对象的生产代价很高昂,而另外一个任务在消费这些对象。通过这种方式,可以有更多的对象在被创建的同时被消费。

    21.仿真
    1》饭店仿真:Java SE5 SynchronousQueue 是一种没有内部容量的阻塞队列,因此每个put()都必须等待一个take(),反之亦然。比如:你把一个对象交个某人,没有任何桌子可以放置这个对象,因此只有在这个人伸出手,准备好接受这个对象时,你才能工作。建议:接受任务将处理对象,将其当作一个消息来对待,而不是向它发送消息。如果可能就准许这项技术,那么构建出的并发系统的可能性就会大大增加。

    21.性能调优
    1》Atomic:只有在非常简单的情况下才有用,只有一个被修改的Atomic对象,并且这个对象独立于其他所有的对象。
    2》synchronized:比Lock可读性高
    3》Lock:性能测试使用lock比synchronized要高效许多,而synchronized开销变化范围太大,而Lock相对比较一致。
    更安全的做法:用传统的互斥方式入手,只有在性能方面需求能够明确指示时,才替换为Atomic
    4》免锁容器:
    ①免锁容器通用策略:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。
    ②CopyOnWriteArrayList好处是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModifycationException,因此你不必编写特殊的代码去防范这种异常。
    ③CopyOnWriteArrySet:使用CopyOnWriteArrayListlai 实现其免锁行为。
    ④ConcurrentHashMap和ConcurrentLinkedQueue免锁机制类似,允许并发的读取和写入,但容器中只有部分内容而不是整个容器可以被复制和修改。修改在完成之前,读取者仍旧不能看到它们。ConcurrentHashMap不会抛出ConcurrentModifycationException异常。
    5》乐观锁:
    ①synchronized ArrayList无论读取者和写入者的数量多少,都具有大致相同的性能,读取者和其他读取者竞争的方式和写入相同。CopyOnWriteArrayList在没有写入者时,速度回快很多。在列表写入的影响没有超过短期同步整个列表时,尽量使用CopyOnWriteArrayList。
    6》乐观加锁:
    Atomic:用compareAndSet()方法设置旧值和新值来更新,如果Atomic对象发现不一致,那么这个操作就会失败。意味着某个其他任务已经于此操作执行期间修改了这个对象。注意:正常情况下我们最好使用互斥(synchronized或Lock)来防止多个任务同时修改一个对象。如果对失败不能执行某些恢复操作,那么你就不要使用Atomic,而是使用互斥。
    7》ReadWriteLock:能否提高程序性能是完全不可确定的,它取决于诸如数据被读取的频率与被修改的频率相比较的结果。只有当你在搜索可以提供性能的方法是,才应该想到它。只有在必须时才引入ReadWriteLock。

    22.总结
    1》并发程序设计使我们理解:
    ①可以运行多个任务
    ②必须考虑当这些任务关闭是,可能出现的所有问题
    ③任务可能会在共享资源上彼此干涉。互斥锁是用来防止这种冲突的基本工具
    ④如果任务设计得不够自信,就有可能会死锁
    ⑤明白什么时候应该使用并发,什么时候应该避免使用并发,使用它们的主要原因:1.要处理很多任务,应用并发能够更有效地使用计算机。2.要能够更好地组织代码。3.要更便于用户使用。
    2》多线程的主要缺陷有:
    ①等待共享资源的时候性能降低
    ②需要处理线程的额外CPU花费
    ③糟糕的程序设计导致不必要的复杂度
    ④有可能产生一些病态行为,如:饿死、竞争、死锁、活锁(多个运行各自任务的线程是的整体无法完成)
    ⑤不同平台导致的不一致性。比如:竞争条件在一些机器上很快出现,有些机器不出现。
    3》线程创建数目要有上界,因为达到一定数量后,线程性能会很差。

    23.进阶书籍推荐:
    1》Java Concurrency in Practice 译名:java 并发编程实战  作者:Brian Goetz  
    2》Concurrent Programming in Java,Second Edition  作者:Doug Lea
    3》The Java Language Specification,Third Edition 作者:Gosling、Joy
    Thinking in Java 这本书强烈推荐大家好好看看,以上是我看并发部分总结出来的一些点,收获很多。此章218页虽然长,并发值得每一个后台程序员仔细阅读。

    参看:Thinking in Java 并发

    展开全文
  • Java 代码在编译后会变成 Java 字节码,字节码被类加载器加载到 JVM 里,JVM 执行字节码,最终需要转化为汇编指令在 CPU 上执行,Java 中所使用的并发机制依赖于 JVM 的实现和 CPU 的指令。本章我们将深入底层一起...

    前言:

    Java 代码在编译后会变成 Java 字节码,字节码被类加载器加载到 JVM 里,JVM 执行字节码,最终需要转化为汇编指令在 CPU 上执行,Java 中所使用的并发机制依赖于 JVM 的实现和 CPU 的指令。本章我们将深入底层一起探索下 Java 并发机制的底层实现原理。

    一、volatile 的应用

    在多线程并发编程中synchronizedvolatile都扮演着重要的角色volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

    如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

    1.1 volatile 的定义与实现原理

    Java 语言规范第3版中对volatile的定义如下:

    Java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

    Java 语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java 线程内存模型确保所有线程看到这个变量的值是一致的。

    在了解volatile实现原理之前,我们先来看下与其实现原理相关的CPU术语与说明。

    术语 英文单词 术语描述
    内存屏障 memory barriers 是一组处理器指令,用于实现对内存操作的顺序限制。
    缓冲行 cache line 缓存中可以分配的最小存储单位,处理器填写缓存行时会加载整个缓存行,需要使用多个主内存读周期。
    原子操作 atomic operation 不可中断的一个或一系列操作。
    缓存行填充 cache line fill 当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存(L1,L2,L3 的或所有)。
    缓存命中 cache hit 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取。
    写命中 write hit 当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存。
    写缺失 write misses the cache 一个有效的缓存行被写入到不存在的内存区域。

    volatile 是如何来保证可见性的呢?让我们在 X86 处理器下通过工具获取 JIT 编译器生成的汇编指令来查看对 volatile 进行写操作时,CPU 会做什么事情。

    Java代码如下。

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

    转变成汇编代码,如下。

    0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
    

    volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查 IA-32 架构软件开发者手册可知,Lock 前缀的指令在多核处理器下会引发了两件事情。

    1. 将当前处理器缓存行的数据写回到系统内存。
    2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

    为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。

    如果对声明了volatile的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。

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

    volatile的两条实现原则

    1. Lock前缀指令会引起处理器缓存回写到内存。
    2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

    1.2 volatile 的使用优化

    著名的 Java 并发编程大师 Doug lea 在 JDK 7 的并发包里新增一个队列集合类LinkedTransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。

    LinkedTransferQueue的代码如下。

    /** 队列中的头部节点 */
    private transient final PaddedAtomicReference<QNode> head;
    /** 队列中的尾部节点 */
    private transient final PaddedAtomicReference<QNode> tail;
    static final class PaddedAtomicReference<T> extends AtomicReference T>{
        // 使用很多4个字节的引用追加到64个字节
        Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
        PaddedAtomicReference(T r) {
            super(r);
        }
    }
    public class AtomicReference<V> implements java.io.Serializable {
        private volatile V value;
        // 省略其他代码
    }
    

    让我们先来看看LinkedTransferQueue这个类,它使用一个内部类类型来定义队列的头节点(head)和尾节点(tail),而这个内部类PaddedAtomicReference相对于父类AtomicReference只做了一件事情,就是将共享变量追加到 64 字节。我们可以来计算下,一个对 象的引用占 4 个字节,它追加了 15 个变量(共占 60 个字节),再加上父类的 value 变量,一共 64 个字节。

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

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

    Doug lea 使用追加到64字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定。

    那么是不是在使用volatile变量时都应该追加到64字节呢?

    不是的。在两种场景下不应该使用这种方式。

    1. 缓存行非 64 字节宽的处理器。

      如 P6 系列和奔腾处理器,它们的 L1 和 L2 高速缓存行是 32 个字节宽。

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

      因为使用追加字节的方式需要处理器读取更多的字节到高速缓冲区,这本身就会带来一定的性能消耗,如果共享变量不被频繁写的话,锁的几率也非常小,就没必要通过追加字节的方式来避免相互锁定。

    不过这种追加字节的方式在 Java 7 下可能不生效,因为 Java 7 变得更加智慧,它会淘汰或重新排列无用字段,需要使用其他追加字节的方式。

    二、synchronized 的实现原理与应用

    Java 中的每一个对象都可以作为锁。具体表现 为以下 3 种形式。

    • 对于普通同步方法,锁是当前实例对象。
    • 对于静态同步方法,锁是当前类的 Class 对象。
    • 对于同步代码块,锁是synchonized括号里配置的对象。

    当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

    那么锁到底存在哪里呢?锁里面会存储什么信息呢?

    从 JVM 规范中可以看到synchonized在 JVM 里的实现原理,JVM 基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorentermonitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在 JVM 规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。

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

    2.1 Java 对象头

    synchronized用的锁是存在 Java 对象头里的。如果对象是数组类型,则虚拟机用 3 个字宽 (Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit。

    Java 对象头的长度:

    长度 内容 说明
    32/64bit Mark Word 存储对象的 hashCode 或锁信息
    32/64bit Class Metadata Address 存储到对象类型数据的指针
    32/64bit Array length 数组的长度(如果当前对象是数组)

    Java 对象头里的 Mark Word 里默认存储对象的 hashCode、分代年龄和锁标记位。32位 JVM 的 Mark Word 的默认存储结构如表所示。

    Java 对象头的存储结构:

    锁状态 25bit 4bit 1bit 是否是偏向锁 2bit 锁标志位
    无锁状态 对象的 hashCode 对象分代年龄 0 01

    在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据,如表所示。

    Mark Word 的状态变化:
    Mark Word 的状态变化
    在64位虚拟机下,Mark Word是64bit大小的,其存储结构如表所示。

    Mark Word 的存储结构:Mark Word 的存储结构

    2.2 锁的升级与对比

    Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java SE 1.6 中,锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率,下文会详细分析。

    2.2.1 偏向锁

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

    当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下 Mark Word 中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。

    1. 偏向锁的撤销

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

      偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 Mark Word 要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

      图中的线程 1 演示了偏向锁初始化的流程,线程 2 演示了偏向锁撤销的流程。

      偏向锁的获得和撤销流程

    2. 关闭偏向锁

      偏向锁在 Java 6 和 Java 7 里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用 JVM 参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

    2.2.2 轻量级锁

    1. 轻量级加锁

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

    2. 轻量级锁解锁

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

      下图是两个线程同时争夺锁,导致锁膨胀的流程图。

      争夺锁导致的锁膨胀流程图
      因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

    2.2.3 锁的优缺点对比

    优点 缺点 适用场景
    偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
    轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程,使用自旋会消耗CPU。 追求响应时间,同步块执行速度非常快。
    重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢。 追求吞吐量,同步块执行速度较长。

    2.3 原子操作的实现原理

    原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。让我们一起来聊一聊在Intel处理器和Java里是如何实现原子操作的。

    2.3.1 CPU 术语定义

    术语名称 英文 解释
    缓存行 Cache line 缓存的最小单位
    比较并交换 Compare and Swap CAS 操作需要输入两个值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,才交换成新值,发生了变化则不交换。
    CPU 流水线 CPU pipeline CPU 流水线的工作方式就像工业生产上的装配流水线,在 CPU 中由 5 ~ 6 个不同功能的电路单元组成一条指令处理流水线,然后将一条 X86 指令分成 5 ~ 6 步后再由这些电路单元分别执行,这样就能实现在一个 CPU 时钟周期完成一条指令,因此提高 CPU 的运算速度。
    内存顺序冲突 Memory order violation 内存顺序冲突一般是由假共享引起的,假共享是指多个 CPU 同时修改同一个缓存行的不同部分而引起其中一个 CPU 的操作无效,当出现这个内存顺序冲突时,CPU 必须清空流水线。

    2.3.2 处理器如何实现原子操作

    1. 使用总线锁保证原子性

      如果多个处理器同时对共享变量进行读改写操作(i++ 就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。举个例子,如果 i=1,我们进行两次 i++ 操作,我们期望的结果是 3,但是有可能结果是 2,如图所示。

      结果对比
      原因可能是多个处理器同时从各自的缓存中读取变量 i,分别进行加 1 操作,然后分别写入系统内存中。那么,想要保证读改写共享变量的操作是原子的,就必须保证 CPU1 读改写共享变量的时候,CPU2 不能操作缓存了该共享变量内存地址的缓存。

      处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个 LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。

    2. 使用缓存锁保证原子性

      在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把 CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

      频繁使用的内存会缓存在处理器的 L1、L2 和 L3 高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在 Pentium 6 和目前的处理器中可以使用 “缓存锁定” 的方式来实现复杂的原子性。

      所谓 “缓存锁定” 是指内存区域如果被缓存在处理器的缓存行中,并且在 Lock 操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言 LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效,在如图所示的例子中,当 CPU1 修改缓存行中的 i 时使用了缓存锁定,那么 CPU2 就不能同时缓存 i 的缓存行。

      但是有两种情况下处理器不会使用缓存锁定。

      第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定。

      第二种情况是:有些处理器不支持缓存锁定。对于 Intel 486 和 Pentium 处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

    针对以上两个机制,我们通过 Intel 处理器提供了很多 Lock 前缀的指令来实现。例如,位测试和修改指令:BTS、BTR、BTC;交换指令 XADD、CMPXCHG,以及其他一些操作数和逻辑指令(如ADD、OR)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。

    2.3.3 Java如何实现原子操作

    1. 使用循环 CAS 实现原子操作

      JVM 中的 CAS 操作正是利用了处理器提供的 CMPXCHG 指令实现的。自旋 CAS 实现的基本思路就是循环进行 CAS 操作直到成功为止,以下代码实现了一个基于 CAS 线程安全的计数器方法 safeCount 和一个非线程安全的计数器 count。

      public class Counter {
      
          private final AtomicInteger atomicI = new AtomicInteger(0);
          private int i = 0;
      
          public static void main(String[] args) {
              final Counter cas = new Counter();
              List<Thread> ts = new ArrayList<>(600);
              long start = System.currentTimeMillis();
              for (int j = 0; j < 100; j++) {
                  Thread t = new Thread(() -> {
                      for (int i = 0; i < 10000; i++) {
                          cas.count();
                          cas.safeCount();
                      }
                  });
                  ts.add(t);
              }
              for (Thread t : ts)
                  t.start();
              for (Thread t : ts)
                  try {
                      t.join();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              System.out.println(cas.i); // 984453(不确定)
              System.out.println(cas.atomicI.get()); // 1000000
              System.out.println(System.currentTimeMillis() - start); // 173(不确定)
          }
      
          /**
           * 使用CAS实现线程安全计数器
           */
          private void safeCount() {
              while (true) {
                  int i = atomicI.get();
                  boolean suc = atomicI.compareAndSet(i, ++i);
                  if (suc)
                      break;
              }
          }
      
          /**
           * 非线程安全计数器
           */
          private void count() {
              i++;
          }
      
      }
      

      从 Java 1.5 开始,JDK 的并发包里提供了一些类来支持原子操作,如 AtomicBoolean(用原子方式更新的 boolean 值)、AtomicInteger(用原子方式更新的 int 值)和 AtomicLong(用原子方式更新的 long 值)。这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增 1 和自减 1。

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

      ABA 问题

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

      ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加 1,那么 A→B→A 就会变成 1A→2B→3A。

      从 Java 1.5 开始,JDK 的 Atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

      public boolean compareAndSet (
              V expectedReference, // 预期引用
              V newReference, // 更新后的引用
              int expectedStamp, // 预期标志
              int newStamp // 更新后的标志
      )
      

      循环时间长开销大

      自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令,那么效率会有一定的提升。

      pause 指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起 CPU 流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。

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

      当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

      还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量 i = 2,j = a,合并一下 ij = 2a,然后用 CAS 来操作 ij。从 Java 1.5 开始,JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。

    3. 使用锁机制实现原子操作

      锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM 内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM 实现锁的方式都用了循环 CAS,即当一个线程想进入同步块的时候使用循环 CAS 的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

    展开全文
  • Java并发编程之美 、Java并发编程的艺术 、 实战Java高并发程序设计 这三本书哪本好一点 感觉都差不多 哪本适合找实习的大学生
  • 当然这些都是并发编程的基本知识,除了使用这些工具以外,Java并发编程中涉及到的技术原理十分丰富。为了更好地把并发知识形成一个体系,也鉴于本人没有能力写出这类文章,于是参考几位并发编程专家的博客和书籍,做...

     

    这里不仅仅是指使用简单的多线程编程,或者使用juc的某个类。当然这些都是并发编程的基本知识,除了使用这些工具以外,Java并发编程中涉及到的技术原理十分丰富。为了更好地把并发知识形成一个体系,也鉴于本人没有能力写出这类文章,于是参考几位并发编程专家的博客和书籍,做一个简单的整理。

     

    一:并发基础和多线程

    首先需要学习的就是并发的基础知识,什么是并发,为什么要并发,多线程的概念,线程安全的概念等。

    然后学会使用Java中的Thread或是其他线程实现方法,了解线程的状态转换,线程的方法,线程的通信方式等。

     

    二:JMM内存模型

    任何语言最终都是运行在处理器上,JVM虚拟机为了给开发者一个一致的编程内存模型,需要制定一套规则,这套规则可以在不同架构的机器上有不同实现,并且向上为程序员提供统一的JMM内存模型。

    所以了解JMM内存模型也是了解Java并发原理的一个重点,其中了解指令重排,内存屏障,以及可见性原理尤为重要。

    JMM只保证happens-before和as-if-serial规则,所以在多线程并发时,可能出现原子性,可见性以及有序性这三大问题。

    下面的内容则会讲述Java是如何解决这三大问题的。

     

    三:synchronized,volatile,final等关键字

    对于并发的三大问题,volatile可以保证原子性和可见性,synchronized三种特性都可以保证(允许指令重排)。

    synchronized是基于操作系统的mutex lock指令实现的,volatile和final则是根据JMM实现其内存语义。

    此处还要了解CAS操作,它不仅提供了类似volatile的内存语义,并且保证操作原子性,因为它是由硬件实现的。

    JUC中的Lock底层就是使用volatile加上CAS的方式实现的。synchronized也会尝试用cas操作来优化器重量级锁。

    了解这些关键字是很有必要的。

     

    四:JUC包

    在了解完上述内容以后,就可以看看JUC的内容了。

    JUC提供了包括Lock,原子操作类,线程池,同步容器,工具类等内容。

    这些类的基础都是AQS,所以了解AQS的原理是很重要的。

    除此之外,还可以了解一下Fork/Join,以及JUC的常用场景,比如生产者消费者,阻塞队列,以及读写容器等。

     

    五:实践

    上述这些内容,除了JMM部分的内容比较不好实现之外,像是多线程基本使用,JUC的使用都可以在代码实践中更好地理解其原理。多尝试一些场景,或者在网上找一些比较经典的并发场景,或者参考别人的例子,在实践中加深理解,还是很有必要的。

     

     

    六:补充

    由于很多Java新手可能对并发编程没什么概念,在这里放一篇不错的总结,简要地提几个并发编程中比要重要的点,也是比较基本的点吗,算是抛砖引玉,开个好头,在大致了解了这些基础内容以后,才能更好地开展后面详细内容的学习。

     

    1.并发编程三要素

    • 原子性
      原子,即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
    • 有序性
      程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
    • 可见性
      当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。

    2. 线程的五大状态

    • 创建状态
      当用 new 操作符创建一个线程的时候
    • 就绪状态
      调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的调度
    • 运行状态
      CPU 开始调度线程,并开始执行 run 方法
    • 阻塞状态
      线程的执行过程中由于一些原因进入阻塞状态
      比如:调用 sleep 方法、尝试去得到一个锁等等​​
    • 死亡状态
      run 方法执行完 或者 执行过程中遇到了一个异常

    3.悲观锁与乐观锁

    • 悲观锁:每次操作都会加锁,会造成线程阻塞。
    • 乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。​

    4.线程之间的协作

    4.1 wait/notify/notifyAll

    这一组是 Object 类的方法
    需要注意的是:这三个方法都必须在同步的范围内调用​

    • wait
      阻塞当前线程,直到 notify 或者 notifyAll 来唤醒​​​​

      
      wait有三种方式的调用
      wait()
      必要要由 notify 或者 notifyAll 来唤醒​​​​
      wait(long timeout)
      在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。
      wait(long timeout,long nanos)
      本质上还是调用一个参数的方法
      public final void wait(long timeout, int nanos) throws InterruptedException {
            if (timeout < 0) {
                   throw new IllegalArgumentException("timeout value is negative");
             }
            if (nanos < 0 || nanos > 999999) {
                    throw new IllegalArgumentException(
                   "nanosecond timeout value out of range");
             }
             if (nanos > 0) {
                   timeout++;
             }
             wait(timeout);
      }
                    ​
      
      • notify
        只能唤醒一个处于 wait 的线程
      • notifyAll
        唤醒全部处于 wait 的线程

    4.2 sleep/yield/join

    这一组是 Thread 类的方法

    • sleep
      让当前线程暂停指定时间,只是让出CPU的使用权,并不释放锁

    • yield
      暂停当前线程的执行,也就是当前CPU的使用权,让其他线程有机会执行,不能指定时间。会让当前线程从运行状态转变为就绪状态,此方法在生产环境中很少会使用到,​​​官方在其注释中也有相关的说明

      
            /**
            * A hint to the scheduler that the current thread is willing to yield
            * its current use of a processor. The scheduler is free to ignore this
            * hint.
            *
            * <p> Yield is a heuristic attempt to improve relative progression
            * between threads that would otherwise over-utilise a CPU. Its use
            * should be combined with detailed profiling and benchmarking to
            * ensure that it actually has the desired effect.
            *
            * <p> It is rarely appropriate to use this method. It may be useful
            * for debugging or testing purposes, where it may help to reproduce
            * bugs due to race conditions. It may also be useful when designing
            * concurrency control constructs such as the ones in the
            * {@link java.util.concurrent.locks} package.
            */​​
            ​​​​
      
    • join
      等待调用 join 方法的线程执行结束,才执行后面的代码
      其调用一定要在 start 方法之后(看源码可知)​
      使用场景:当父线程需要等待子线程执行结束才执行后面内容或者需要某个子线程的执行结果会用到 join 方法​

    5.valitate 关键字

    5.1 定义

    java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

    valitate是轻量级的synchronized,不会引起线程上下文的切换和调度,执行开销更小。

    5.2 原理

    1. 使用volitate修饰的变量在汇编阶段,会多出一条lock前缀指令
    2. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
    3. 它会强制将对缓存的修改操作立即写入主存
    4. 如果是写操作,它会导致其他CPU里缓存了该内存地址的数据无效

    5.3 作用

    内存可见性
    多线程操作的时候,一个线程修改了一个变量的值 ,其他线程能立即看到修改后的值
    防止重排序
    即程序的执行顺序按照代码的顺序执行(处理器为了提高代码的执行效率可能会对代码进行重排序)

    并不能保证操作的原子性(比如下面这段代码的执行结果一定不是100000)

    
        public class testValitate {
        public volatile int inc = 0;
        public void increase() {
            inc = inc + 1;
        }
        public static void main(String[] args) {
            final testValitate test = new testValitate();
            for (int i = 0; i < 100; i++) {
                new Thread() {
                    public void run() {
                        for (int j = 0; j < 1000; j++)
                            test.increase();
                    }
                }.start();
            }
            while (Thread.activeCount() > 2) {  //保证前面的线程都执行完
                Thread.yield();
            }
            System.out.println(test.inc);
         }
       }
    

    6. synchronized 关键字

    确保线程互斥的访问同步代码

    6.1 定义

    synchronized 是JVM实现的一种锁,其中锁的获取和释放分别是
    monitorenter 和 monitorexit 指令,该锁在实现上分为了偏向锁、轻量级锁和重量级锁,其中偏向锁在 java1.6 是默认开启的,轻量级锁在多线程竞争的情况下会膨胀成重量级锁,有关锁的数据都保存在对象头中

    6.2 原理

    加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令(利用javap -verbose 字节码文件可看到关,关于这两条指令的文档如下:

    • monitorenter
      Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
      • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
      • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
      • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.​

    • monitorexit
      The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
      The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.​​

    加了 synchronized 关键字的方法,生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

    6.3 关于使用

    • 修饰普通方法
      同步对象是实例对象
    • 修饰静态方法
      同步对象是类本身
    • 修饰代码块
      可以自己设置同步对象​

    6.4 缺点

    会让没有得到锁的资源进入Block状态,争夺到资源之后又转为Running状态,这个过程涉及到操作系统用户模式和内核模式的切换,代价比较高。Java1.6为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

    7. CAS

    AtomicBoolean,AtomicInteger,AtomicLong以及 Lock 相关类等底层就是用 CAS实现的,在一定程度上性能比 synchronized 更高。

    7.1 什么是CAS

    CAS全称是Compare And Swap,即比较替换,是实现并发应用到的一种技术。操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

    7.2 为什么会有CAS

    如果只是用 synchronized 来保证同步会存在以下问题
    synchronized 是一种悲观锁,在使用上会造成一定的性能问题。在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。一个线程持有锁会导致其它所有需要此锁的线程挂起。

    7.3 实现原理

    Java不能直接的访问操作系统底层,是通过native方法(JNI)来访问。CAS底层通过Unsafe类实现原子性操作。

    7.4 存在的问题

    • ABA问题
      什么是ABA问题?比如有一个 int 类型的值 N 是 1
      此时有三个线程想要去改变它:
      线程A ​​:希望给 N 赋值为 2
      线程B: 希望给 N 赋值为 2
      线程C: 希望给 N 赋值为 1​​
      此时线程A和线程B同时获取到N的值1,线程A率先得到系统资源,将 N 赋值为 2,线程 B 由于某种原因被阻塞住,线程C在线程A执行完后得到 N 的当前值2
      此时的线程状态
      线程A成功给 N 赋值为2
      线程B获取到 N 的当前值 1 希望给他赋值为 2,处于阻塞状态
      线程C获取当好 N 的当前值 2 ​​​​​希望给他赋值为1
      ​​
      然后线程C成功给N赋值为1
      ​最后线程B得到了系统资源,又重新恢复了运行状态,​在阻塞之前线程B获取到的N的值是1,执行compare操作发现当前N的值与获取到的值相同(均为1),成功将N赋值为了2。

      在这个过程中线程B获取到N的值是一个旧值​​,虽然和当前N的值相等,但是实际上N的值已经经历了一次 1到2到1的改变
      上面这个例子就是典型的ABA问题​
      怎样去解决ABA问题
      给变量加一个版本号即可,在比较的时候不仅要比较当前变量的值 还需要比较当前变量的版本号。Java中AtomicStampedReference 就解决了这个问题
    • 循环时间长开销大
      在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

    CAS只能保证一个共享变量的原子操作

    8. AbstractQueuedSynchronizer(AQS)

    AQS抽象的队列式同步器,是一种基于状态(state)的链表管理方式。state 是用CAS去修改的。它是 java.util.concurrent 包中最重要的基石,要学习想学习 java.util.concurrent 包里的内容这个类是关键。 ReentrantLock​、CountDownLatcher、Semaphore 实现的原理就是基于AQS。想知道他怎么实现以及实现原理 可以参看这篇文章https://www.cnblogs.com/waterystone/p/4920797.html

    9. Future

    在并发编程我们一般使用Runable去执行异步任务,然而这样做我们是不能拿到异步任务的返回值的,但是使用Future 就可以。使用Future很简单,只需把Runable换成FutureTask即可。使用上比较简单,这里不多做介绍。

    10. 线程池

    如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。

    Java中线程池的实现类 ThreadPoolExecutor,其构造函数的每一个参数的含义在注释上已经写得很清楚了,这里几个关键参数可以再简单说一下

    • corePoolSize :核心线程数即一直保留在线程池中的线程数量,即使处于闲置状态也不会被销毁。要设置 allowCoreThreadTimeOut 为 true,才会被销毁。
    • maximumPoolSize:线程池中允许存在的最大线程数
    • keepAliveTime :非核心线程允许的最大闲置时间,超过这个时间就会本地销毁。
    • workQueue:用来存放任务的队列。
      • SynchronousQueue:这个队列会让新添加的任务立即得到执行,如果线程池中所有的线程都在执行,那么就会去创建一个新的线程去执行这个任务。当使用这个队列的时候,maximumPoolSizes一般都会设置一个最大值 Integer.MAX_VALUE
      • LinkedBlockingQueue:这个队列是一个无界队列。怎么理解呢,就是有多少任务来我们就会执行多少任务,如果线程池中的线程小于corePoolSize ,我们就会创建一个新的线程去执行这个任务,如果线程池中的线程数等于corePoolSize,就会将任务放入队列中等待,由于队列大小没有限制所以也被称为无界队列。当使用这个队列的时候 maximumPoolSizes 不生效(线程池中线程的数量不会超过corePoolSize),所以一般都会设置为0。
      • ArrayBlockingQueue:这个队列是一个有界队列。可以设置队列的最大容量。当线程池中线程数大于或者等于 maximumPoolSizes 的时候,就会把任务放到这个队列中,当当前队列中的任务大于队列的最大容量就会丢弃掉该任务交由 RejectedExecutionHandler 处理。

    最后,本文主要对Java并发编程开发需要的知识点作了简单的讲解,这里每一个知识点都可以用一篇文章去讲解,由于篇幅原因不能对每一个知识点都详细介绍,我相信通过本文你会对Java的并发编程会有更近一步的了解。如果您发现还有缺漏或者有错误的地方,可以在评论区补充,谢谢。

    个人公众号:程序员黄小斜

    微信公众号【程序员黄小斜】新生代青年聚集地,程序员成长充电站。作者黄小斜,职业是阿里程序员,身份是斜杠青年,希望和更多的程序员交朋友,一起进步和成长!专注于分享技术、面试、职场等成长干货,这一次,我们一起出发。

    关注公众号后回复“2019”领取我这两年整理的学习资料,涵盖自学编程、求职面试、算法刷题、Java技术学习、计算机基础和考研等8000G资料合集。

    技术公众号:Java技术江湖

    微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,专注于 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!

    关注公众号后回复“PDF”即可领取200+页的《Java工程师面试指南》强烈推荐,几乎涵盖所有Java工程师必知必会的知识点。

    展开全文
  • 【死磕Java并发】----- 死磕 Java 并发精品合集

    万次阅读 多人点赞 2018-07-22 15:12:29
    【死磕 Java 并发】系列是 LZ 在 2017 年写的第一个死磕系列... 【死磕Java并发】—–深入分析synchronized 的实现原理 synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同...
  • [超级链接:Java并发学习系列-绪论] 本章主要对Java并发(Concurrent)在不同jdk版本中的发展简史进行学习。 Java语言从第一版本至今,内置了对并发(Concurrent)的各种支持技术。 为了能够让我们在学习Java并发...
  • java并发编程面试题

    万次阅读 2020-09-28 23:25:46
    4、JVM对Java的原生锁做了哪些优化?5、为什么说Synchronized是非公平锁?6、什么是锁消除和锁粗化?7、为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么特性?8、乐观锁一定就是好...
  • 本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star!【Java学习 面试指南】 一份涵盖大部分Java程序员所...Java 并发基础常见面试题总结 1. 什么是线程和进程? 1.1. 何为进程? 进程是程...
  • Java并发编程基础与实战
  • Java并发编程精讲

    万人学习 2019-09-28 15:16:34
    课程会讲解Java并发相关技术的基础、原理和应用,从线程安全、线程(池), 锁实现和并发容器等高并发Java实现,去深入理解在并发编程中, 一些最容易被忽视的点,这些点也是我在多年编程经验中实际用到, 对于每...
  • java并发实战

    千次阅读 2019-03-03 00:34:35
    推荐一个Java并发编程实战的学习专栏。此专栏为极客时间收费专栏。 学习交流加 个人qq: 1126137994 个人微信: liu1126137994 学习交流资源分享qq群: 962535112 对于一个 Java 程序员而言, 能否熟练掌握...
  • 谈谈我对Java并发的理解——读《Java并发编程实战有感》
  • Java 并发工具包 java.util.concurrent 用户指南

    万次阅读 多人点赞 2015-03-03 09:40:29
    1. java.util.concurrent - Java 并发工具包Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包。这个包包含有一系列能够让 Java并发编程变得更加简单轻松的类。在这个包被添加以前,你需要自己去...
  • java并发编程

    2017-01-17 14:37:14
    为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章    【Java并发编程】实现多线程的两种方法  【Java...
  • Java并发机制及锁的实现原理

    万次阅读 多人点赞 2016-07-18 20:04:21
    Java并发机制及锁实现原理
  • Java 并发源码合集

    2018-04-11 00:00:00
    【死磕Java并发】—– 深入分析synchronized的实现原理【死磕Java并发】—– 深入分析volatile的实现原理【死磕Java并发】—– Java内存模型之happens-before【死磕Java并发】—– Java内存模型之重排序【死磕Java...
  • ReadWriteLock读写锁的使用(Java并发

    万次阅读 2019-03-27 13:53:27
    说到Java并发编程,很多开发第一个想到同时也是经常常用的肯定是Synchronized,但是Synchronized存在明显的一个性能问题就是读与读之间互斥,简言之就是,我们编程想要实现的最好效果是,可以做到读和读互不影响,读...
  • Java 并发编程的艺术

    千次阅读 2019-01-11 23:30:52
    书中采用循序渐进的讲解方式,从并发编程的底层实现机制入手,逐步介绍了在设计 Java 并发程序时各种重要的技术、设计模式与应用,同时辅以丰富的示例代码,使得开发人员能够更快地领悟 Java 并发编程的要领,围绕着...
  • Java并发技术学习总结

    千次阅读 2018-06-28 14:40:06
    Java并发 这篇总结主要是基于我Java并发技术系列的文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 更多详细内容可以查看我的专栏文章:Java并发技术指南 ...
  • Java并发编程】实现多线程的两种方法 【Java并发编程】线程的中断 【Java并发编程】正确挂起、恢复、终止线程 【Java并发编程】守护线程和线程阻塞 【Java并发编程】Volatile关键字(上) 【Java并发编程】...
  • Java并发知识点汇总

    万次阅读 2017-09-22 11:22:34
    Java并发知识点汇总 Java并发相关的教程很多,其一是并发一直是开发人员必备技能,其二是并发本身涉及内容较多。但是网上关于Java并发相关的博客均比较散乱,没有自成一体。本文尝试从并发知识体系上相关的做一个...
  • 根据《Java并发编程实践》一书
  • [Java 并发] Java并发编程实践 思维导图 - 第四章 对象的组合。 整理的思维导图,希望对大家有所帮助。
  • Java - Java并发脑图

    千次阅读 2018-04-25 18:45:30
    转载自: https://www.jianshu.com/p/d8a7cbf124c5 图片连接: https://upload-images.jianshu.io/upload_images/2615789-2465df1a22cfde14.png Java并发脑图高清版
  • 摘要:这是翻译自一个大概30个小节的关于Java并发编程的入门级教程,原作者Jakob Jenkov,译者Zhenning Lang,转载请注明出处,thanks and have a good time here~~
  • Java并发API案例分析之并发设计原理

    千次阅读 多人点赞 2021-01-15 12:07:58
    并发、并行、同步、锁、线程安全……这一篇我记录了学习并发设计原理的关键知识,还有一些常用重要的Java并发编程API,学习一些关于并发程序设计的原理,弄懂来龙去脉,相对更加深入地理解并这部分知识。在学习理论...
  • Java并发编程完整总结

    千次阅读 2018-06-15 00:11:18
    Java并发编程系列: 【Java并发编程】实现多线程的两种方法 【Java并发编程】线程的中断 【Java并发编程】正确挂起、恢复、终止线程 【Java并发编程】守护线程和线程阻塞 【Java并发编程】Volatile关键字(上)...
  • Java并发编程之美

    千次阅读 2018-09-01 15:21:57
    一、前言 并发编程相比 Java 中其他知识点学习门槛较高,从而导致很多人望而却步。但无论是职场面试,还是高并发/高流量的系统的...- 第一部分Java并发编程基础篇主要讲解Java并发编程的基础知识,主要讲解线程有关...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 118,134
精华内容 47,253
关键字:

java并发

java 订阅