Java并发编程及并发包中类分析

一、并发编程线程基础:

        1.什么是线程:  

        线程是进程中的一个实体,线程本身是不会独立存在的进程是代码在数据集合上的一 次运行活动 , 是系统进行资源分配和调度的基本单位 线程则是进程的 执行路径, 一 个进程中至少有一个 线程,进程中的多个 线程共享进程资源。
        
        每个线程都有自己 的栈资源,用于存储线程的局部变量 ,这些局部变 是该线 程私有的,其它线程是访问不了的,除此之外枝还用来存放线程的调用栈帧。
         堆是一 个进程中最大的一 块内存,堆是被进程中的所有线程共享的,是进程创建时分 配的,堆里面主要存放使用 new 操作创 建的对象实例。
        方法区则用来存放 NM 加载的类、常量 及静态变 量等 信息,也是线程共享的。

        2、线程创建、运行:

        Java中有三种线程创建方式,分别为:

  •  实现 Runnable 接口的 run 方法;
  •  继承 thread,重写 run 方法;
  •  使用 FutureTask 方式;

        3、线程通知与等待:

  •  wait()函数:当一个线程调用一个共享变量的wait(0方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:1)其他线程调用了该共享对象的notify()或notifyAll()方法;2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

        另外需要注意的是,如果调用 wait() 方法线程没有事先获取该对象的监视器锁,则调用 wait() 方法时调用线程会抛出IllegalMonitorStateException 异常。

  •   获取共享变量的监视器锁:

        执行synchronized同步代码块时使用该共享变量作为参数:

    synchornized(共享变量) {
        //dosomething
    }

           调用该共享变量的方法,并且该方法使用了synchronized修饰:

    synchronized void add(int a,int b) {
        //dosomething
    }

        另外需要注意的是,一个线程可以从挂起状态变为可以运行状态( 就是被唤醒),即使该线程没有被其他线程调用 notify ()notifyall()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。

     //生成线程
    synchronized (queue) {
        while (queue.size() == MAX_SIZE) {
            try {
                //挂起当前线程,释放通过同步块获取的queue上的锁,
                // 让消费者线程可以获取该锁,然后获取队列里面的元素
                queue.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //空闲则生成元素,并通知消费者线程
        queue.add(ele);
        queue.notifyAll();
    }
    
    //消费线程
    synchronized (queue) {
        while (queue.size() == 0) {
            try {
                //挂起当前线程,释放通过同步块获取的queue上的锁,
                // 让生成者线程可以获取该锁,将生成元素放入队列
                queue.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //空闲则生成元素,并通知消费者线程
        queue.take();
        queue.notifyAll();
    }
  •  notify()函数:一个线程调用共享的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪一个等待的线程是随机的。

        此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可继续执行。

  •   notifyall()函数:不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上一个线程,notifyall()方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

        4、等待线程执行终止的join方法:

        在未完成join方法前调用interrupt()方法,该线程抛出InterruptedException异常返回。

        5、让线程睡眠的sleep方法:

       Thread 中有静态的sleep方法,当 一个执行中的线程调用了 Thread sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该程所拥有的监视器资源,比如锁还是持有不让出的,指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,获取到 CPU 资源后就可以继续运行了。如果在睡眠期间其线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用 sleep 方法的地方抛IntermptedException 异常而返回。

        6、让出CPU执行权的yield方法:

          Thread 有一个静态yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己CPU使用,但是线程调度器可以无条件忽略这个暗示。

        当一个线程调用yield 方法时, 当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。

        7、线程中断:

        Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

  •  void interrupt()方法:中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际上并没有被中断,它会继续往下执行。如线程A因为调用了wait系列函数、join方法或sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt方法,线程A会调用这些方法的地方抛出IntermptedException 异常而返回。
  • bool isInterrupted()方法:检测当前线程是否被中断,如是返回true,否则返回false。不清除中断标志。
  • bool interrupted()方法:检测当前线程是否被中断。与isInterrupted不同的是,该方法如发现当前线程被中断,则会清除中断标志。

        8、线程上下文切换:

        在线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。

        9、线程死锁:

        死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现场,在无外力作用情况下,这些线程会一直互相等待而无法继续运行下去。

        死锁的产生必须具备以下四个条件:

  • 互斥条件:指线程对已经获取到的资源进行排他性的使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源;
  • 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源;
  • 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
  • 环路等待条件:指在发生死锁时,必然存在一个线程资源的环形链,即线程集合{T0、T1、T2...Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,...Tn正在等待已被T0占用的资源。

        避免死锁,只需要破坏至少一个构造死锁的必要条件即可,目前只有请求并持有和环路等待条件是可以破坏的。造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁。

        10、守护线程与用户线程:

        Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。

        守护线程和用户线程区别:当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出。言外之意,只要有一个用户线程还没结束,正常情况下JVM就不会退出。

        如希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。

        11、ThreadLocal类:

        ThreadLocal是JDK提供的,它提供了线程本地变量,也就是如你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存。

        在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals中,如当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用ThreadLocal的remove方法删除线程的threadLocals中的本地变量。

  • ThreadLocal不可继承性:同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。
  • InheritableThreadLocal类:继承自ThreadLocal,其提供了一个特性,就是子线程可以访问父线程中设置的本地变量。
        

二、并发编程的其他基础:

        1、并发和并行概念:

  • 并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束;
  • 并行:是说在单位时间内多个任务同时在执行。

        并发任务强强在一个时间段内同时执行,而一个时间段由多个单位时间积累而成,所以说并发的多个任务在单位时间内不一定在执行。

        2、Java中的线程安全问题:

        共享资源是该资源被多个线程所持有或者说多个线程都可以去访问资源;

        线程安全问题时指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。

        3、Java中共享变量的内存可见性问题:

  •  多线程下处理共享变量时Java的内存模型:

        Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫做工作内存,线程读写变量时操作的是自己工作内存中的变量。

        

  •  实际中线程中的工作内存:

        如下图所示是一个双核CPU系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算执行算术逻辑运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。那么Java内存模型的工作内存,就对应在这里的L1或L2缓存或者CPU的寄存器。

        当一个线程操作共享变量时,它首先从内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完后将变量值更新到主内存。

        

  • 不可见性分析:

        假设如上图的CPU架构,线程A和线程B使用不同的CPU,并且当前两级Cache都为空,那么这时候由于Cache的存在,将会导致内存不可见问题,分析如下:

  1.  线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是1。
  2. 线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回 X=1;到这里一切都是正常的,因为这时候主内存中也是 X=1。然后线程 B修改X的值为2,并将其存放到线程2所在的一级Cache 和共享二级Cache中最后更新主内存中x的值为2;到这里一切都是好的。
  3. 线程A这次又需要修改X的值,获取时一级缓存命中,并且X=1,到这里问题就出现了,明明线程B已经把X的值修改为了2,为何线程A获取的还是1呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。

        4、Java中的synchronized关键字:

  •  synchronized关键字介绍:

        synchronized 块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait 系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。

        另外,由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized 的使用就会导致上下文切换。

  •    synchronized的内存语义:

          进入 synchronized 块的内存语义是把在synchronized 块内使用到的变量从线程的工作内存中清除,这样在synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized 块内对共享变量的修改刷新到主内存。

       5、Java中的volatile关键字:

         对于解决内存可见性问题,Java还提供了一种弱形式的同步,也就是使用 volatile 关键字。该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

        volatile使用场景:

  • 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取一计算一写入三步操作,这三步操作不是原子性的,而volatile 不保证原子性。     
  • 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为 volatile 的。

        6、 Java中的原子性操作:

          所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行不存在只执行其中一部分的情况。 

        atomic是不会阻塞线程(或者说只是在硬件级别上阻塞了),线程安全的加强版的volatile原子操作。java.util.concurrent.atomic 包里,多了一批原子处理类,主要用于在高并发环境下的高效程序处理。

  • 基本类:AtomicInteger、AtomicLong、AtomicBoolean.
  • 引用类型:AtomicReference、AtomicReference的ABA实例、AtomicStampedRerence、AtomicMarkableReference。
  • 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。
  • 属性原子修改器(Updater):AtomicIntegerFieldUpdater 、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater。

        7、Java中的CAS操作:

           在Java中,锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销。Java提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是 volatile只能保证共享变量的可见性,不能解决读一改一写等的原子性问题。CAS即Compare and Swap,其是JDK提供的非阻塞原子性操作,它通过硬件保证了比较一更新操作的原子性。

        8、Unsafe类:

           JDK 的 rt.jar 包中的 Unsafe 类提供了硬件级别的原子性操作,Unsafe 类中的方法都是native 方法,它们使用 JN 的方式访问本地 C++ 实现库。

        9、Java指令重排序:

        Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。

        重排序在多线程下会导致非预期的程序执行结果,而使用volatile修饰ready就可以避免重排序和内存可见性问题。

        写 volatile 变量时,可以确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。读 volatile 变量时,可以确保 volatile 读之后的操作不会被编译器重排序到 volatile读之前。

        10、伪共享:

        当CPU访问某个变量时,首先会去看CPUCache内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个Cache 行大小的内存复制到 Cache 中。由于存放到 Cache 行的是内存块而不是单个变量,所以可能会把多个变量存放到一个Cache 行中。当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。

        如何避免伪共享:在JDK8之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中。JDK8提供了一个sun.misc.Contended 注解,用来解决伪共享问题。

        11、锁的概念:

  • 乐观锁:是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。
  • 悲观锁:指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。
  • 公平锁与非公平锁:根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁而非公平锁则在运行时闯入,也就是先来不一定先得。
  • 独占锁与共享锁:根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock就是以独占方式实现的。共享锁则可以同时由多个线程持有,例如ReadWriteLock读写锁,它允许一个资源可以被多线程同时进行读操作。
  • 可重入锁:当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时是否会被阻塞呢?如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数地进入被该锁锁住的代码。
  • 自旋锁:当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是10,可以使用 -XX:PreBlockSpinsh 参数设置该值),很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。由此看来自旋锁是使用CPU时间换取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费。
  • 隐式锁:又称线程同步synchronized,当用来修饰一个方法或者一个代码块时,能够保证在同一时刻最多只有一个线程执行该段代码,
  • 显示锁Lock和ReentrantLock:Lock 是一个接口提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有加锁和解锁的方法都是显式的。包路径是:javauti.coneuentlocks.Lock。核心方法是lock()、unlock()、tryLock(),实现类有ReentrantLock、ReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock
  • 显示锁 ReadWriteLock和ReentrantReadWriteLock:ReadWriteLock也是一个接口,提供了readLock 和 writeLock 两种锁的操作机制,也就是一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。也就是说读写锁使用的场合是一个共享资源被大量读取操作,而只有少量的写操作。
  • 显示锁StampedLock:基于能力的锁,可以很好地实现悲观锁和乐观锁的逻辑。它使用三种模式来控制读/写访问。Stampedock 的状态包含了版本和模式。锁获取方法根据锁的状态返回一个表示和控制访问的标志(stamp)。

        Synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock 简单对比:

  • Sychronized 是在JVM 层面上实现的,可以通过一些监控工具监控 synchronized 的锁定,当代码执行时出现异常,JVM 会自动释放锁定。当只有少量竞争者的时候,synchronized是一个很好的通用的锁实现。Synchronized 的锁是针对一个对象的。
  • ReentrantLock、ReentrantReadWriteLock,、StampedLock 都是代码块层面的锁定,要保证锁定一定会被释放,就必须将unLockO放到finally{}中。
  • ReentrantLock是一个很好的通用的锁实现,使用于比较简单的加锁、解锁的业务逻辑,如果实现复杂的锁机制,当线程增长能够预估时也是可以的。
  • ReentrantReadWriteLock对Lock 又进行了扩展,引入了read 和 write 阻塞和并发机制,相对于 ReentrantLock,它可以实现更复杂的锁机制,且并发性也更高些。
  • StampedLock 又在Lock 的基础上,实现了可以满足乐观锁和悲观锁等一些在读线程越来越多的业务场景,对吞吐量有巨大的改进,但并不是说要替代之前的Lock,毕竟它还是有些应用场景的。
  • StampedLock有一个复杂的 API相对于前面两种Lock 锁,对于加锁操作,很容易误用其他方法,如果理解不深入也更容易出现死锁和不必要的麻烦。
  • 推荐如果不是业务非得需要,建议使用 ReentrantLock和ReentrantReadWriteLock 即可满足大部分业务场景的需要。

            12、单例模式线程安全及不安全的写法:

  • 线程不安全的,不正确的写法:
public class Singleton {
    private static Singleton mInstance;
    
    private Singleton() {
        
    }
    //创建时没同步锁,线程不安全
    public static Singleton getInstance() {
        if (mInstance == null) {
            mInstance = new Singleton();
        }
        return mInstance;
    }
}
  • 线程安全性,但是高并发性能不是很高的写法:
public class Singleton {
    private static Singleton mInstance;
    
    private Singleton() {
        
    }
    //synchronized同步方法创建过程,线程安全。但隐式锁创建过程性能不好
    public static synchronized Singleton getInstance() {
        if (mInstance == null) {
            mInstance = new Singleton();
        }
        return mInstance;
    }
}
  • 线程安全,性能又高的,这种写法最为常见:
public class Singleton {
    private static Singleton mInstance;
    private static byte[] lock = new byte[0];


    private Singleton() {

    }

    public static Singleton getInstance() {
        if (mInstance == null) {
            //synchronized同步创建的代码块,线程安全。隐式锁创建
            synchronized (lock) {
                //双非空判断mInstance 
                if (mInstance == null) {
                    mInstance = new Singleton();
                }
            }
        }
        return mInstance;
    }
}
  • 线程安全,性能又高的,这种写法也最为常见:
public class Singleton {
    private static Singleton mInstance;
    //创建一个可重入锁
    private static ReentrantLock lock = new ReentrantLock();


    private Singleton() {

    }

    public static Singleton getInstance() {
        if (mInstance == null) {
            //显示锁的形式,锁住创建过程。
            lock.lock();
            if (mInstance == null) {
                mInstance = new Singleton();
            }
            lock.unlock();
        }
        return mInstance;
    }
}

三、线程安全的集合类:

        1、Hashtable:

        Hashtable 和 HashMap 一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)
映射。Hashtable 继承于Dictionary,它实现了Map、Cloneable、java.io.Serializable 接口。Hashtable的函数都是用synchronized同步的。

        2、concurrentHashMap:

        ConcurrentHashMap继承于AbstractMap,实现了Map、java.io.Serializable 接口。这是 HashMap 的线程安全版,同 Hashtable相比,ConcurrentHashMap 不仅保证了访问的线程安全性,而且在效率上与Hashtable 相比,有较大的提高。ConcurrentHashMap 允许多个修改操作并发进行,其关键在于使用了锁分离技术,即代码块锁,而不是方法锁。它使用了多个锁来控制对 hash 表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是个小的 hash table,它们有自己的锁(由 ReentrantLock来实现的)。只要多个修改操作发生在不同的段上,它们就可以并发进行。

        3、CopyOnWriteArrayList:

        CopyOnWriteArrayList 中的 set、add、remove 等方法,都使用了ReentrantLock 的lockO来加锁,unlockO来解锁。当增加元素的时候使用Arrays.copyOf()来拷贝副本,在副本上增加元素,然后改变原引用指向副本。读操作不需要加锁,而写操作类实现中对其进行了加锁。因此,CopyOnWriteAmayList 类是一个线程安全的 List 接口的实现,这对于读操作远远多于写操作的应用非常适合。特别是在并发情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,不受多线程并发问题影响的。

        4、CopyOnWriteArraySet:

        CopyOnWriteArraySet 是在 CopyOnWriteArrayList 的基础上使用了 Java 的装饰模式,很多方法如:储存介质使用了 CopyOnWriteArrayList来存储数据,remove 方法调用 CopyOnWriteArrayList的remove方法,add方法调用了CopyOnWriteArrayList的addIfAbsent方法。所以CopyOnWriteArrayList 的实现原理适用CopyOnWriteArraySet。

        5、CopyOnWrite机制:

        CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite 容器进行并发地读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。

        6、Vector:

        Vector 是矢量队列,通过数组保存数据。它继承于AbstractList,实现了List、RandomAccess、Cloneable 这些接口。Vector 中的操作是线程安全的,它是利用 synchronized 同步显示锁的方法锁的机制实现,实现安全机制类似Hashtable。

        7、常用的StringBuffer与StringBuilder:

        StringBuffer是线程安全的,而StringBuilder不是线程安全的。

四、多线程之间交互:线程阈

        线程阈是线程与线程之间互相制约和交互的机制。

        1、阻塞队列(BlockingQueue):

         是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空;当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿取元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿取元素。

        2、数组阻塞队列 ArrayBlockingQueue:

        是一个由数组支持的有界的阻塞队列。此队列按FIFO(先进先出)原则对元素进行排序。队列的头部是在队列中存在时间最长的元素。队列的尾部是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。

        3、链表阻塞队列 LinkedBlockingQueue

        是基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回。只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒。反之,对于消费者这端的处理也基于同样的原理。而 LinkedBlockingQueue 之所以能够高效地处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

        4、优先级阻塞队列 PriorityBlockingQueue

        是一个支持优先级排序的无界阻塞队列(优先级的判断通过构造函数传入的 Compator 对象来决定),但需要注意的是 PriorityBlockingQueue 并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实PriorityBlockingQueue 时,内部控制线程同步的锁采用的是公平锁。

        5、ConcurrentLinkedQueue无界非阻塞队列

        其底层数据结构使用单向链表实现,对于入队和出队操作使用CAS来实现线程安全。

        6、延时队列 DelayQueue

       DelayQueue 是一个支持延时获取元素的使用优先级队列的实现的无界阻塞队列。队列中的元素必须实现Delayed接口和Comparable接口,也就是说DelayQueue 里面的元素必须有public int compareTo(To)和 long getDelay(TimeUnit unit)方法存在 ,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。 

        7、同步队列 SynchronousQueue

        是一个不存储元素的阻塞队列。每一个put 操作必须等待一个 take 操作,否则不能继续添加元素。SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的量高于LinkedBlockingQueue 和 ArrayBlockingQueue.

        8、链表双向阻塞队列 LinkedBlockingDeque

        LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque 多了 addFirst、addLast、offerFirst、oferLast、peekFirst、peekLast等方法,以First 单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另外插入方法 add等同于addLast,移除方法remove等效于removeFirst。

        9、链表传输队列 LinkedTransferQueue

        LinkedTransfcrQueue 是一个由链表结构组成的无界传输阻塞队列。TransferQueue是一个继承了BlockingQueue 的接口,并且增加若干新的方法。LinkedTransferQueue是实现类,其定义为一个无界的队列,一样具有先进先出(FIFO:first-in-first-out)的特性。

        10、同步计数器CountDownLatch

        CountDownLatch 是一个同步辅助类,直译过来就是倒计数(CountDown)门闩(Latch)。倒计数不用说,门闩的意思顾名思义就是阻止前进。在完成一组正在其他线程中执行的操作之前它允许一个或多个线程一直等待。用给定的计数初始化CountDownLatch。由于调用了countDown()方法,所以在当前计数到达零之前,await方法会一直受阻塞。之后,会释放所有等待的线程,await的所有后续调用都将立即返回。这种现象只出现一次--计数无法被重置。

        11、抽象队列化同步器 AbstractQueuedSynchronizer

        AbstractOueuedSynchronizer 是java.util.concurrent 的核心组件之一,它提供了一个基于 FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架。该类利用了一个int 来表示状态,期望它能够成为实现大部分同步需求的基础。使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire 和release 的方式来操纵状态。然而,多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握。

        12、同步计数器 Semaphore

        Semaphore 是一个计数信号量。从概念上讲,信号量维护了一个许可集合。如有必要,在许可可用前会阻塞每一个acquire0,然后再获取该许可。每个release0)添加一个许可,从而可能释放一个正在阻塞的获取者。

        13、同步计数器 CyclicBarrier

        CyclicBarrier 是一个同步辅助类,翻译过来叫循环栅栏、循环屏障。它允许一组线程互相等待直到到达某个公共屏障点(common barrier point),然后所有的这组线程再同步往后面执行。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时CyclicBarier 很有用。不要错误理解了,不是线程屏障可以重复使用,而是多个线程都像多个循环一样,都循环到这个点了,再一起开始往后面执行。

五、线程池:

         在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其他更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗费资源的对象创建和销毁。如何利用己有对象来服务就是一个需要解决的关键问题,其实这就是一些“池化资源”技术产生的原因。比如大家所熟悉的数据库连接池,正是遵循这一思想而产生的

        1、 Executors

        Executors 是个线程的工厂类,方便快速地创建很多线程池,也可以说是一个线程池的工具类。配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是最优的,因此,在 Executors 类里面提供了一些静态工厂,生成一些常用的线程池.。

        2、newSingleThreadExecutor

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

        3、newCachedThreadPool

        创建一个缓存池大小可根据需要伸缩的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。

        4、newFixedThreadPool

        创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

        5、线程池的好处:

  • 降低资源消耗。通过重复利用己创建的线程,降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。但是要做到合理地利用线程池,必须对其原理了如指掌。
  • 防止服务器过载,形成内存溢出,或者CPU 耗尽。

         关于线程的应用的demo:
Android中线程的相关知识及线程池的管理工具类-CSDN博客文章浏览阅读3.9k次。一.引子:在Android默认主线程(UI线程)不能进行耗时操作的,如果在主线程中进行了耗时的操作会导致ANR,所以在很多的操作必然会开启新的线程去做耗时的操作。二.线程的开启有两种方式及线程相关知识: 1.继承Thread类创建线程类:首先,定义Thread类的子类,并重写该类的run()方法,这个方法就代表了线程要完成的任务,因此把run()方法称为执行体;再者,创建Threa...https://blog.csdn.net/wangyongyao1989/article/details/70185946        

    六、Fork/Join:

        Fork/Join(分叉/结合)框架是一个比较特殊的线程池框架,专用于需要将一个任务不断分解成子任务(分叉),再不断进行汇总得到最终结果(结合)的计算过程。比起传统的线程池类ThreadPoolExecutor,ForkJoinPool 实现了工作窃取算法,使得空闲线程能够主动分担从别的线程分解出来的子任务,从而让所有的线程都尽可能处于饱满的工作状态,并因此提高了执行效率。

               1、Future 任务机制和 FutureTask

         Future 类就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成获取结果。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果。Future 类位于 java.util.concurrent包下,它也是一个接口。

        FutureTask目前是Future接口的一个唯一实现类。

                2、Fork/Join框架:

        Fork/Join 框架是 Java7提供的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

  • fork0,这个方法决定了 ForkJoinTask的异步执行,凭借这个方法可以创建新的任务。
  • join (),该方法负责在计算完成后返回结果,因此允许一个任务等待另一任务执行完成。
public class ForkJoinTaskDemo {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        CountTask countTask1 = new CountTask(1, 5);
        ForkJoinTask<Integer> result1 = forkJoinPool.submit(countTask1);
        CountTask countTask2 = new CountTask(1, 100);
        System.out.println("1-5最终相加的结果:" + result1.get());
        ForkJoinTask<Integer> result2 = forkJoinPool.submit(countTask2);
        System.out.println("1-100最终相加的结果:" + result2.get());
        System.out.println("Thread Main End");
    }

    static class CountTask extends RecursiveTask<Integer> {
        private static final long serialVersionUID = 3336021432713606929L;
        private static int splitSize = 2;
        private int start, end;

        public CountTask(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected Integer compute() {
            int sum = 0;
            boolean canCompute = (start - end) <= splitSize;
            //如果任务已经不需要再拆分了就开始计算boolean canCompute=(end-start)<=splitSize
            if (canCompute) {
                for (int i = start; i <= end; i++) {
                    sum = sum + i;
                }
            } else {
                //拆分成两个子任务
                int middle = (start + end) / 2;
                CountTask fistTask = new CountTask(start, middle);
                CountTask secondTask = new CountTask(middle + 1, end);
                fistTask.fork();//开始执行
                secondTask.fork();
                // 获得第一个子任务的结果,得不到结果,此线程不会往下面执行
                int firstResult = fistTask.join();
                int secondResult = secondTask.join();
                //合开两个儿子的执行结果
                sum = firstResult + secondResult;
            }
            return sum;
        }
    }
}

        3、Fork/Join框架设计思想:

  • 第一步分割任务。首先我们需要有一个fork 类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停地分割,直到分割出的子任务足够小。
  • 第二步执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

        

七、Java并发相关的源码分析

        (待完善。。。。)

        1、锁:

  • synchronized
  • Lock和ReentrantLock:
  • ReentrantReadWriteLock.ReadLock
  • ReentrantReadWriteLock.WriteLock
  • ReadWriteLock和ReentrantReadWriteLock
  • StampedLock

        

        2、集合:

  • Hashtable
  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • CopyOnWriteArraySet

        3、队列

  • BlockingQueue
  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue
  • ConcurrentLinkedQueue
  • DelayQueue
  • LinkedBlockingDeque
  • LinkedTransferQueue

        4、线程池

        

        5、线程同步器

  • CountDownLatch
  • Semaphore
  • CyclicBarrier

        

  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值