精华内容
下载资源
问答
  • 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 并发】系列是 LZ 在 2017 年写的第一个死磕系列,一直没有做一个合集,这篇博客则是将整个系列做一个概览。

    先来一个总览图:

    chenssy_juc_201712

    【高清图,请关注“Java技术驿站”公众号,回复:脑图JUC】

    【死磕Java并发】—–深入分析synchronized 的实现原理

    synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。深入分析 synchronized 的内在实现机制,锁优化、锁升级过程。

    【死磕Java并发】—–深入分析volatile的实现原理

    volatile 可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在 JVM 底层 volatile 是采用“内存屏障”来实现的。这篇博文将带你分析 volatile 的本质

    【死磕Java并发】—–Java内存模型之happens-before

    happens-before 原则是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。

    定义如下:

    1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
    2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

    【死磕Java并发】—–Java内存模型之重排序

    在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

    • 在单线程环境下不能改变程序运行的结果;
    • 存在数据依赖关系的不允许重排序

    as-if-serial 语义保证在单线程环境下重排序后的执行结果不会改变。

    【死磕Java并发】—–Java内存模型之分析volatile

    volatile的内存语义是:

    • 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
    • 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量

    总是说 volatile 保证可见性,happens-before 是 JMM 实现可见性的基础理论,两者会碰撞怎样的火花?这篇博文给你答案。

    【死磕Java并发】—–Java内存模型之从JMM角度分析DCL

    DCL,即Double Check Lock,双重检查锁定。是实现单例模式比较好的方式,这篇博客告诉你 DCL 中为何要加 volatile 这个关键字。

    【死磕Java并发】—–J.U.C之AQS:AQS简介

    AQS,AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),为 JUC 并发包中的核心基础组件。

    【死磕Java并发】—–J.U.C之AQS:CLH同步队列

    前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

    【死磕Java并发】—–J.U.C之AQS:同步状态的获取与释放

    AQS的设计模式采用的模板方法模式,子类通过继承的方式,实现它的抽象方法来管理同步状态,对于子类而言它并没有太多的活要做,AQS提供了大量的模板方法来实现同步,主要是分为三类:独占式获取和释放同步状态、共享式获取和释放同步状态、查询同步队列中的等待线程情况。

    【死磕Java并发】—–J.U.C之AQS:阻塞和唤醒线程

    当需要阻塞或者唤醒一个线程的时候,AQS 都是使用 LockSupport 这个工具类来完成。

    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

    【死磕Java并发】—–J.U.C之重入锁:ReentrantLock

    一个可重入的互斥锁定 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁定相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用 isHeldByCurrentThread()getHoldCount() 方法来检查此情况是否发生。

    这篇博客带你理解 重入锁:ReentrantLock 内在本质。

    【死磕Java并发】—–J.U.C之读写锁:ReentrantReadWriteLock

    读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞。

    读写锁的主要特性:

    • 公平性:支持公平性和非公平性。
    • 重入性:支持重入。读写锁最多支持65535个递归写入锁和65535个递归读取锁。
    • 锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁

    【死磕Java并发】—–J.U.C之Condition

    在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、notify()系列方法可以实现等待/通知模式。在Java SE5后,Java提供了Lock接口,相对于Synchronized而言,Lock提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活

    【死磕Java并发】—-深入分析CAS

    CAS,Compare And Swap,即比较并交换。Doug lea大神在同步组件中大量使用 CAS 技术鬼斧神工地实现了Java 多线程的并发操作。整个 AQS 同步组件、Atomic 原子类操作等等都是以 CAS 实现的。可以说CAS是整个JUC的基石。

    【死磕Java并发】—–J.U.C之并发工具类:CyclicBarrier

    CyclicBarrier,一个同步辅助类。它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

    【死磕Java并发】—–J.U.C之并发工具类:CountDownLatch

    CountDownLatch 所描述的是”在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待“。

    用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。

    【死磕Java并发】—–J.U.C之并发工具类:Semaphore

    Semaphore,信号量,是一个控制访问多个共享资源的计数器。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。

    【死磕Java并发】—–J.U.C之并发工具类:Exchanger

    可以在对中对元素进行配对和交换的线程的同步点。每个线程将条目上的某个方法呈现给 exchange 方法,与伙伴线程进行匹配,并且在返回时接收其伙伴的对象。Exchanger 可能被视为 SynchronousQueue 的双向形式。Exchanger 可能在应用程序(比如遗传算法和管道设计)中很有用。

    【死磕Java并发】—–J.U.C之Java并发容器:ConcurrentHashMap

    ConcurrentHashMap 作为 Concurrent 一族,其有着高效地并发操作。在1.8 版本以前,ConcurrentHashMap 采用分段锁的概念,使锁更加细化,但是 1.8 已经改变了这种思路,而是利用 CAS + Synchronized 来保证并发更新的安全,当然底层采用数组+链表+红黑树的存储结构。这篇博客带你彻底理解 ConcurrentHashMap。

    【死磕Java并发】—–J.U.C之ConcurrentHashMap红黑树转换分析

    在 1.8 ConcurrentHashMap 的put操作中,如果发现链表结构中的元素超过了TREEIFY_THRESHOLD(默认为8),则会把链表转换为红黑树,已便于提高查询效率。那么具体的转换过程是怎么样的?这篇博客给你答案。

    【死磕Java并发】—–J.U.C之Java并发容器:ConcurrentLinkedQueue

    ConcurrentLinkedQueue是一个基于链接节点的无边界的线程安全队列,它采用FIFO原则对元素进行排序。采用“wait-free”算法(即CAS算法)来实现的。

    CoucurrentLinkedQueue规定了如下几个不变性:

    1. 在入队的最后一个元素的next为null
    2. 队列中所有未删除的节点的item都不能为null且都能从head节点遍历到
    3. 对于要删除的节点,不是直接将其设置为null,而是先将其item域设置为null(迭代器会跳过item为null的节点)
    4. 允许head和tail更新滞后。这是什么意思呢?意思就说是head、tail不总是指向第一个元素和最后一个元素(后面阐述)。

    【死磕Java并发】—–J.U.C之Java并发容器:ConcurrentSkipListMap

    我们在Java世界里看到了两种实现key-value的数据结构:Hash、TreeMap,这两种数据结构各自都有着优缺点。

    • Hash表:插入、查找最快,为O(1);如使用链表实现则可实现无锁;数据有序化需要显式的排序操作。
    • 红黑树:插入、查找为O(logn),但常数项较小;无锁实现的复杂性很高,一般需要加锁;数据天然有序。

    这里介绍第三种实现 key-value 的数据结构:SkipList。SkipList 有着不低于红黑树的效率,但是其原理和实现的复杂度要比红黑树简单多了。

    ConcurrentSkipListMap 其内部采用 SkipLis 数据结构实现。

    【死磕Java并发】—–J.U.C之阻塞队列:ArrayBlockingQueue

    ArrayBlockingQueue,一个由数组实现的有界阻塞队列。该队列采用FIFO的原则对元素进行排序添加的。

    ArrayBlockingQueue 为有界且固定,其大小在构造时由构造函数来决定,确认之后就不能再改变了。ArrayBlockingQueue 支持对等待的生产者线程和使用者线程进行排序的可选公平策略,但是在默认情况下不保证线程公平的访问,在构造时可以选择公平策略(fair = true)。公平性通常会降低吞吐量,但是减少了可变性和避免了“不平衡性”。

    【死磕Java并发】—–J.U.C之阻塞队列:PriorityBlockingQueue

    PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排序,当然我们也可以通过构造函数来指定Comparator来对元素进行排序。需要注意的是PriorityBlockingQueue不能保证同优先级元素的顺序。

    【死磕Java并发】—–J.U.C之阻塞队列:DelayQueue

    DelayQueue是一个支持延时获取元素的无界阻塞队列。里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行。也就是说只有在延迟期到时才能够从队列中取元素。

    DelayQueue主要用于两个方面:

    • 缓存:清掉缓存中超时的缓存数据
    • 任务超时处理

    【死磕Java并发】—–J.U.C之阻塞队列:SynchronousQueue

    SynchronousQueue与其他BlockingQueue有着不同特性:

    1. SynchronousQueue没有容量。与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
    2. 因为没有容量,所以对应 peek, contains, clear, isEmpty … 等方法其实是无效的。例如clear是不执行任何操作的,contains始终返回false,peek始终返回null。
    3. SynchronousQueue分为公平和非公平,默认情况下采用非公平性访问策略,当然也可以通过构造函数来设置为公平性访问策略(为true即可)。
    4. 若使用 TransferQueue, 则队列中永远会存在一个 dummy node(这点后面详细阐述)。

    SynchronousQueue非常适合做交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务。

    【死磕Java并发】—–J.U.C之阻塞队列:LinkedTransferQueue

    LinkedTransferQueue 是基于链表的 FIFO 无界阻塞队列,它出现在 JDK7 中。Doug Lea 大神说 LinkedTransferQueue 是一个聪明的队列。它是 ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、无界的LinkedBlockingQueues 等的超集。

    【死磕Java并发】—–J.U.C之阻塞队列:LinkedBlockingDeque

    LinkedBlockingDeque 是一个由链表组成的双向阻塞队列,双向队列就意味着可以从对头、对尾两端插入和移除元素,同样意味着 LinkedBlockingDeque 支持 FIFO、FILO 两种操作方式。

    LinkedBlockingDeque 是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为 Integer.MAX_VALUE。

    【死磕Java并发】—–深入分析ThreadLocal

    ThreadLocal 提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

    所以ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以说ThreadLocal为多线程环境下变量问题提供了另外一种解决思路。

    【死磕Java并发】—–J.U.C之线程池:ThreadPoolExecutor

    鼎鼎大名的线程池。不需要多说!!!

    这篇博客深入分析 Java 中线程池的实现。

    【死磕Java并发】—–J.U.C之线程池:ScheduledThreadPoolExecutor

    ScheduledThreadPoolExecutor 是实现线程的周期、延迟调度的。

    ScheduledThreadPoolExecutor,继承 ThreadPoolExecutor 且实现了 ScheduledExecutorService 接口,它就相当于提供了“延迟”和“周期执行”功能的 ThreadPoolExecutor。在JDK API中是这样定义它的:ThreadPoolExecutor,它可另行安排在给定的延迟后运行命令,或者定期执行命令。需要多个辅助线程时,或者要求 ThreadPoolExecutor 具有额外的灵活性或功能时,此类要优于 Timer。 一旦启用已延迟的任务就执行它,但是有关何时启用,启用后何时执行则没有任何实时保证。按照提交的先进先出 (FIFO) 顺序来启用那些被安排在同一执行时间的任务。


    欢迎扫一扫我的公众号关注 — 及时得到博客订阅哦!

    这里写图片描述

    展开全文
  • Java并发编程基础与实战
  • [超级链接:Java并发学习系列-绪论] 本章主要对Java并发(Concurrent)在不同jdk版本中的发展简史进行学习。 Java语言从第一版本至今,内置了对并发(Concurrent)的各种支持技术。 为了能够让我们在学习Java并发...

    [超级链接:Java并发学习系列-绪论]

    本章主要对Java并发(Concurrent)在不同jdk版本中的发展简史进行学习。

    Java语言从第一版本至今,内置了对并发(Concurrent)的各种支持技术。

    为了能够让我们在学习Java并发(Concurrent)时,不被各种各样的并发技术弄得晕头转向,本章先对Java个版本中的主要并发技术进行简述。

    注:由于时间有限,如果哪里有误,请指出,谢谢!

    1.JDK1.4及之前

    在JDK1.4及之前的版本,主要提供的并发技术有:

    • synchronized关键字
    • volatile关键字
    • 不变模式

    1.1.volatile关键字

    引用百度百科的解释:

    volatile是一个类型修饰符(type specifier),就像大家更熟悉的const一样,它是被设计用来修饰被不同线程访问和修改的变量。volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

    注:百度百科的解释并不是专门针对java语音中的volatile,而是C语音,不过其设计思想类型,可以拿来借鉴。

    更通俗易懂的说法,在Java语音中,使用volatile关键字的目的:

    • 标识这个变量是易变型变量
    • 保证这个变量的可见性
    • 保证一定的有序性

    volatile关键字还涉及到JMM(Java内存模型,Java Memory Model)以及并发的三个特性(原子性、有序性和可见性),关于volatile关键字的更多内容,计划在后续章节中进行更加详细的学习。

    1.2.synchronized关键字

    引用百度百科的一段解释:

    synchronized 关键字,代表这个方法(或代码块)加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法synchronized 代码块

    简单来说,synchronized关键字以同步方法和同步代码块的方式,方法和代码块上的对象加锁使得同一时刻,在这个对象上的多个线程,只能由持有这个对象锁的单个线程进行代码的调用执行。

    synchronized 关键字能够保证代码的原子性、可见性和有序性

    关于synchronized关键字的更多内容,计划在后续章节中进行更加详细的学习。

    1.3.不变模式

    所谓不变模式,就是指:在并发编程中,为确保数据的一致性和正确性,使用一种不可改变的对象。依靠其不可变的性质,来确保在没有同步的情况下依旧保持一致性和正确性。

    Java中不变模式相关技术有:

    • final关键字
    • String类型

    关于不变模式,就简单进行这些介绍。如果感兴趣,可以自行学习。

    2.JDK5

    众所周知,JDK5是Java发展的一个重要版本,提供了很多技术,如泛型 Generic枚举类型 Enumeration可变参数varargs注解 Annotations等等。

    在JDK1.5版本中,也提供了对并发编程极为重要的一个包:java.util.concurrent(并发包)

    java.util.concurrent(并发包)提供了一些列较为给力的并发技术,主要有:

    • 原子(Atomic)类型:如AtomicInteger、AtomicReference等,保证变量的原子性和可见性
    • 显式锁(Lock)接口:对之前版本锁机制的重构,相较于synchronized 关键字,能够提供更加灵活的特性,如:能够指定锁定公平性、可以实现分组唤醒(Condition)、性能更好的锁。主要包括:Lock接口、ReadWriteLock接口和Condition接口
    • 计数器(CountDownLatch):利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行。
    • 回环栅栏(CyclicBarrier):通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
    • 信号量(Semaphore):Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
    • 并发集合:即集合类在并发环境下的版本。主要有:BlockingQueue(Queue)、ConcurrentMap(Map)、ConcurrentHashMap(HashMap)、CopyOnWriteArrayList(ArrayList)。
    • Callable和Future接口:为了解决继承Thread类实现Runnable接口存在的弊端(不允许声明检查型异常,不能定义返回值),而引入的线程的新的定义方式。
    • 执行器(Executor接口):Executors相关类隐藏了如何处理Runnable的细节,提供了一组方法,能够创建拥有完善配置的线程池和executor。

    关于原子(Atomic)类型、显式锁(Lock)接口、并发集合、Callable和Future接口、执行器(Executor接口)的更多内容,计划在后续章节中进行更加详细的学习。

    3.JDK7

    在JDK1.7版本中,主要提供的并发编程技术有:

    • TransferQueue:比BlockingQueue性能更好的并发集合实现。
    • 分支合并(Fork/Join)框架:运用分治法(divide-and-conquer)的思想,实现线程池中任务的自动调度,并且这种调度对用户来说是透明的,典型应用ForkJoinPool。

    关于分支合并(Fork/Join)框架的更多内容,计划在后续章节中进行更加详细的学习。

    4.JDK8

    在JDK.18版本中,主要提供的并发编程技术有:

    • 加法器(Adder)和累加器(Accumulator):原子类型的扩充与优化,主要有:LongAdder、LongAccumulator、DoubleAdder和DoubleAccumulator,比AtomicLong和AtomicDouble性能更优。
    • CompletableFuture:JDK5中Future的增强版。
    • StampedLock:JDK5中ReadWriteLock的改进版。

    参考文献

    [1] 关于Java并发编程的总结和思考
    [2] Java并发编程:CountDownLatch、CyclicBarrier和 Semaphore
    [3] Callable接口、Runable接口、Future接口
    [4] Java并发编程:CountDownLatch、CyclicBarrier和 Semaphore
    [5] 高并发Java(10):JDK8对并发的新支持

    展开全文
  • java并发编程面试题

    万次阅读 2020-09-28 23:25:46
    4、JVM对Java的原生锁做了哪些优化?5、为什么说Synchronized是非公平锁?6、什么是锁消除和锁粗化?7、为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么特性?8、乐观锁一定就是好...

    文章目录

    1、Synchronized用过吗,其原理是什么?

    Synchronized是由JVM实现的一种实现互斥同步的一种方式,如果你查看被Synchronized修饰过 的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块,在编译前后被编译器生成了 monitorenter和monitorexit两个字节码指令。

    在虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+ 1 ;当执行monitorexit指令时将锁计数器-1 ;当计数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。Java中Synchronize通过在对象头设置标记,达到了获取锁和释放锁的目的。

    2、你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的 锁?

    "锁"的本质其实是monitorenter和monitorexit字节码指令的一个Reference类型的参数,即要锁定和解锁的对象。我们知道,使用Synchronized可以修饰不同的对象,因此,对应的对象锁可以这么确定。

    1. 如果Synchronized明确指定了锁对象,比如Synchronized(变量名)、Synchronized(this)等,说明加解锁对象为该对象。

    2. 若Synchronized修饰的方法为非静态方法,表示此方法对应的对象为锁对象;若Synchronized修饰的方法为静态方法,则表示此方法对应的类对象为锁对象。注意,当一个对象被锁住时,对象里面所有用Synchronized修饰的方法都将产生堵塞,而对象里非Synchronized修饰的方法可正常被调用,不受锁影响。

    3、什么是可重入性,为什么说Synchronized是可重入锁?

    可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。例如一个类中的同步方法调用另一个同步方法,假如Synchronized不支持重入,进入method2方法时当前线程获得锁,method2方法里面执行method1时当前线程又要去尝试获取锁,这时如果不支持重入,它就要等释放,把自己阻塞,导致自己锁死自己。 对Synchronized来说,可重入性是显而易见的,刚才提到,在执行monitorenter指令时,如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器+1, 其实本质上就通过这种方式实现了可重入性。

    4、JVM对Java的原生锁做了哪些优化?

    在Java 6之前,Monitor的实现完全依赖底层操作系统的互斥锁来实现, 也就是我们刚才在问题二中所阐述的获取/释放锁的逻辑。

    由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个 线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内 核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做 了大量的优化。一种优化是使用自旋锁,即在把线程进行阻塞操作之前先 让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无 需再让线程执行阻塞操作,避免了用户态到内核态的切换。

    现代JDK中还提供了三种不同的Monitor实现,也就是三种不同的锁:

    1. 偏向锁(Biased Locking)
    2. 轻量级锁
    3. 重量级锁

    这三种锁使得JDK得以优化Synchronized的运行,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。

    1. 当没有竞争出现时,默认会使用偏向锁。

    JVM会利用CAS操作,在对象头上的Mark Word部分设置线程ID,以表 示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应 用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可 以降低无竞争开销。

    1. 如果有另一线程试图锁定某个被偏斜过的对象,JVM就撤销偏斜锁,切 换到轻量级锁实现。

    2. 轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

    5、为什么说Synchronized是非公平锁?

    非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待 线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样 做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。

    6、什么是锁消除和锁粗化?

    锁消除

    指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检 测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。
    程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程 序员自己加入的。

    锁粗化

    原则上,同步块的作用范围要尽量小。但是如果一系列的连续操 作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进 行互斥同步操作也会导致不必要的性能损耗。

    锁粗化就是增大锁的作用域。

    7、为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么特性?

    Synchronized显然是一个悲观锁,因为它的并发策略是悲观的:不管是否 会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁 计数器和检查是否有被阻塞的线程需要被唤醒等操作。

    随着硬件指令集的 发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没 有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲 突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要 线程挂起,所以被称为非阻塞同步。

    乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预 期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。 这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样, 如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否 则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。 CAS具有原子性,它的原子性由CPU硬件指令实现保证,即使用JNI调 用Native方法调用由C++编写的硬件级别指令,JDK中提供了 Unsafe 类执行这些操作。

    8、乐观锁一定就是好的吗?

    乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:

    1. 乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐 观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗 粒度大小。

    2. 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。

    3. ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判 断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后 来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发 生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景 的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。

    9、跟Synchronized相比,可重入锁ReentrantLock其实现原理有什 么不同?

    锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某种标记。

    Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原 生的锁实现方式,而ReentrantLock以及所有的基于Lock接口的实现 类,都是通过用一个volitile修饰的int型变量,并保证每个线程都能拥有 对该int的可见性和原子修改,其本质是基于所谓的AQS框架。

    10、那么请谈谈AQS框架是怎么回事儿?

    AQS(AbstractQueuedSynchronizer类)是一个用来构建锁和同步器的框架, 各种Lock包中的锁(常用的有ReentrantLock、ReadWriteLock),以及其他如 Semaphorex CountDownLatch,甚至是早期的 FutureTask 等,都是基于AQS来构建。

    1. AQS在内部定义了一个volatile int state变量,表示同步状态:当线程调 用lock方法时,如果state=O,说明没有任何线程占有共享资源的 锁,可以获得锁并将state=1;如果state=1,则说明有线程目前正在使 用共享变量,其他线程必须加入同步队列进行等待。

    2. AQS通过Node内部类构成的一个双向链表结构的同步队列,来完成线 程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。

      1. Node类是对要访问同步代码的线程的封装,包含了线程本身及其状 态叫waitStatus(有五种不同取值,分别表駅是否被阻塞,是否等待唤醒,是 否已经被取消等),每个Node结点关联其prev结点和next结点,方便线程释放锁后快速唤醒下一个在等待的线程,是一个FIFO的过程。
      2. Node类有两个常量,SHARED和EXCLUSIVE,分别代表共享模式和 独占模式。所谓共享模式是一个锁允许多条线程同时操作(信号量 Semaphore就是基于AQS的共享模式实现的),独占模式是同一个时 间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等 待(如 ReentranLock)。
    3. AQS通过内部类ConditionObject构建等待队列(可有多个),当Condition调用wait。方法后,线程将会加入等待队列中,而当 Condition调用signal。方法后,线程将从等待队列转移动同步队列中进 行锁竞争。

    4. AQS和Condition各自维护了不同的队列,在使用Lock和Condition 的时候,其实就是两个队列的互相移动。

    11、请尽可能详尽地对比下Synchronized和ReentrantLock的异同。

    ReentrantLock是Lock的实现类,是一个互斥的同步锁。从功能角度, ReentrantLock比Synchronized的同步操作更精细(因为可以像普通对象一 样使用),甚至实现Synchronized没有的高级功能,如:

    1. 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以 选择放弃等待,对处理执行时间非常长的同步块很有用。
    2. 带超时的获取锁尝试:在指定的时间范围内获取锁,如果时间到了仍然无 法获取则返回。
    3. 可以判断是否有线程在排队等待获取锁。
    4. 可以响应中断请求:与Synchronized不同,当获取到锁的线程被中断
      时,能够响应中断,中断异常将会被抛出,同时锁会被释放。
    5. 可以实现公平锁。

    从锁释放角度,Synchronized在JVM层面上实现的,不但可以通过一些 监控工具监控Synchronized的锁定,而且在代码执行出现异常时,JVM 会自动释放锁定;但是使用Lock则不行,Lock是通过代码实现的,要保证 锁定一定会被释放,就必须将unLock。放到finally。中。

    从性能角度,Synchronized早期实现比较低效,对比ReentrantLock,大多数场景性能都相差较大。
    但是在Java 6中对其进行了非常多的改进,在竞争不激烈时,Synchronized的性能要优于ReetrantLock;在高竞争情况下, Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常 态。

    12、ReentrantLock是如何实现可重入性的?

    ReentrantLock内部自定义了同步器Sync(Sync既实现了 AQS,又实现了 AOS,而AOS提供了一种互斥锁持有的方式),其实就是加锁的时候通过 CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下 当前维护的那个线程ID和当前请求的线程ID是否一样,一样就可重入了。

    13、除了 ReetrantLock,你还接触过JUC中的哪些并发工具?

    通常所说的并发包(JUC)也就是java.util.concurrent及其子包,集中了Java并发的各种基础工具类,具体主要包括几个方面:

    1. 提供了 CountDownLatch、CyclicBarrier、Semaphore等,比 Synchronized更加高级,可以实现更加丰富多线程操作的同步结构。

    2. 提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通 过类似快照机制实现线程安全的动态数组CopyOnWriteArrayList等各种线
      程安全的容器。

    3. 提供了 ArrayBlockingQueue、SynchorousQueue 或针对特定场景的 PriorityBlockingQueue等,各种并发队列实现。

    4. 强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行等。

    14、请谈谈 ReadWriteLock 和 StampedLock。

    虽然ReentrantLock和Synchronized简单实用,但是行为上有一定局限 性,要么不占,要么独占。实际应用场景中,有时候不需要大量竞争的写 操作,而是以并发读取为主,为了进一步优化并发操作的粒度,Java提 供了读写锁。读写锁基于的原理是多个读操作不需要互斥,如果读锁试图 锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作 结束,这样就可以自动保证不会读取到有争议的数据。

    ReadWriteLock代表了一对锁,下面是一个基于读写锁实现的数据结构, 当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出优势。

    读写锁看起来比Synchronized的粒度似乎细一些,但在实际应用中,其 表现也并不尽如人意,主要还是因为相对比较大的开销。所以,JDK在后期引入了 StampedLock,在提供类似读写锁的同时,还支持优化读模式。 优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先 试着修改,然后通过validate方法确认是否进入了写模式,如果没有进 入,就成功避免了开销;如果进入,则尝试获取读锁。

    15、如何让Java的线程彼此同步?你了解过哪些同步器?

    JUC中的同步器三个主要的成员:CountDownLatchCyclicBarrierSemaphore,通过它们可以方便地实现很多线程之间协作的功能。

    CountDownLatch叫倒计数,允许一个或多个线程等待某些操作完成。

    CyclicBarrier叫循环栅栏,它实现让一组线程等待至某个状态之后再全部 同时执行,而且当所有等待线程被释放后,CyclicBarrier可以被重复使 用。CyclicBarrier的典型应用场景是用来等待并发线程结束。

    CyclicBarrier 的主要方法是await(),await()每被调用一次,计数便会减少1,并阻塞住 当前线程。当计数减至0时,阻塞解除,所有在此CyclicBarrier上面阻塞的线程开始运行。
    在这之后,如果再次调用await。,计数就又会变成N-1,新一轮重新开 始,这便是Cyclic的含义所在。CyclicBarrier.await。带有返回值,用来表 示当前线程是第几个到达这个Barrier的线程。

    Semaphore, Java版本的信号量实现,用于控制同时访问的线程个数,来 达到限制通用资源访问的目的,其原理是通过acquire。获取一个许可,如 果没有就等待,而release。释放一个许可。

    如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire 进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比 如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类 似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。

    16、CyclicBarrier和CountDownLatch看起来很相似,请对比下呢?

    它们的行为有一定相似度,区别主要在于:

    1. CountDownLatch是不可以重置的,所以无法重用,CyclicBarrier没有这种限制,可以重用。

    2. CountDownLatch的基本操作组合是countDown/await,调用await的线 程阻塞等待countDown足够的次数,不管你是在一个线程还是多个线程 里countDown,只要次数足够即可。CyclicBarrier的基本操作组合就是 await,当所有的伙伴都调用了 await,才会继续进行任务,并自动进行重 置。

    CountDownLatch目的是让一个线程等待其他N个线程达到某个条件后, 自己再去做某个事(通过CyclicBarrier的第二个构造方法public CyclicBarrier(int parties, Runnable barrierAction),在新线程里做事可以达到同样的效果)。而CyclicBarrier的目的是让N多线程互相等待直到所有 的都达到某个状态,然后这N个线程再继续执行各自后续(通过CountDownLatch在某些场合也能完成类似的效果)。

    17、Java中的线程池是如何实现的?

    在Java中,所谓的线程池中的“线程",其实是被抽象为了一个静态内部 类Worker,它基于AQS实现,存放在线程池的HashSet workers成员变量中;

    而需要执行的任务则存放在成员变量workQueue(BlockingQueue workQueue)中。
    这样,整个线程池实现的基本思想就是:从workQueue中不断取出需要执 行的任务,放在Workers中进行处理。

    18、创建线程池的几个核心构造参数?

    Java中的线程池的创建其实非常灵活,我们可以通过配置不同的参数,创建出行为不同的线程池,这几个参数包括:

    1. corePoolSize:线程池的核心线程数。
    2. maximumPoolSize:线程池允许的最大线程数。
    3. keepAliveTime:超过核心线程数时闲置线程的存活时间。
    4. workQueue :任务执行前保存任务的队列,保存由execute方法提交的 Runnable 任务。
    19、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?

    不是。线程池默认初始化后不启动Worker,等待有请求时才启动。

    每当我们调用execute()方法添加一个任务时,线程池会做如下判断:

    1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这 个任务

    2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放 入队列;

    3. 如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务

    4. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。当一个线程完成任务时,它会从队列中取下一个任务来执行。当一个线程 无事可做,超过一定的时间(keepAliveTime )时,线程池会判断。

    如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

    20、既然提到可以通过配置不同参数创建出不同的线程池,那么Java中默认实现好的线程池又有哪些呢?请比较它们的异同。
    1. SingleThreadExecutor 线程池

      这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有 任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代 它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

      1. corePoolSize:'只有一个核心线程在工作。
      2. maximumPoolSize: 1。
      3. keepAliveTime: 0L。
      4. workQueue:new LinkedBlockingQueue(),其缓冲队列 是无界的。
    2. FixedThreadPool 线程池

      FixedThreadPool是固定大小的线程池,只有核心线程。每次提交一个任 务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦 达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程 池会补充一个新线程。
      FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服
      务器。

      1. corePoolSize: nThreads
      2. maximumPoolSize: nThreads
      3. keepAliveTime: 0L
      4. workQueue:new LinkedBlockingQueue(),其缓冲队列 是无界的。

    3.CachedThreadPool线程池

    CachedThreadPool是无界线程池,如果线程池的大小超过了处理任务所 需要的线程,那么就会回收部分空闲(60秒不执行任务)线程,当任务数增 加时,此线程池又可以智能的添加新线程来处理任务。线程池大小完全依 赖于操作系统(或者说JVM)能够创建的最大线程大小。SynchronousQueue 是一个是缓冲区为1的阻塞队列。缓存型池子通常用于执行一些生存期很 短的异步型任务,因此在一些面向连接的daemon型SERVER中用得不 多。但对于生存期短的异步任务,它是Executor的首选。
    1. corePoolSize: 0
    1. maximumPoolSize: Integer.MAX_VALUE
    1. keepAliveTime: 60L
    1. workQueue:new SynchronousQueue<Runnable>(), —个是缓冲区为 1的阻塞队列。
    
    1. ScheduledThreadPool 线程池

      ScheduledThreadPool :核心线程池固定,大小无限的线程池。此线程池 支持定时以及周期性执行任务的需求。创建一个周期性执行任务的线程 池。如果闲置,非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内 回收。

      1. corePoolSize: corePoolSize
      2. maximumPoolSize: Integer.MAX_VALUE
      3. keepAliveTime: DEFAULT_KEEPALIVE_MILLIS
      4. workQueue:new DelayedWorkQueue()
    21、如何在Java线程池中提交线程?

    线程池最常用的提交任务的方法有两种:

    1. execute。: ExecutorService.execute 方法接收一个例,它用来执行一个 任务:
      utorService.execut«(Runnable runable)

    2. submit。: ExecutorService.submit。方法返回的是 Future 对象。可以用 isDone()来查询Future是否已经完成,当任务完成时,它具有一个结果, 可以调用get。来获取结果。也可以不用isDone。进行检查就直接调用 get(),在这种情况下,get()将阻塞,直至结果准备就绪。

    22、什么是Java的内存模型,Java中各个线程是怎么彼此看到对方的变量的?

    Java的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变 量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字 段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数, 因为这些是线程私有的,不会被共享,所以不存在竞争问题。
    Java中各个线程是怎么彼此看到对方的变量的呢?Java中定义了主内存与 工作内存的概念:
    所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该 线程使用到的变量的主内存副本拷贝。

    线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接 读写主内存的变量。不同的线程之间也无法直接访问对方工作内存的变 量,线程间变量值的传递需要通过主内存。

    23、请谈谈volatile有什么特点,为什么它能保证变量对所有线程的可见性?

    关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被 定义成volatile之后,具备两种特性:

    1. 保证此变量对所有线程的可见性。当一条线程修改了这个变量的值,新 值对于其他线程是可以立即得知的。而普通变量做不到这一点。

    2. 禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程中,得到 正确结果,但是不保证程序代码的执行顺序。

    Java的内存模型定义了8种内存间操作:

    lock 和 unlock

    1. 把一个变量标识为一条线程独占的状态。
    2. 把一个处于锁定状态的变量释放出来,释放之后的变量才能被其他线程 锁定。

    read 和 write

    1. 把一个变量值从主内存传输到线程的工作内存,以便load。
    2. 把store操作从工作内存得到的变量的值,放入主内存的变量中。

    load 和 store

    把read操作从主内存得到的变量值放入工作内存的变量副本中。•把工 作内存的变量值传送到主内存,以便write。

    use 和 assgin

    1. 把工作内存变量值传递给执行引擎。
    2. 将执行引擎值传递给工作内存变量值。

    volatile的实现基于这8种内存间操作,保证了一个线程对某个volatile变量的修改,一定会被另一个线程看见,即保证了可见性。

    24、既然volatile能够保证线程间的变量可见性,是不是就意味着基于volatile变量的运算就是并发安全的?

    不是。基于volatile变量的运算在并发下不一定是安全的。volatile 变量在各个线程的工作内存,不存在一致性问题(各个线程的工作内存中 volatile变量,每次使用前都要刷新到主内存)。

    但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一 样是不安全的。

    25、请对比下volatile对比Synchronized的异同。

    Synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
    ThreadLoca l和Synchonized都用于解决多线程并发访问,防止任务在共 享资源上产生冲突。但是ThreadLocal与Synchronized有本质的区别。

    Synchronized用于实现同步机制,是利用锁的机制使变量或代码块在某一 时该只能被一个线程访问,是一种“以时间换空间’'的方式。

    而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一 时间访问到的并不是同一个对象,根除了对变量的共享,是一种“以空间 换时间”的方式。

    26、请谈谈ThreadLocal是怎么解决并发安全的?

    ThreadLocal这是Java提供的一种保存线程私有信息的机制,因为其在 整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模 块之间传递信息,比如事务ID、Cookie等上下文相关信息。

    ThreadLocal为每一个线程维护变量的副本,把共享数据的可见范围限制 在同一个线程之内,其实现原理是,在ThreadLocal类中有一个Map,用 于存储每一个线程的变量的副本。

    27、很多人都说要慎用ThreadLocal,谈谈你的理解,使用ThreadLocal需要注意些什么?

    使用 ThreadLocal 要注意 remove!

    ThreadLocal的实现是基于一个所谓的ThreadLocalMap,在 ThreadLocalMap中,它的key是一个弱引用。
    通常弱引用都会和引用队列配合清理机制使用,但是ThreadLocal是个例外,它并没有这么做。
    这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束, 进而回收相应ThreadLocalMap!这就是很多00M的来源,所以通常都会建议,应用一定要自己负责remove,并且不要和线程池配合,因为 worker线程往往是不会退出的。

    展开全文
  • 本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star!【Java学习 面试指南】 一份涵盖大部分Java程序员所...Java 并发基础常见面试题总结 1. 什么是线程和进程? 1.1. 何为进程? 进程是程...
  • java并发实战

    千次阅读 2019-03-03 00:34:35
    推荐一个Java并发编程实战的学习专栏。此专栏为极客时间收费专栏。 学习交流加 个人qq: 1126137994 个人微信: liu1126137994 学习交流资源分享qq群: 962535112 对于一个 Java 程序员而言, 能否熟练掌握...
  • Java并发编程精讲

    万人学习 2019-09-28 15:16:34
    课程会讲解Java并发相关技术的基础、原理和应用,从线程安全、线程(池), 锁实现和并发容器等高并发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并发编程系列

    千次阅读 2021-01-26 16:37:53
    Java并发基础 1.多线程基础 2.线程安全基础 Java并发编程 1.Lock锁与生产者消费者问题 2.八锁问题带你彻底理解对象锁和类锁 3.集合类的线程安全问题
  • Java 并发源码合集

    2018-02-19 00:00:00
    【死磕Java并发】—– 深入分析synchronized的实现原理【死磕Java并发】—– 深入分析volatile的实现原理【死磕Java并发】—– Java内存模型之happens-before【死磕Java并发】—– Java内存模型之重排序【死磕Java...
  • Java 并发编程的艺术

    千次阅读 2019-01-11 23:30:52
    书中采用循序渐进的讲解方式,从并发编程的底层实现机制入手,逐步介绍了在设计 Java 并发程序时各种重要的技术、设计模式与应用,同时辅以丰富的示例代码,使得开发人员能够更快地领悟 Java 并发编程的要领,围绕着...
  • Java并发编程】实现多线程的两种方法 【Java并发编程】线程的中断 【Java并发编程】正确挂起、恢复、终止线程 【Java并发编程】守护线程和线程阻塞 【Java并发编程】Volatile关键字(上) 【Java并发编程】...
  • 根据《Java并发编程实践》一书
  • Java并发技术学习总结

    千次阅读 2018-06-28 14:40:06
    Java并发 这篇总结主要是基于我Java并发技术系列的文章而形成的的。主要是把重要的知识点用自己的话说了一遍,可能会有一些错误,还望见谅和指点。谢谢 更多详细内容可以查看我的专栏文章:Java并发技术指南 ...
  • [Java 并发] Java并发编程实践 思维导图 - 第四章 对象的组合。 整理的思维导图,希望对大家有所帮助。
  • Java并发知识点汇总

    万次阅读 2017-09-22 11:22:34
    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并发脑图高清版
  • Java并发API案例分析之并发设计原理

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

    万次阅读 多人点赞 2016-07-18 20:04:21
    Java并发机制及锁实现原理
  • Java并发编程完整总结

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

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

    千次阅读 2019-11-25 21:57:37
    java多线程是JAVA高阶编程最需掌握的...1、java并发锁ReentrantLock源码分析一 可重入支持中断锁的实现原理 2、java并发锁机制-ReentrantLock Condtion准备篇之Object.wait,Object.notify与Condtion 3、java并发锁...
  • 根据《Java并发编程实践》一书整理的思维导图。
  • Java 并发编程 1、在 java 中守护线程和本地线程区别? 2、线程与进程的区别? 3、什么是多线程中的上下文切换? 4、死锁与活锁的区别,死锁与饥饿的区别? 5、Java 中用到的线程调度算法是什么? 6、什么是...
  • 根据《Java并发编程实践》一书整理的思维导图。
  • Java 并发编程面试题

    千次阅读 2019-12-28 10:56:17
    Java 并发编程专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 并发编程 吊打自己 不推荐什么吊打面试官,我们先吊打自己,争取打败自己。 本篇内容不涉及答案,答案需要去 《Java 并发编程》 专栏学习...
  • 根据《Java并发编程实践》一书整理的思维导图。希望能够有所帮助。 第一部分: 第二部分: 第三部分:

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 140,509
精华内容 56,203
热门标签
关键字:

java并发

java 订阅