并发基础学习笔记

目录

 

一、基础知识

1. 多线程的实现

2. synchronized关键字

3. 线程的状态

4. sleep和wait的区别

5. 锁的类型和锁的优化

6. ConcurrentLinkedQueue和LinkedBlockingQueue的区别

7. 前台线程和后台线程

8. 如何保证线程安全

9. JVM 的线程和操作系统的线程是什么关系?

10. 如何中断线程的执行?

11. 守护线程

二、多线程之间的通信

1.wait/notify机制

2.同步

3.while轮询的方式

4.管道通信

三、线程的调度

四、线程池的使用

1. 为什么用线程池?

2. 线程池的原理

3. 当一个任务被添加进线程池时,执行策略:

4. 常见四种线程池:

5. 判断线程池中的线程是否执行完

6. 线程池中的最大线程数

五、ThreadLocal的使用

    1.ThreadLocal基础知识

    2.原理

六、CAS(Compare and Swap)操作

    1.定义

    2.CAS的ABA问题

    3.原子类实现原理

七、AbstractQueuedSynchronizer(AQS)同步器的原理

    1. 同步器的应用

    2. 实现原理

八、concurrent包中使用过哪些类?分别说说使用在什么场景?为什么要使用?

    1. lock

    2. concurrenthashmaap

    3. CopyOnWriteArrayList(比较适用于读多写少的并发场景)

    4.CyclicBarrier 和 CountDownLatch 的区别

    5. Condition 接口及其实现原理

    6. 阻塞队列:BlockingQueue

九、常见题目

    1、Lo4j是线程安全的吗?怎么保证线程安全?

    2、虚假唤醒

    3、 单CPU有必要使用多线程吗?


一、基础知识

1. 多线程的实现

1.1 实现多线程的三种方式

 (1)继承Thread类

 (2)实现Runnable接口

 (3)使用ExecutorService、Callable、Future实现有返回结果的多线程

1.2 Thread和Runable的区别和联系

(1)联系:

    1、Thread类实现了Runable接口。

    2、都需要重写里面Run方法。

(2)不同:

    1、实现Runnable的类更具有健壮性,避免了单继承的局限。

    2、Runnable更容易实现资源共享,能多个线程同时处理一个资源。

2. synchronized关键字

    (1)synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

    1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

    2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

    3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

    4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

    (2)Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。

    (3)Synchronized 原理

    在编译的字节码中加入了两条指令来进行代码的同步。

    monitorenter :

    每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

    1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

    2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

    3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    monitorexit:

    执行monitorexit的线程必须是objectref所对应的monitor的所有者。

    指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 

     通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

    (4)synchronized与Lock的区别

    lock是一个类,主要有以下几个方法:

    lock():获取锁,如果锁被暂用则一直等待

    unlock():释放锁

    tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true

    tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间

    (1)Lock的加锁和解锁都是由java代码实现的,而synchronize的加锁和解锁的过程是由JVM管理的。

    (2)synchronized能锁住类、方法和代码块,而Lock是块范围内的。

    (3)Lock能提高多个线程读操作的效率;

    (4)Lock:Lock实现和synchronized不一样,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。而Lock底层其实是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它重新去拿吃的就好啦,所以它很乐观。如果面试问起,你就说底层主要靠volatile和CAS操作实现的。

3. 线程的状态

    yield方法:使当前线程从执行状态变为就绪状态。

    sleep方法:强制当前正在执行的线程休眠,当睡眠时间到期,则返回到可运行状态。

    join方法:通常用于在main()主线程内,等待其它线程完成再结束main()主线程。

    线程的阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    (1)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

    (2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

    (3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(sleep是不会释放持有的锁)

4. sleep和wait的区别

    Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。但是sleep是不释放锁的。

5. 锁的类型和锁的优化

    在 java 中锁的实现主要有两类:内部锁 synchronized(对象内置的monitor锁)和显示锁java.util.concurrent.locks.Lock。

    在 java.util.concurrent.locks 包中有很多Lock的实现类,常用的有 ReentrantLock 和ReadWriteLock,其实现都依赖 java.util.concurrent.AbstractQueuedSynchronizer 类。

    5.1 锁的一些概念

  •     可重入锁:指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响,执行对象中所有同步方法不用再次获得锁。

        避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。

  •     自旋锁:所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会让出时间片,所以其他线程依旧有申请锁和释放锁的机会。自旋锁省去了阻塞锁的时间空间(队列的维护等)开销,但是长时间自旋就变成了“忙式等待”,忙式等待显然还不如阻塞锁。所以自旋的次数一般控制在一个范围内,例如10,100等,在超出这个范围后,自旋锁会升级为阻塞锁。
  •     公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
  •     读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写。
  •     独占锁:是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
  •     乐观锁:每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。

    5.2 synchronized的优化(jdk 1.6之后引入)

  •     偏向锁(无锁):大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后(线程的id会记录在对象的Mark Wod中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。
  •     轻量级锁(CAS):轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。
  •     重量级锁:虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。

6. ConcurrentLinkedQueue和LinkedBlockingQueue的区别

    Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。

    6.1 LinkedBlockingQueue

    由于LinkedBlockingQueue实现是线程安全的(锁机制),实现了先进先出等特性,是作为生产者消费者的首选,LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。

    6.2 ConcurrentLinkedQueue

    ConcurrentLinkedQueue是Queue的一个安全实现.Queue中元素按FIFO原则进行排序.采用CAS操作,来保证元素的一致性。

7. 前台线程和后台线程

    main()函数(主函数)是一个前台线程,前台线程是程序中必须执行完成的,而后台线程则是java中所有前台结束后结束,不管有没有完成,后台线程主要用与内存分配等方面。                                                                                           

    前台线程和后台线程的区别和联系:

    1、后台线程不会阻止进程的终止。属于某个进程的所有前台线程都终止后,该进程就会被终止。所有剩余的后台线程都会停止且不会完成。

    2、可以在任何时候将前台线程修改为后台线程,方式是设置Thread.IsBackground 属性。

    3、不管是前台线程还是后台线程,如果线程内出现了异常,都会导致进程的终止。

    4、托管线程池中的线程都是后台线程,使用new Thread方式创建的线程默认都是前台线程。

    说明:   

            应用程序的主线程以及使用Thread构造的线程都默认为前台线程                       

        使用Thread建立的线程默认情况下是前台线程,在进程中,只要有一个前台线程未退出,进程就不会终止。主线程就是一个前台线程。而后台线程不管线程是否结束,只要所有的前台线程都退出(包括正常退出和异常退出)后,进程就会自动终止。一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。

8. 如何保证线程安全

    8.1 并发的三个核心概念

    并发编程中需要关注的三个核心概念:原子性、可见性、顺序性。

    原子性:跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

    锁和同步(同步方法和同步代码块)、CAS(CPU级别的CAS指令)。

    可见性:当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。

    volatile关键字来保证可见性。

    顺序性:程序执行的顺序按照代码的先后顺序执行。

    volatile在一定程序上保证顺序性,另外还可以通过synchronized和锁来保证顺序性。

    8.2 volatile 的工作机制(如何保证可见性和禁止指令重排序,虚拟机部分也有)

    加入volatile关键字时在汇编代码中会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

    (1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;(禁止重排序)

       (2)它会强制将对缓存的修改操作立即写入主存;(可见性)

       (3)如果是写操作,它会导致其他CPU中对应的缓存行无效。(可见性)

    volatile 如何保证原子性:

     (1)对变量的写操作不依赖于当前值(只有单一的线程修改变量的值)。

     (2)该变量没有包含在具有其他变量的不变式中。

    8.3 保证线程安全

    线程安全是指多线程访问同一代码不会产生不确定的结果,线程安全低依靠线程同步来实现。

    一般说来,确保线程安全的方法有这几个:竞争与原子操作、同步与锁、可重入、过度优化(使用volatile关键字试图阻止过度优化)。

    (1)通过架构设计

    (2)保证类无状态

    无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 。不能保存数据,是不变类,是线程安全的。

    (3)锁

    (4)ThreadLocal(待考究)

    ThreadLocal 是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

    关于线程的同步,一般有以下解决方法:

    (1) 在需要同步的方法的方法签名中加入synchronized关键字。

    (2)使用synchronized块对需要进行同步的代码段进行同步。

    (3)使用JDK 5中提供的java.util.concurrent.lock包中的Lock对象。

9. JVM 的线程和操作系统的线程是什么关系?

    Java并没有自己的线程模型,而是使用了操作系统的原生线程。因为实现自己的线程模型需要做的事情很多:如何处理阻塞、如何在多CPU之间合理地分配线程、如何锁定,包括创建、销毁线程等等。

1)操作系统实现线程的三种方式

    一种是用户支持线程,另一种是内核支持线程,还有一种是两种的组合方式(一对一、多对一、多对多)。设置用户级线程的系统,调度是以进程为单位的。而设置了内核级线程的系统是以线程为单位进行调度的。

2)JVM中的线程实现

    Java里的线程是由JVM来管理的,JVM相对于OS是一个进程,JVM中的线程如何对应到操作系统的线程是由JVM的实现来确定的。

    现在的大多数虚拟机来看,Java线程在Windows及Linux平台上的实现方式是内核线程(一对一)的实现方式,一条Java线程就映射到一条轻量级进程中(内核线程的一种高级接口)。这种方式实现的线程,是直接由操作系统内核支持的——由内核完成线程切换,内核通过操纵调度器(Thread Scheduler)实现线程调度,并将线程任务反映到各个处理器上。

10. 如何中断线程的执行?

    (1)设置退出标志(boolean类型的变量),使线程正常退出,也就是当run()方法完成后线程终止。

    (2)使用 interrupt() 方法中断线程。

    (3)使用 stop 方法强行终止线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!)

    如果强制让线程停止有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了解锁,导致数据得不到同步的处理,可能出现数据不一致的问题。

11. 守护线程

    为系统中的其它对象和线程提供服务,守护线程在没有用户线程可服务时自动离开(Java垃圾回收线程就是一个典型的守护线程)

 

二、多线程之间的通信

1.wait/notify机制

    wait():该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法。进入 wait()方法后,当前线程释放锁。在从 wait()返回前,线程与其他线程竞争重新获得锁。如果调用 wait()时,没有持有适当的锁,则抛出 IllegalMonitorStateException,它是 RuntimeException 的一个子类,因此,不需要 try-catch 结构。

    notify():该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个 wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的 wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait 状态等待的线程由于没有得到该对象的通知,会继续阻塞在 wait 状态,直到这个对象发出一个 notify 或 notifyAll。这里需要注意:它们等待的是被 notify 或 notifyAll,而不是锁。

    notifyAll():notifyAll 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll 线程退出调用了 notifyAll 的 synchronized 代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

2.同步

(多个线程通过synchronized关键字这种方式来实现线程间的通信)

    本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

3.while轮询的方式

    尽管线程A一直在while中执行,需要占用CPU。但是,线程的调度是由JVM或者说是操作系统来负责的,并不是说线程A一直在while循环,然后线程B就占用不到CPU了。对于线程A而言,它就相当于一个“计算密集型”作业了。如果我们的while循环是不断地测试某个条件是否成立,那么这种方式就很浪费CPU。如果同步快中代码进入了死循环,则可能导致多线程无法继续执行下去。

4.管道通信

(通过管道,将一个线程中的消息发送给另一个)

 

三、线程的调度

    线程有两种调度模型:

  •     分时调度模型 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  •     抢占式调度模型 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

    Java使用的是抢占式调度模型。

四、线程池的使用

1. 为什么用线程池?

  •     1.创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率。(线程复用)
  •     2.线程并发数量过多,抢占系统资源从而导致阻塞。(控制并发数量)
  •     3.对线程进行一些简单的管理。(管理线程)

2. 线程池的原理

    (1)线程复用:实现线程复用的原理应该就是要保持线程处于存活状态(就绪,运行或阻塞)

    (2)控制并发数量:(核心线程和最大线程数控制)

    (3)管理线程(设置线程的状态)

3. 当一个任务被添加进线程池时,执行策略:

  •     线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
  •     线程数量达到了corePoolSize,则将任务移入队列等待空闲线程将其取出去执行(通过getTask()方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源,整个getTask操作在自旋下完成)
  •     队列已满,新建线程(非核心线程)执行任务
  •     队列已满,总线程数又达到了maximumPoolSize,就会执行任务拒绝策略。

4. 常见四种线程池:

   (1)可缓存线程池CachedThreadPool()

    public static ExecutorService newCachedThreadPool() {         return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                       60L, TimeUnit.SECONDS,                                       new SynchronousQueue<Runnable>());     }

    根据源码可以看出:

    这种线程池内部没有核心线程,线程的数量是有没限制的。

    在创建任务时,若有空闲的线程时则复用空闲的线程,若没有则新建线程。

    没有工作的线程(闲置状态)在超过了60S还不做事,就会销毁。

    适用:执行很多短期异步的小程序或者负载较轻的服务器。

    (2)FixedThreadPool 定长线程池

    public static ExecutorService newFixedThreadPool(int nThreads) {      return new ThreadPoolExecutor(nThreads, nThreads,                                   0L, TimeUnit.MILLISECONDS,                                   new LinkedBlockingQueue<Runnable>());     }

    根据源码可以看出:

    该线程池的最大线程数等于核心线程数,所以在默认情况下,该线程池的线程不会因为闲置状态超时而被销毁。

    如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的闲置线程,会创建新的线程去执行任务。如果当前执行任务数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务。

    适用:执行长期的任务,性能好很多。

    (3)SingleThreadPool

 

    public static ExecutorService newSingleThreadExecutor() {       return new FinalizableDelegatedExecutorService         (new ThreadPoolExecutor(1, 1,                                 0L, TimeUnit.MILLISECONDS,                                 new LinkedBlockingQueue<Runnable>()));     }

    根据源码可以看出:

    有且仅有一个工作线程执行任务

    所有任务按照指定顺序执行,即遵循队列的入队出队规则。

    适用:一个任务一个任务执行的场景。

   (4)ScheduledThreadPool

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {      return new ScheduledThreadPoolExecutor(corePoolSize);     }     //ScheduledThreadPoolExecutor():     public ScheduledThreadPoolExecutor(int corePoolSize) {        super(corePoolSize, Integer.MAX_VALUE,           DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,           new DelayedWorkQueue());     }

    根据源码可以看出:

    DEFAULT_KEEPALIVE_MILLIS就是默认10L,这里就是10秒。这个线程池有点像是CachedThreadPool和FixedThreadPool 结合了一下。

    不仅设置了核心线程数,最大线程数也是Integer.MAX_VALUE。

    这个线程池是上述4个中唯一一个有延迟执行和周期执行任务的线程池。

适用:周期性执行任务的场景(定期的同步数据)

 

总结:除了new ScheduledThreadPool 的内部实现特殊一点之外,其它线程池内部都是基于ThreadPoolExecutor类(Executor的子类)实现的。

    (1)ThreadPoolExecutor类构造器语法形式:

    ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime,timeUnit,workQueue,threadFactory,handle);   

    方法参数:

       corePoolSize:核心线程数(最小存活的工作线程数量)

       maxPoolSize:最大线程数

         keepAliveTime:线程存活时间(在corePoreSize<maxPoolSize情况下有用,线程的空闲时间超过了keepAliveTime就会销毁)

         timeUnit:存活时间的时间单位

         workQueue:阻塞队列,用来保存等待被执行的任务(①synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务;②LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;③ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小)

         threadFactory:线程工厂,主要用来创建线程;

         handler:表示当拒绝处理任务时的策略(①丢弃任务并抛出RejectedExecutionException异常;②丢弃任务,但是不抛出异常;③丢弃队列最前面的任务,然后重新尝试执行任务;④由调用线程处理该任务)

    (2)在ThreadPoolExecutor类中有几个非常重要的方法:

        execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

      submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,实际上它还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。

      shutdown()不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。

        shutdownNow()立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

5. 判断线程池中的线程是否执行完

(1)使用 isTerminated()方法

    调用ExecutorService.shutdown方法的时候,线程池不再接收任何新任务,但此时线程池并不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。在调用shutdown方法后我们可以在一个死循环里面用isTerminated方法判断是否线程池中的所有线程已经执行完毕,如果子线程都结束了,我们就可以做关闭流等后续操作了。

(2)使用闭锁(CountDownLatch)

    CountDownLatch是一种灵活的闭锁实现,它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生了,而await方法等待计数器达到零,即表示需要等待的事情都已经发生。

(3)使用信号量机制

6. 线程池中的最大线程数

    一般说来,线程池的大小经验值应该这样设置:(其中N为CPU的个数)

  •     如果是CPU密集型应用,则线程池大小设置为N+1
  •     如果是IO密集型应用,则线程池大小设置为2N+1

    如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。

    但是,IO优化中,这样的估算公式可能更适合:

    最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

    因为很显然,线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

    下面举个例子:

    比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。

五、ThreadLocal的使用

    1.ThreadLocal基础知识

    很多地方叫做线程本地变量,也有些地方叫做线程本地存储。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

    ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。

    Synchronized 用于线程间的数据共享,而 ThreadLocal 则用于线程间的数据隔离。

    初始容量16,负载因子2/3,解决冲突的方法是再hash法,也就是:在当前hash的基础上再自增一个常量进行哈希。

    2.原理

    ThreadLocalMap中用于存储数据的entry定义,使用了弱引用,可能造成内存泄漏。

    当线程没有结束,但是ThreadLocal对象已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。解决办法:

    (1)使用完线程共享变量后,显式调用ThreadLocalMap.remove方法清除线程共享变量;

    (2)ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。

    ThreadLocal类提供的几个方法:

  1.     public T get() { }  
  2.     public void set(T value) { }  
  3.     public void remove() { }  
  4.     protected T initialValue() { }  

      get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。

    每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

    初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

     然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

    3.总结

           1)实际通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

            2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,可以用不同的ThreadLocal作为key,区分不同的value方便存取。

            3)在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

    4.应用场景

    最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等。

六、CAS(Compare and Swap)操作

    1.定义

    CAS算法是由硬件直接支持来保证原子性的,CAS操作过程是一个原子操作,它是由一条CPU指令完成的。有三个操作数:要更新的变量V、旧的预期值A和新值B,当且仅当V==A时,CAS用新值B原子化地更新V的值,否则,它什么都不做。

    2.CAS的ABA问题

     当然CAS也并不完美,它存在"ABA"问题,假若一个变量初次读取是A,在compare阶段依然是A,但其实可能在此过程中,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。 

    解决:各种乐观锁的实现中通常都会用版本戳version(修改次数、版本号)来对记录或对象标记,避免并发操作带来的问题。

    3.原子类实现原理

(AtomicInteger这个原子类来进行分析)

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

    public final boolean compareAndSet(int expect, int update) {         return unsafe.compareAndSwapInt(this, valueOffset, expect, update);     }

    compareAndSet方法来进行原子更新操作,这个方法的语义是:

     先检查当前value(偏移量为 valueOffset 的值)是否等于expect,如果相等,则意味着value没被其他线程修改过,则用update更新value的值并返回true。如果不相等,compareAndSet则会返回false,然后循环继续尝试更新。

    compareAndSet调用了Unsafe类的compareAndSwapInt方法,compareAndSwapInt是个native方法,也就是平台相关的。它是基于CPU的CAS指令来完成的。

七、AbstractQueuedSynchronizer(AQS)同步器的原理

    1. 同步器的应用

     同步器主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,对同步状态的修改或者访问主要通过同步器提供的3个方法:

  •     getState() 获取当前的同步状态
  •     setState(int newState) 设置当前同步状态
  •     compareAndSetState(int expect,int update) 使用CAS设置当前状态,该方法能够保证状态设置的原子性。

    

    同步器可以支持独占式的获取同步状态,也可以支持共享式的获取同步状态,这样可以方便实现不同类型的同步组件(ReentrantLock、Semaphore、CountDownLatch)。 同步器也是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。

    2. 实现原理

    2.1 同步队列

    同步器AQS内部的实现是依赖同步队列(一个FIFO的双向队列,其实就是双向链表)来完成同步状态的管理。

       当前线程获取同步状态失败时,同步器AQS会将当前线程和等待状态等信息构造成为一个节点(Node)加入到同步队列,同时会阻塞当前线程; 当同步状态释放的时候,会把首节点中的线程唤醒,使首节点的线程再次尝试获取同步状态。

    Node的主要字段有:

    (1)waitStatus:等待状态。

    (2)prev:前驱节点

    (3)next:后继节点

    (4)thread:当前节点代表的线程

    (5)nextWaiter:Node既可以作为同步队列节点使用,也可以作为Condition的等待队列节点使用。在作为同步队列节点时,nextWaiter可能有两个标识当前节点模式的值:EXCLUSIVE(独占模式)、SHARED(共享模式);在作为等待队列节点使用时,nextWaiter保存后继节点。

    (1)同步器拥有首节点(head)和尾节点(tail),同步队列的基本结构如下:

    (2)同步队列设置尾节点(未获取到锁的线程加入同步队列): 同步器AQS中包含两个节点类型的引用:一个指向头结点的引用(head),一个指向尾节点的引用(tail),当一个线程成功的获取到锁(同步状态),其他线程无法获取到锁,而是被构造成节点(包含当前线程,等待状态)加入到同步队列中等待获取到锁的线程释放锁。这个加入队列的过程,必须要保证线程安全。否则如果多个线程的环境下,可能造成添加到队列等待的节点顺序错误,或者数量不对。因此同步器提供了CAS原子的设置尾节点的方法(保证一个未获取到同步状态的线程加入到同步队列后,下一个未获取的线程才能够加入)。

    

    (3)同步队列设置首节点(原头节点释放锁,唤醒后继节点):同步队列遵循FIFO,头节点是获取锁(同步状态)成功的节点,头节点在释放同步状态的时候,会唤醒后继节点,而后继节点将会在获取锁(同步状态)成功时候将自己设置为头节点。设置头节点是由获取锁(同步状态)成功的线程来完成的,由于只有一个线程能够获取同步状态,则设置头节点的方法不需要CAS保证,只需要将头节点设置成为原首节点的后继节点 ,并断开原头结点的next引用。

    2.2 源码分析

    2.2.1 acquire(int)  

    此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。获取到资源后,线程就可以去执行其临界区代码了。

    

    (1)调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;

    (2)没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;

    (3)acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

    (4)如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

    2.2.2 release(int)

    此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0,它是根据tryRelease()的返回值来判断该线程是否已经完成释放资源),它会唤醒等待队列里的其他线程来获取资源。

 

    

    2.2.3 acquireShared(int)

    共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。

    (1)tryAcquireShared()尝试获取资源,成功则直接返回;

    (2)失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。

     其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)。

    2.2.4 releaseShared()

    共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。

 

八、concurrent包中使用过哪些类?分别说说使用在什么场景?为什么要使用?

    外层框架主要有Lock(ReentrantLock、ReadWriteLock等)、同步器(semaphores等)、阻塞队列(BlockingQueue等)、Executor(线程池)、并发容器(ConcurrentHashMap等)、还有Fork/Join框架;

    内层有AQS(AbstractQueuedSynchronizer类,锁功能都由他实现)、非阻塞数据结构、原子变量类(AtomicInteger等无锁线程安全类)三种。

    底层就是volatile和CAS。

    1. lock

    2. concurrenthashmaap

    ConcurrentHashMap并没有采用synchronized进行控制,而是使用了ReentrantLock。

    3. CopyOnWriteArrayList(比较适用于读多写少的并发场景)

    CopyOnWriteArrayList是一个线程安全、并且在读操作时无锁的ArrayList。和ArrayList不同的是,add元素的时候会创建一个新的object数组,大小比之前数组大1。将之前的数组元素复制到新数组,并将新加入的元素加到数组末尾。

    优点:而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常。

    缺点:一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

    4.CyclicBarrier 和 CountDownLatch 的区别

    CountDownLatch 类位于 java.util.concurrent 包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch 来实现,调用 await() 方法的线程会被挂起,它会等待直到count值为0才继续执行。

    CyclicBarrier可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

    5. Condition 接口及其实现原理

    Condition 实现等待的时候内部是一个等待队列,等待队列是一个单向队列,等待队列中的每一个节点是一个 AbstractQueuedSynchronizer.Node 实例。

    每个 Condition 对象中保存了 firstWaiter 和 lastWaiter 作为队列首节点和尾节点,每个节点使用 Node.nextWaiter 保存下一个节点的引用。

    每当一个线程调用 Condition.await() 方法,那么该线程会释放锁,然后构造成一个Node节点加入到等待队列的队尾。

    Condition 的本质就是等待队列和同步队列的交互:

    当一个持有锁的线程调用 Condition.await() 时,它会执行以下步骤:

    (1)构造一个新的等待队列节点加入到等待队列队尾

    (2)释放锁(把该节点从同步队列队首移除)

    (3)自旋,直到它在等待队列上的节点移动到了同步队列(通过其他线程调用signal())或被中断

    (4)阻塞当前节点,直到它获取到了锁,也就是它在同步队列上的节点排队排到了队首。

    当一个持有锁的线程调用 Condition.signal() 时,它会执行以下操作:

    从等待队列的队首开始,尝试对队首节点执行唤醒操作,如果节点CANCELLED,就尝试唤醒下一个节点;如果再CANCELLED则继续迭代。对每个节点执行唤醒操作时,其实是将节点加入同步队列。

    6. 阻塞队列:BlockingQueue

    BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。BlockingQueue不光实现了一个完整队列所具有的基本功能,同时在多线程环境下,他还自动管理了多线间的自动等待和唤醒功能,从而使得程序员可以忽略这些细节,关注更高级的功能。

 

    6.1 ArrayBlockingQueue

      基于数组的阻塞队列实现(计算插入和取出元素位置时体现出是循环队列),在ArrayBlockingQueue内部,维护了一个定长数组(创建的时候需要指定容量capacity,因为不会自动扩容)以便缓存队列中的数据对象,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

      ArrayBlockingQueue的 put()和 take()都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue。

    按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。

    ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。

    

    

    

    

    

    6.2 LinkedBlockingQueue

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

      如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

    

 

 

    

 

    6.3 DelayQueue

      DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

      使用场景:

      DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。

    6.4 PriorityBlockingQueue

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

    6.5 SynchronousQueue

    SynchronousQueue 队列内部仅允许容纳一个元素,当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。 7. 原子变量类(无锁类)

    见第六章。

九、常见题目

    1、Lo4j是线程安全的吗?怎么保证线程安全?

    Lo4j 是线程安全的。Log4j中有一个AsyncAppender可以做一个桥接的方式将其他的Appender连接起来。而这个AsyncAppender的作用就是Log4j对所有的LogEvent的事件实际输出是异步的。好像默认的Buffer Size是128条。

    2、虚假唤醒

    不应该被唤醒的线程被唤醒了。

    当创建有两个消费者的生产消费者模型的时候,会产生虚假唤醒,导致product 为负数;当消费者线程A发现没货的时候,wait之后释放锁,另外一个 消费者线程B获得锁开始执行,结果也没货,开始wait,当生产者生产之后 notifyAll,A,B线程开始继续向下执行,结果进行了两次–操作,导致 product成为了负数。

    JDK文档 object 的 wait 方法已经考虑到这种情况,防止虚假唤醒,应该放在循环中,多次进行检查,直到满足条件才进行下一步。

    3、 单CPU有必要使用多线程吗?

        通常一个任务不光 cpu 上要花时间, io 上也要花时间(例如去数据库查数据,去抓网页,读写文件等)。 一个线程在等 io 的时候, cpu 是闲置的,另一个线程正好可以利用 cpu 进行计算。 多个线程一起跑,可以把 io 和 cpu 都跑满了。

    

 

LockSupport工具

 

Fork/Join框架的理解

jdk8的parallelStream的理解

分段锁的原理,锁力度减小的思考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IMUHERO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值