java 并发编程学习_java 并发编程实战和 并发编程艺术 - CSDN
  • 如和学习java并发编程

    2017-01-01 17:09:56
    学习java并发必读

            伟大的《java编程思想》曾经说过,(原谅我用伟大这两个字,确实是博主看过的关于Java最好的书,也是这本书,让我对java有了全新的理解)

            学习java并发就像进入了另外一个学习领域,就像学习一门新的编程语言,或者是学习一套新的语言概念,要理解并发编程,其难度跟理解面向对象编程难度差不多,;你花一点功夫,就可以理解它的基本机制,但是要想真正掌握它的本质,就需要深入的学习与理解;

             博主将并发的学习分为三部曲:

             1.理解java线程实现的原理

                      主要涉及到操作系统的线程,java虚拟机线程的映射以及什么是抢占式的线程调度;
                  理解并发,java和跟它没什么关系的线程

             2.学习java线程类库

                      深入理解Volatile关键字

             3.并发编程实战



    (博主现在也在学习java并发的路上,会在后续的学习中将内容更新。。。。)

    展开全文
  • Java并发编程学习

    2018-05-09 09:02:28
    转载出处:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的...

    转载出处:http://blog.csdn.net/ns_code/article/details/17539599

     

       为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章

     

       【Java并发编程】实现多线程的两种方法

       【Java并发编程】线程的中断

       【Java并发编程】正确挂起、恢复、终止线程

       【Java并发编程】守护线程和线程阻塞

       【Java并发编程】Volatile关键字(上)

       【Java并发编程】Volatile关键字(下)

       【Java并发编程】synchronized关键字

       【Java并发编程】synchronized的另个一重要作用:内存可见性

       【Java并发编程】实现内存可见性的两种方法比较:synchronized和Volatile

       【Java并发编程】多线程环境下安全使用集合API

       【Java并发编程】死锁

       【Java并发编程】可重入内置锁

       【Java并发编程】线程间协作:wait、notify、notifyAll

       【Java并发编程】notify通知的遗漏

       【Java并发编程】notifyAll造成的早期通知问题

       【Java并发编程】生产者—消费者模型

       【Java并发编程】深入Java内存模型(1)——happen—before规则及其对DCL问题的分析

       【Java并发编程】深入Java内存模型(2)——内存操作规则总结

       【Java并发编程】并发新特性—Executor框架与线程池

       【Java并发编程】并发新特性—Lock锁与条件变量

       【Java并发编程】并发新特性—阻塞队列与阻塞栈

       【Java并发编程】并发新特性—障碍器CyclicBarrier

       【Java并发编程】并发新特性—信号量Semaphore

    展开全文
  • 当然这些都是并发编程的基本知识,除了使用这些工具以外,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语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更好的选择。本文是对并发编程的一点总结和思考,同时也分享了Java 5以后的版本中如何编写并发代码的一点点经验。

    为什么需要并发

      并发其实是一种解耦合的策略,它帮助我们把做什么(目标)和什么时候做(时机)分开。这样做可以明显改进应用程序的吞吐量(获得更多的CPU调度时间)和结构(程序有多个部分在协同工作)。做过Java Web开发的人都知道,Java Web中的Servlet程序在Servlet容器的支持下采用单实例多线程的工作模式,Servlet容器为你处理了并发问题。

    误解和正解

      最常见的对并发编程的误解有以下这些:

    -并发总能改进性能(并发在CPU有很多空闲时间时能明显改进程序的性能,但当线程数量较多的时候,线程间频繁的调度切换反而会让系统的性能下降)
    -编写并发程序无需修改原有的设计(目的与时机的解耦往往会对系统结构产生巨大的影响)
    -在使用Web或EJB容器时不用关注并发问题(只有了解了容器在做什么,才能更好的使用容器)

      下面的这些说法才是对并发客观的认识:

    -编写并发程序会在代码上增加额外的开销
    -正确的并发是非常复杂的,即使对于很简单的问题
    -并发中的缺陷因为不易重现也不容易被发现
    -并发往往需要对设计策略从根本上进行修改

    并发编程的原则和技巧

    单一职责原则

    分离并发相关代码和其他代码(并发相关代码有自己的开发、修改和调优生命周期)。

    限制数据作用域

    两个线程修改共享对象的同一字段时可能会相互干扰,导致不可预期的行为,解决方案之一是构造临界区,但是必须限制临界区的数量。

    使用数据副本

    数据副本是避免共享数据的好方法,复制出来的对象只是以只读的方式对待。Java 5的java.util.concurrent包中增加一个名为CopyOnWriteArrayList的类,它是List接口的子类型,所以你可以认为它是ArrayList的线程安全的版本,它使用了写时复制的方式创建数据副本进行操作来避免对共享数据并发访问而引发的问题。

    线程应尽可能独立

    让线程存在于自己的世界中,不与其他线程共享数据。有过Java Web开发经验的人都知道,Servlet就是以单实例多线程的方式工作,和每个请求相关的数据都是通过Servlet子类的service方法(或者是doGet或doPost方法)的参数传入的。只要Servlet中的代码只使用局部变量,Servlet就不会导致同步问题。Spring MVC的控制器也是这么做的,从请求中获得的对象都是以方法的参数传入而不是作为类的成员,很明显Struts 2的做法就正好相反,因此Struts 2中作为控制器的Action类都是每个请求对应一个实例。

    Java 5以前的并发编程

    Java的线程模型建立在抢占式线程调度的基础上,也就是说:

    • 所有线程可以很容易的共享同一进程中的对象。
    • 能够引用这些对象的任何线程都可以修改这些对象。
    • 为了保护数据,对象可以被锁住。

      Java基于线程和锁的并发过于底层,而且使用锁很多时候都是很万恶的,因为它相当于让所有的并发都变成了排队等待。
      在Java 5以前,可以用synchronized关键字来实现锁的功能,它可以用在代码块和方法上,表示在执行整个代码块或方法之前线程必须取得合适的锁。对于类的非静态方法(成员方法)而言,这意味这要取得对象实例的锁,对于类的静态方法(类方法)而言,要取得类的Class对象的锁,对于同步代码块,程序员可以指定要取得的是那个对象的锁。
      不管是同步代码块还是同步方法,每次只有一个线程可以进入,如果其他线程试图进入(不管是同一同步块还是不同的同步块),JVM会将它们挂起(放入到等锁池中)。这种结构在并发理论中称为临界区(critical section)。这里我们可以对Java中用synchronized实现同步和锁的功能做一个总结:

    • 只能锁定对象,不能锁定基本数据类型
    • 被锁定的对象数组中的单个对象不会被锁定
    • 同步方法可以视为包含整个方法的synchronized(this) { … }代码块
    • 静态同步方法会锁定它的Class对象
    • 内部类的同步是独立于外部类的
    • synchronized修饰符并不是方法签名的组成部分,所以不能出现在接口的方法声明中
    • 非同步的方法不关心锁的状态,它们在同步方法运行时仍然可以得以运行
    • synchronized实现的锁是可重入的锁。

      在JVM内部,为了提高效率,同时运行的每个线程都会有它正在处理的数据的缓存副本,当我们使用synchronzied进行同步的时候,真正被同步的是在不同线程中表示被锁定对象的内存块(副本数据会保持和主内存的同步,现在知道为什么要用同步这个词汇了吧),简单的说就是在同步块或同步方法执行完后,对被锁定的对象做的任何修改要在释放锁之前写回到主内存中;在进入同步块得到锁之后,被锁定对象的数据是从主内存中读出来的,持有锁的线程的数据副本一定和主内存中的数据视图是同步的 。
      在Java最初的版本中,就有一个叫volatile的关键字,它是一种简单的同步的处理机制,因为被volatile修饰的变量遵循以下规则:

    • 变量的值在使用之前总会从主内存中再读取出来。
    • 对变量值的修改总会在完成之后写回到主内存中。

      使用volatile关键字可以在多线程环境下预防编译器不正确的优化假设(编译器可能会将在一个线程中值不会发生改变的变量优化成常量),但只有修改时不依赖当前状态(读取时的值)的变量才应该声明为volatile变量。
      不变模式也是并发编程时可以考虑的一种设计。让对象的状态是不变的,如果希望修改对象的状态,就会创建对象的副本并将改变写入副本而不改变原来的对象,这样就不会出现状态不一致的情况,因此不变对象是线程安全的。Java中我们使用频率极高的String类就采用了这样的设计。如果对不变模式不熟悉,可以阅读阎宏博士的《Java与模式》一书的第34章。说到这里你可能也体会到final关键字的重要意义了。

    Java 5的并发编程

    不管今后的Java向着何种方向发展或者灭忙,Java 5绝对是Java发展史中一个极其重要的版本,这个版本提供的各种语言特性我们不在这里讨论(有兴趣的可以阅读我的另一篇文章《Java的第20年:从Java版本演进看编程技术的发展》),但是我们必须要感谢Doug Lea在Java 5中提供了他里程碑式的杰作java.util.concurrent包,它的出现让Java的并发编程有了更多的选择和更好的工作方式。Doug Lea的杰作主要包括以下内容:

    • 更好的线程安全的容器
    • 线程池和相关的工具类
    • 可选的非阻塞解决方案
    • 显示的锁和信号量机制

    下面我们对这些东西进行一一解读。

    原子类

    Java 5中的java.util.concurrent包下面有一个atomic子包,其中有几个以Atomic打头的类,例如AtomicInteger和AtomicLong。它们利用了现代处理器的特性,可以用非阻塞的方式完成原子操作,代码如下所示:

    /**
     ID序列生成器
    */
    public class IdGenerator {
        private final AtomicLong sequenceNumber = new AtomicLong(0);
    
        public long next() {
            return sequenceNumber.getAndIncrement(); 
        }
    }

    显示锁

    基于synchronized关键字的锁机制有以下问题:

    • 锁只有一种类型,而且对所有同步操作都是一样的作用
    • 锁只能在代码块或方法开始的地方获得,在结束的地方释放
    • 线程要么得到锁,要么阻塞,没有其他的可能性

    Java 5对锁机制进行了重构,提供了显示的锁,这样可以在以下几个方面提升锁机制:

    • 可以添加不同类型的锁,例如读取锁和写入锁
    • 可以在一个方法中加锁,在另一个方法中解锁
    • 可以使用tryLock方式尝试获得锁,如果得不到锁可以等待、回退或者干点别的事情,当然也可以在超时之后放弃操作

    显示的锁都实现了java.util.concurrent.Lock接口,主要有两个实现类:

    • ReentrantLock - 比synchronized稍微灵活一些的重入锁
    • ReentrantReadWriteLock - 在读操作很多写操作很少时性能更好的一种重入锁

    对于如何使用显示锁,可以参考我的Java面试系列文章《Java面试题集51-70》中第60题的代码。只有一点需要提醒,解锁的方法unlock的调用最好能够在finally块中,因为这里是释放外部资源最好的地方,当然也是释放锁的最佳位置,因为不管正常异常可能都要释放掉锁来给其他线程以运行的机会。

    CountDownLatch

    CountDownLatch是一种简单的同步模式,它让一个线程可以等待一个或多个线程完成它们的工作从而避免对临界资源并发访问所引发的各种问题。下面借用别人的一段代码(我对它做了一些重构)来演示CountDownLatch是如何工作的。

    import java.util.concurrent.CountDownLatch;
    
    /**
     * 工人类
     * @author 骆昊
     *
     */
    class Worker {
        private String name;        // 名字
        private long workDuration;  // 工作持续时间
    
        /**
         * 构造器
         */
        public Worker(String name, long workDuration) {
            this.name = name;
            this.workDuration = workDuration;
        }
    
        /**
         * 完成工作
         */
        public void doWork() {
            System.out.println(name + " begins to work...");
            try {
                Thread.sleep(workDuration); // 用休眠模拟工作执行的时间
            } catch(InterruptedException ex) {
                ex.printStackTrace();
            }
            System.out.println(name + " has finished the job...");
        }
    }
    
    /**
     * 测试线程
     * @author 骆昊
     *
     */
    class WorkerTestThread implements Runnable {
        private Worker worker;
        private CountDownLatch cdLatch;
    
        public WorkerTestThread(Worker worker, CountDownLatch cdLatch) {
            this.worker = worker;
            this.cdLatch = cdLatch;
        }
    
        @Override
        public void run() {
            worker.doWork();        // 让工人开始工作
            cdLatch.countDown();    // 工作完成后倒计时次数减1
        }
    }
    
    class CountDownLatchTest {
    
        private static final int MAX_WORK_DURATION = 5000;  // 最大工作时间
        private static final int MIN_WORK_DURATION = 1000;  // 最小工作时间
    
        // 产生随机的工作时间
        private static long getRandomWorkDuration(long min, long max) {
            return (long) (Math.random() * (max - min) + min);
        }
    
        public static void main(String[] args) {
            CountDownLatch latch = new CountDownLatch(2);   // 创建倒计时闩并指定倒计时次数为2
            Worker w1 = new Worker("骆昊", getRandomWorkDuration(MIN_WORK_DURATION, MAX_WORK_DURATION));
            Worker w2 = new Worker("王大锤", getRandomWorkDuration(MIN_WORK_DURATION, MAX_WORK_DURATION));
    
            new Thread(new WorkerTestThread(w1, latch)).start();
            new Thread(new WorkerTestThread(w2, latch)).start();
    
            try {
                latch.await();  // 等待倒计时闩减到0
                System.out.println("All jobs have been finished!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    ConcurrentHashMap

      ConcurrentHashMap是HashMap在并发环境下的版本,大家可能要问,既然已经可以通过Collections.synchronizedMap获得线程安全的映射型容器,为什么还需要ConcurrentHashMap呢?因为通过Collections工具类获得的线程安全的HashMap会在读写数据时对整个容器对象上锁,这样其他使用该容器的线程无论如何也无法再获得该对象的锁,也就意味着要一直等待前一个获得锁的线程离开同步代码块之后才有机会执行。实际上,HashMap是通过哈希函数来确定存放键值对的桶(桶是为了解决哈希冲突而引入的),修改HashMap时并不需要将整个容器锁住,只需要锁住即将修改的“桶”就可以了。HashMap的数据结构如下图所示。
    这里写图片描述

      此外,ConcurrentHashMap还提供了原子操作的方法,如下所示:

    • putIfAbsent:如果还没有对应的键值对映射,就将其添加到HashMap中。
    • remove:如果键存在而且值与当前状态相等(equals比较结果为true),则用原子方式移除该键值对映射
    • replace:替换掉映射中元素的原子操作

    CopyOnWriteArrayList

      CopyOnWriteArrayList是ArrayList在并发环境下的替代品。CopyOnWriteArrayList通过增加写时复制语义来避免并发访问引起的问题,也就是说任何修改操作都会在底层创建一个列表的副本,也就意味着之前已有的迭代器不会碰到意料之外的修改。这种方式对于不要严格读写同步的场景非常有用,因为它提供了更好的性能。记住,要尽量减少锁的使用,因为那势必带来性能的下降(对数据库中数据的并发访问不也是如此吗?如果可以的话就应该放弃悲观锁而使用乐观锁),CopyOnWriteArrayList很明显也是通过牺牲空间获得了时间(在计算机的世界里,时间和空间通常是不可调和的矛盾,可以牺牲空间来提升效率获得时间,当然也可以通过牺牲时间来减少对空间的使用)。
    这里写图片描述

      可以通过下面两段代码的运行状况来验证一下CopyOnWriteArrayList是不是线程安全的容器。

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    class AddThread implements Runnable {
        private List<Double> list;
    
        public AddThread(List<Double> list) {
            this.list = list;
        }
    
        @Override
        public void run() {
            for(int i = 0; i < 10000; ++i) {
                list.add(Math.random());
            }
        }
    }
    
    public class Test05 {
        private static final int THREAD_POOL_SIZE = 2;
    
        public static void main(String[] args) {
            List<Double> list = new ArrayList<>();
            ExecutorService es = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
            es.execute(new AddThread(list));
            es.execute(new AddThread(list));
            es.shutdown();
        }
    }

    上面的代码会在运行时产生ArrayIndexOutOfBoundsException,试一试将上面代码25行的ArrayList换成CopyOnWriteArrayList再重新运行。

    List<Double> list = new CopyOnWriteArrayList<>();

    Queue

      队列是一个无处不在的美妙概念,它提供了一种简单又可靠的方式将资源分发给处理单元(也可以说是将工作单元分配给待处理的资源,这取决于你看待问题的方式)。实现中的并发编程模型很多都依赖队列来实现,因为它可以在线程之间传递工作单元。
      Java 5中的BlockingQueue就是一个在并发环境下非常好用的工具,在调用put方法向队列中插入元素时,如果队列已满,它会让插入元素的线程等待队列腾出空间;在调用take方法从队列中取元素时,如果队列为空,取出元素的线程就会阻塞。
    这里写图片描述
      可以用BlockingQueue来实现生产者-消费者并发模型(下一节中有介绍),当然在Java 5以前也可以通过wait和notify来实现线程调度,比较一下两种代码就知道基于已有的并发工具类来重构并发代码到底好在哪里了。

    • 基于wait和notify的实现
    import java.util.ArrayList;
    import java.util.List;
    import java.util.UUID;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 公共常量
     * @author 骆昊
     *
     */
    class Constants {
        public static final int MAX_BUFFER_SIZE = 10;
        public static final int NUM_OF_PRODUCER = 2;
        public static final int NUM_OF_CONSUMER = 3;
    }
    
    /**
     * 工作任务
     * @author 骆昊
     *
     */
    class Task {
        private String id;  // 任务的编号
    
        public Task() {
            id = UUID.randomUUID().toString();
        }
    
        @Override
        public String toString() {
            return "Task[" + id + "]";
        }
    }
    
    /**
     * 消费者
     * @author 骆昊
     *
     */
    class Consumer implements Runnable {
        private List<Task> buffer;
    
        public Consumer(List<Task> buffer) {
            this.buffer = buffer;
        }
    
        @Override
        public void run() {
            while(true) {
                synchronized(buffer) {
                    while(buffer.isEmpty()) {
                        try {
                            buffer.wait();
                        } catch(InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    Task task = buffer.remove(0);
                    buffer.notifyAll();
                    System.out.println("Consumer[" + Thread.currentThread().getName() + "] got " + task);
                }
            }
        }
    }
    
    /**
     * 生产者
     * @author 骆昊
     *
     */
    class Producer implements Runnable {
        private List<Task> buffer;
    
        public Producer(List<Task> buffer) {
            this.buffer = buffer;
        }
    
        @Override
        public void run() {
            while(true) {
                synchronized (buffer) {
                    while(buffer.size() >= Constants.MAX_BUFFER_SIZE) {
                        try {
                            buffer.wait();
                        } catch(InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    Task task = new Task();
                    buffer.add(task);
                    buffer.notifyAll();
                    System.out.println("Producer[" + Thread.currentThread().getName() + "] put " + task);
                }
            }
        }
    
    }
    
    public class Test06 {
    
        public static void main(String[] args) {
            List<Task> buffer = new ArrayList<>(Constants.MAX_BUFFER_SIZE);
            ExecutorService es = Executors.newFixedThreadPool(Constants.NUM_OF_CONSUMER + Constants.NUM_OF_PRODUCER);
            for(int i = 1; i <= Constants.NUM_OF_PRODUCER; ++i) {
                es.execute(new Producer(buffer));
            }
            for(int i = 1; i <= Constants.NUM_OF_CONSUMER; ++i) {
                es.execute(new Consumer(buffer));
            }
        }
    }
    
    • 基于BlockingQueue的实现
    import java.util.UUID;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.LinkedBlockingQueue;
    
    /**
     * 公共常量
     * @author 骆昊
     *
     */
    class Constants {
        public static final int MAX_BUFFER_SIZE = 10;
        public static final int NUM_OF_PRODUCER = 2;
        public static final int NUM_OF_CONSUMER = 3;
    }
    
    /**
     * 工作任务
     * @author 骆昊
     *
     */
    class Task {
        private String id;  // 任务的编号
    
        public Task() {
            id = UUID.randomUUID().toString();
        }
    
        @Override
        public String toString() {
            return "Task[" + id + "]";
        }
    }
    
    /**
     * 消费者
     * @author 骆昊
     *
     */
    class Consumer implements Runnable {
        private BlockingQueue<Task> buffer;
    
        public Consumer(BlockingQueue<Task> buffer) {
            this.buffer = buffer;
        }
    
        @Override
        public void run() {
            while(true) {
                try {
                    Task task = buffer.take();
                    System.out.println("Consumer[" + Thread.currentThread().getName() + "] got " + task);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    /**
     * 生产者
     * @author 骆昊
     *
     */
    class Producer implements Runnable {
        private BlockingQueue<Task> buffer;
    
        public Producer(BlockingQueue<Task> buffer) {
            this.buffer = buffer;
        }
    
        @Override
        public void run() {
            while(true) {
                try {
                    Task task = new Task();
                    buffer.put(task);
                    System.out.println("Producer[" + Thread.currentThread().getName() + "] put " + task);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            }
        }
    
    }
    
    public class Test07 {
    
        public static void main(String[] args) {
            BlockingQueue<Task> buffer = new LinkedBlockingQueue<>(Constants.MAX_BUFFER_SIZE);
            ExecutorService es = Executors.newFixedThreadPool(Constants.NUM_OF_CONSUMER + Constants.NUM_OF_PRODUCER);
            for(int i = 1; i <= Constants.NUM_OF_PRODUCER; ++i) {
                es.execute(new Producer(buffer));
            }
            for(int i = 1; i <= Constants.NUM_OF_CONSUMER; ++i) {
                es.execute(new Consumer(buffer));
            }
        }
    }

      使用BlockingQueue后代码优雅了很多。

    并发模型

      在继续下面的探讨之前,我们还是重温一下几个概念:

    概念 解释
    临界资源 并发环境中有着固定数量的资源
    互斥 对资源的访问是排他式的
    饥饿 一个或一组线程长时间或永远无法取得进展
    死锁 两个或多个线程相互等待对方结束
    活锁 想要执行的线程总是发现其他的线程正在执行以至于长时间或永远无法执行



      重温了这几个概念后,我们可以探讨一下下面的几种并发模型。

    生产者-消费者

      一个或多个生产者创建某些工作并将其置于缓冲区或队列中,一个或多个消费者会从队列中获得这些工作并完成之。这里的缓冲区或队列是临界资源。当缓冲区或队列放满的时候,生产这会被阻塞;而缓冲区或队列为空的时候,消费者会被阻塞。生产者和消费者的调度是通过二者相互交换信号完成的。

    读者-写者

      当存在一个主要为读者提供信息的共享资源,它偶尔会被写者更新,但是需要考虑系统的吞吐量,又要防止饥饿和陈旧资源得不到更新的问题。在这种并发模型中,如何平衡读者和写者是最困难的,当然这个问题至今还是一个被热议的问题,恐怕必须根据具体的场景来提供合适的解决方案而没有那种放之四海而皆准的方法(不像我在国内的科研文献中看到的那样)。

    哲学家进餐

      1965年,荷兰计算机科学家图灵奖得主Edsger Wybe Dijkstra提出并解决了一个他称之为哲学家进餐的同步问题。这个问题可以简单地描述如下:五个哲学家围坐在一张圆桌周围,每个哲学家面前都有一盘通心粉。由于通心粉很滑,所以需要两把叉子才能夹住。相邻两个盘子之间放有一把叉子如下图所示。哲学家的生活中有两种交替活动时段:即吃饭和思考。当一个哲学家觉得饿了时,他就试图分两次去取其左边和右边的叉子,每次拿一把,但不分次序。如果成功地得到了两把叉子,就开始吃饭,吃完后放下叉子继续思考。
      把上面问题中的哲学家换成线程,把叉子换成竞争的临界资源,上面的问题就是线程竞争资源的问题。如果没有经过精心的设计,系统就会出现死锁、活锁、吞吐量下降等问题。
    这里写图片描述
      下面是用信号量原语来解决哲学家进餐问题的代码,使用了Java 5并发工具包中的Semaphore类(代码不够漂亮但是已经足以说明问题了)。

    //import java.util.concurrent.ExecutorService;
    //import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    /**
     * 存放线程共享信号量的上下问
     * @author 骆昊
     *
     */
    class AppContext {
        public static final int NUM_OF_FORKS = 5;   // 叉子数量(资源)
        public static final int NUM_OF_PHILO = 5;   // 哲学家数量(线程)
    
        public static Semaphore[] forks;    // 叉子的信号量
        public static Semaphore counter;    // 哲学家的信号量
    
        static {
            forks = new Semaphore[NUM_OF_FORKS];
    
            for (int i = 0, len = forks.length; i < len; ++i) {
                forks[i] = new Semaphore(1);    // 每个叉子的信号量为1
            }
    
            counter = new Semaphore(NUM_OF_PHILO - 1);  // 如果有N个哲学家,最多只允许N-1人同时取叉子
        }
    
        /**
         * 取得叉子
         * @param index 第几个哲学家
         * @param leftFirst 是否先取得左边的叉子
         * @throws InterruptedException
         */
        public static void putOnFork(int index, boolean leftFirst) throws InterruptedException {
            if(leftFirst) {
                forks[index].acquire();
                forks[(index + 1) % NUM_OF_PHILO].acquire();
            }
            else {
                forks[(index + 1) % NUM_OF_PHILO].acquire();
                forks[index].acquire();
            }
        }
    
        /**
         * 放回叉子
         * @param index 第几个哲学家
         * @param leftFirst 是否先放回左边的叉子
         * @throws InterruptedException
         */
        public static void putDownFork(int index, boolean leftFirst) throws InterruptedException {
            if(leftFirst) {
                forks[index].release();
                forks[(index + 1) % NUM_OF_PHILO].release();
            }
            else {
                forks[(index + 1) % NUM_OF_PHILO].release();
                forks[index].release();
            }
        }
    }
    
    /**
     * 哲学家
     * @author 骆昊
     *
     */
    class Philosopher implements Runnable {
        private int index;      // 编号
        private String name;    // 名字
    
        public Philosopher(int index, String name) {
            this.index = index;
            this.name = name;
        }
    
        @Override
        public void run() {
            while(true) {
                try {
                    AppContext.counter.acquire();
                    boolean leftFirst = index % 2 == 0;
                    AppContext.putOnFork(index, leftFirst);
                    System.out.println(name + "正在吃意大利面(通心粉)...");   // 取到两个叉子就可以进食
                    AppContext.putDownFork(index, leftFirst);
                    AppContext.counter.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class Test04 {
    
        public static void main(String[] args) {
            String[] names = { "骆昊", "王大锤", "张三丰", "杨过", "李莫愁" };   // 5位哲学家的名字
    //      ExecutorService es = Executors.newFixedThreadPool(AppContext.NUM_OF_PHILO); // 创建固定大小的线程池
    //      for(int i = 0, len = names.length; i < len; ++i) {
    //          es.execute(new Philosopher(i, names[i]));   // 启动线程
    //      }
    //      es.shutdown();
            for(int i = 0, len = names.length; i < len; ++i) {
                new Thread(new Philosopher(i, names[i])).start();
            }
        }
    
    }

      现实中的并发问题基本上都是这三种模型或者是这三种模型的变体。

    测试并发代码

    对并发代码的测试也是非常棘手的事情,棘手到无需说明大家也很清楚的程度,所以这里我们只是探讨一下如何解决这个棘手的问题。我们建议大家编写一些能够发现问题的测试并经常性的在不同的配置和不同的负载下运行这些测试。不要忽略掉任何一次失败的测试,线程代码中的缺陷可能在上万次测试中仅仅出现一次。具体来说有这么几个注意事项:

    • 不要将系统的失效归结于偶发事件,就像拉不出屎的时候不能怪地球没有引力。
    • 先让非并发代码工作起来,不要试图同时找到并发和非并发代码中的缺陷。
    • 编写可以在不同配置环境下运行的线程代码。
    • 编写容易调整的线程代码,这样可以调整线程使性能达到最优。
    • 让线程的数量多于CPU或CPU核心的数量,这样CPU调度切换过程中潜在的问题才会暴露出来。
    • 让并发代码在不同的平台上运行。
    • 通过自动化或者硬编码的方式向并发代码中加入一些辅助测试的代码。

    Java 7的并发编程

      Java 7中引入了TransferQueue,它比BlockingQueue多了一个叫transfer的方法,如果接收线程处于等待状态,该操作可以马上将任务交给它,否则就会阻塞直至取走该任务的线程出现。可以用TransferQueue代替BlockingQueue,因为它可以获得更好的性能。
      刚才忘记了一件事情,Java 5中还引入了Callable接口、Future接口和FutureTask接口,通过他们也可以构建并发应用程序,代码如下所示。

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    
    
    public class Test07 {
        private static final int POOL_SIZE = 10;
    
        static class CalcThread implements Callable<Double> {
            private List<Double> dataList = new ArrayList<>();
    
            public CalcThread() {
                for(int i = 0; i < 10000; ++i) {
                    dataList.add(Math.random());
                }
            }
    
            @Override
            public Double call() throws Exception {
                double total = 0;
                for(Double d : dataList) {
                    total += d;
                }
                return total / dataList.size();
            }
    
        }
    
        public static void main(String[] args) {
            List<Future<Double>> fList = new ArrayList<>();
            ExecutorService es = Executors.newFixedThreadPool(POOL_SIZE);
            for(int i = 0; i < POOL_SIZE; ++i) {
                fList.add(es.submit(new CalcThread()));
            }
    
            for(Future<Double> f : fList) {
                try {
                    System.out.println(f.get());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            es.shutdown();
        }
    }

      Callable接口也是一个单方法接口,显然这是一个回调方法,类似于函数式编程中的回调函数,在Java 8 以前,Java中还不能使用Lambda表达式来简化这种函数式编程。和Runnable接口不同的是Callable接口的回调方法call方法会返回一个对象,这个对象可以用将来时的方式在线程执行结束的时候获得信息。上面代码中的call方法就是将计算出的10000个0到1之间的随机小数的平均值返回,我们通过一个Future接口的对象得到了这个返回值。目前最新的Java版本中,Callable接口和Runnable接口都被打上了@FunctionalInterface的注解,也就是说它可以用函数式编程的方式(Lambda表达式)创建接口对象。
      下面是Future接口的主要方法:

    • get():获取结果。如果结果还没有准备好,get方法会阻塞直到取得结果;当然也可以通过参数设置阻塞超时时间。
    • cancel():在运算结束前取消。
    • isDone():可以用来判断运算是否结束。

      Java 7中还提供了分支/合并(fork/join)框架,它可以实现线程池中任务的自动调度,并且这种调度对用户来说是透明的。为了达到这种效果,必须按照用户指定的方式对任务进行分解,然后再将分解出的小型任务的执行结果合并成原来任务的执行结果。这显然是运用了分治法(divide-and-conquer)的思想。下面的代码使用了分支/合并框架来计算1到10000的和,当然对于如此简单的任务根本不需要分支/合并框架,因为分支和合并本身也会带来一定的开销,但是这里我们只是探索一下在代码中如何使用分支/合并框架,让我们的代码能够充分利用现代多核CPU的强大运算能力。

    import java.util.concurrent.ForkJoinPool;
    import java.util.concurrent.Future;
    import java.util.concurrent.RecursiveTask;
    
    class Calculator extends RecursiveTask<Integer> {
        private static final long serialVersionUID = 7333472779649130114L;
    
        private static final int THRESHOLD = 10;
        private int start;
        private int end;
    
        public Calculator(int start, int end) {
            this.start = start;
            this.end = end;
        }
    
        @Override
        public Integer compute() {
            int sum = 0;
            if ((end - start) < THRESHOLD) {    // 当问题分解到可求解程度时直接计算结果
                for (int i = start; i <= end; i++) {
                    sum += i;
                }
            } else {
                int middle = (start + end) >>> 1;
                // 将任务一分为二
                Calculator left = new Calculator(start, middle);
                Calculator right = new Calculator(middle + 1, end);
                left.fork();
                right.fork();
                // 注意:由于此处是递归式的任务分解,也就意味着接下来会二分为四,四分为八...
    
                sum = left.join() + right.join();   // 合并两个子任务的结果
            }
            return sum;
        }
    
    }
    
    public class Test08 {
    
        public static void main(String[] args) throws Exception {
            ForkJoinPool forkJoinPool = new ForkJoinPool();
            Future<Integer> result = forkJoinPool.submit(new Calculator(1, 10000));
            System.out.println(result.get());
        }
    }

      伴随着Java 7的到来,Java中默认的数组排序算法已经不再是经典的快速排序(双枢轴快速排序)了,新的排序算法叫TimSort,它是归并排序和插入排序的混合体,TimSort可以通过分支合并框架充分利用现代处理器的多核特性,从而获得更好的性能(更短的排序时间)。

    参考文献

    1. Benjamin J. Evans, etc, The Well-Grounded Java Developer. Jul 21, 2012
    2. Robert Martin, Clean Code. Aug 11, 2008.
    3. Doug Lea, Concurrent Programming in Java: Design Principles and Patterns. 1999
    展开全文
  • Java并发编程技术大纲

    2017-12-10 21:24:53
    Java并发编程技术大纲:
  • 封面 内容简介 并发编程领域的扛鼎之作,作者是阿里和1号店的资深Java技术专家...它选取了Java并发编程中核心的技术进行讲解,从JDK源码、JVM、CPU等多角度全面剖析和讲解了Java并发编程的框架、工具、原理和方法,...
  • Java并发编程之美

    2018-10-28 20:01:02
    一、前言 并发编程相比 Java 中其他知识点学习门槛较高,从而导致很多人望而却步。但无论是职场面试,还是高并发/高流量的系统的...- 第一部分Java并发编程基础篇主要讲解Java并发编程的基础知识,主要讲解线程有关...
  • 思维导图如下:
  • 本人已有多年的java开发经验,但是至今仍对java并发编程相关一知半解,甚为羞愧,决定一边写博客一边学习,作为一个学习记录和分享。...Java并发编程学习(1)---线程基础:https://blog.csdn.net/u011294519/arti...
  • 最近在阅读《Java并发编程之美》这本书,为了督促自己啃完这本书,计划每读完一章写一篇阅读笔记,供以后参考 笔记列表 第一部分 Java并发编程基础篇 第1章 并发编程线程基础 第2章 并发编程的其他基础知识 第...
  • Java并发编程从入门到精通—PDF高清版 下载地址: 链接: https://pan.baidu.com/s/1IJ9eQ9knEdOtdz-Ju-enfQ 提取码: fesx
  • java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作,主要提供了以下功能: 1、通过Unsafe类可以分配内存,可以释放内存; 类中提供的3个本地方法allocateMemory、...
  • 并发编程相比 Java 中其他知识点学习起来门槛相对较高,学习起来比较费劲,从而导致很多人望而却步;而无论是职场面试和高并发高流量的系统的实现却都还离不开并发编程,从而导致能够真正掌握...
  • 浅谈Java并发编程

    2017-05-02 23:44:24
    以前接触并发编程的机会还是挺少的,但是进入了一个大平台,用户量很可观,所以在程序处理是并发的情况很常见,所以觉得好好了解这方面的知识很有必要,言归正传,接下来我将从一下几点浅谈Java并发编程学习总结:...
  • 借用 Java 并发编程实践中...并发编程相比 Java 中其他知识点学习起来门槛相对较高,学习起来比较费劲,从而导致很多人望而却步;而无论是职场面试和高并发高流量的系统的实现却都还离不开并发编程,从而导致能够真...
  • 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章    【Java并发编程】实现多线程的两种方法  【Java...
  • Java并发编程最佳实例详解系列: Java并发编程(一)线程定义、状态和属性 Java并发编程(一)线程定义、状态和属性 线程是指程序在执行过程中,能够执行程序代码的一个执行单元。在java语言中,线程有四种状态...
  • Java并发编程之美 、Java并发编程的艺术 、 实战Java高并发程序设计 这三本书哪本好一点 感觉都差不多 哪本适合找实习的大学生
  • 借用 Java 并发编程实践中的...并发编程相比 Java 中其他知识点学习起来门槛相对较高,学习起来比较费劲,从而导致很多人望而却步;而无论是职场面试和高并发高流量的系统的实现却都还离不开并发编程,从而导致能够...
  • 作者:知乎用户链接:...Java并发编程实战 (豆瓣) (java并发的圣经)多处理器编程的艺术 (豆瓣) (并发编程的各种算法,java实现,有点难度)并发的艺术 (豆瓣) (多核处理器的共享内存模型中的各种算法...
1 2 3 4 5 ... 20
收藏数 216,403
精华内容 86,561
关键字:

java 并发编程学习