Java并发包

线程、线程安全

Posted by candy1126xx on February 6, 2017

线程的生命周期

  • 新建状态:创建一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start()这个线程。
  • 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态:如果就绪状态的线程获取到 CPU 资源,就可以执行run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  • 阻塞状态:在线程死亡前失去所占用资源,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态;同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用);其他阻塞:通过调用线程的 sleep() 或 join() 或发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到死亡状态。

启动线程的方法

继承Thread类创建线程类

  1. 定义Thread类的子类,并重写该类的run方法。该run方法的方法体就代表了线程要完成的任务,因此把run()方法称为执行体;
  2. 创建Thread子类的实例,即创建了线程对象;
  3. 调用线程对象的start()方法来启动该线程。

通过Runnable接口创建线程类

  1. 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体;
  2. 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
  3. 调用线程对象的start()方法来启动该线程。

通过Callable和Future创建线程

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值;
  2. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程;
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

多线程带来的问题

如何控制CPU资源?

  • Thread.join():使当前线程交出CPU资源,给调用join()的线程,等待它执行完毕,重新拿回CPU资源。
  • Thread.sleep(duration):使当前线程交出CPU资源,进入其他阻塞状态,但不释放锁;duration时间后进入就绪状态,在下一轮的资源争夺中必定胜出。
  • Thread.yield():使当前线程交出CPU资源,进入就绪状态,但不参与下一轮的资源争夺,也不释放锁。
  • Thread.interrupt():为线程打上一个“中断”标记,线程依然存在,依然运行。
  • Thread.interrupted()/Thread.isInterrupted():检查线程是否有“中断”标记,并清除它,根据返回值可以实现逻辑上的“中断”(即我们可以指定线程在中断时干些什么,但其实它始终在运行)

重排序、竞态条件、临界区

在没有同步的情况下,编译器、处理器、运行时为了更有效率,都可能对操作的执行顺序进行一些调整,这称为重排序。

由于不正确的执行时序而出现不正确的结果的情况,叫做竞态条件。

导致竞态条件发生的代码区称作临界区,在临界区中使用适当的同步就可以避免竞态条件。

线程安全

能够避免发生竞态条件,就叫做“是线程安全的”。它包括两重含义:

  • 原子性:在一个线程对一个变量执行一系列操作时,通过某种方式防止其他线程读写该变量,从而确保其他线程只能在这些操作执行之前或之后读写该变量,则称这些操作具有原子性。
  • 可见性:一个线程对一个变量执行的写操作,如果能正确地被其他线程读取,则称这个写操作具有可见性。

实现线程安全的第一个思路:同步

同步就是用锁来保证代码的执行顺序(原子性),而“所有线程共用同一个锁”确保了可见性。

锁的性质

  • 内置锁/显示锁:以Object对象及其子类作为锁,叫做内置锁;以实现Lock接口的对象作为锁,叫做显示锁。
  • 可重入/不可重入: 可重入是指,如果一个线程调用一个synchonized方法,它获得了锁;在释放锁之前,它又调用了该方法,则它又一次获得了这个锁,方法可以执行。
  • 可中断/不可中断:可中断是指,在线程尝试获取锁的过程中,可以响应中断。
  • 公平/非公平:非公平是指,在一条线程请求锁时,先判断锁是否被占用,如果没被占用则直接获取锁,而不用管阻塞队列中是否有其它请求。
  • 内置锁是可重入、不可中断、非公平的。

声明锁

  • 用synchronized修饰方法,意味着该方法以方法的调用对象为锁,意味着当在一个线程中调用该方法后,该线程就获得了方法调用对象的内置锁,在该线程执行完该方法时会释放锁,在锁被释放之前,其他想要以内置锁形式访问该对象的线程都会进入同步阻塞状态。
  • 用static synchronized修饰方法,意味着该方法以Class对象为锁。
  • 在方法内用synchronized(mLock){}包裹代码块,其中mLock若是实现了Lock接口的对象则称为使用显式锁实现同步,否则仍然是内置锁实现同步。更进一步,如果mLock换成XXX.class,则是以Class对象为锁。当在一个线程中调用该方法并执行到这一步时,该线程就获得了这个锁,在该线程释放锁之前,其他执行到这一步(或在其它需要这把锁的步骤)的线程都会进入同步阻塞状态。与内置锁相比,显式锁可以通过mLock.unlock()主动释放锁,更加灵活,也更易错误使用。

操作内置锁

  • Object.wait():Object的内置锁被释放,原先拥有该锁的线程进入等待阻塞状态,直到调用Object.notify()/Object.notifyAll()。
  • Object.notify():原先因调用过Object.wait()而进入等待阻塞状态的线程们,从中随机选出一个,使其变为就绪状态。
  • Object.notifyAll():原先因调用过Object.wait()而进入等待阻塞状态的线程们,全部变为就绪状态。

操作显示锁

  • Lock.lock():使当前线程获得锁,其他执行到这条语句的线程将进入同步阻塞状态。
  • Lock.unlock():使当前线程释放锁,其他等待这把锁的线程进入就绪状态。

显示锁:ReentrantLock

ReentrantLock是可重入的、可中断的、可公平也可不公平的锁。

ReentrantLock中有1个Sync对象,它继承自AbstractQueuedSynchronizer。AbstractQueuedSynchronizer封装了一个双向链表,表头为成员变量head,尾为tail。链表的元素为AbstractQueuedSynchronizer.Node对象,代表一次锁请求。

AbstractQueuedSynchronizer的成员变量state记录这把锁当前被同一线程申请了几次。

AbstractQueuedSynchronizer.Node的成员变量waitStatus记录某次锁请求的状态,有CANCELLED、SIGNAL、CONDITION、PROPAGATE四种。

AbstractQueuedSynchronizer继承自AbstractOwnableSynchronizer。AbstractOwnableSynchronizer中声明了成员变量exclusiveOwnerThread用于保存锁的独占线程的引用。

Sync对象有两个实现子类,FairSync对象或NonFairSync对象。

CAS操作是指,我预判那个值是x,如果是,把它改为y;否则返回那个值。

当有1条线程请求锁时,在NonFairSync.lock()中:

  1. 用1个CAS操作尝试把AbstractQueuedSynchronizer的state + 1;“非公平”就体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。如果设置成功,设置AbstractOwnableSynchronizer的exclusiveOwnerThread为当前线程。
  2. 否则,再判断state是否为0,如果是,再次尝试CAS操作;否则,判断锁的独占线程是否是当前线程。
  3. 如果是,state加1,表示重入了;否则获取锁失败,准备向链表插入1个Node对象。
  4. 在向链表插入Node对象的过程中,使用“死循环 + CAS”实现非阻塞原子操作,最终使不同线程的Node 对象有序地插入链表。
  5. 用表头的元素再次请求锁,如果请求失败,把表头元素线程挂起,进入同步阻塞状态。

当有1条线程释放锁时,在NonFairSync.unlock()中:

  1. 用1个CAS操作把AbstractQueuedSynchronizer的state - 1;
  2. 判断state是否是0,如果是把AbstractOwnableSynchronizer的exclusiveOwnerThread置为null。
  3. 如果不为null且waitState不为0,用表头的线程对象去尝试获取锁。

FairSync对象与NonFairSync对象的差别仅在于“公平性”。FairSync对象要求每一次请求都要入队。

实现线程安全的第二个思路:volatile

同步使并发对执行效率的提升不能发挥100%的作用。所以有了不严格的volatile。

用volatile修饰变量,意味着对该变量的操作不会参与重排序,从而保证了对它进行写操作的可见性。然而它被没有像锁机制那样阻止其他线程使用该变量,所以它更加脆弱。

实现线程安全的第三个思路:ThreadLocal

ThreadLocal是使用线程封闭的思路来实现线程安全。ThreadLocal会为使用相同变量的不同线程创建不同的存储,并且只能通过get()/set()方法访问这份存储,这样就不会出现竞态条件。

然而,它会占用额外的空间,get()/set()方法也会使执行效率下降。所以仅适用于少量的、线程单例的数据。

高级工具

Callable和Future

考虑这种情景:主线程需要做一个判断,判断的依据是自己运算的结果1和异步任务运算的结果2。如果用Runnable,需要在得出结果1的时候检查是否已经得出结果2,如果没有要阻塞主线程直到得出结果2;还要在得出结果2的时候检查是否已经得出结果1。如果用Callable和Future的组合,就不用这么麻烦,在做判断的地方执行Future.get(),就可以实现上述逻辑。

CompletionService

CompletionService适用于使用Callable和Future的组合实现多个异步任务。比如同时开启下载任务1和下载任务2。假设任务1耗时长而任务2耗时短,如果用CompletionService.task().get(),会在任务2完成时返回任务2的结果,然后在任务1完成时再返回任务1的结果;如果用循环Future.get(),任务1结束前会阻塞,就算任务2完成了也不能返回。

Condition和显式锁

每个Condition都代表一个阻塞队列,这样每个类型的线程可以使用各自的阻塞队列,从而更精确地控制唤醒哪类线程。

Semaphore

锁机制只允许1个线程访问共享资源,那么如果想要允许n个线程访问共享资源就要用到Semaphore。

CountDownLatch

CountDownLatch可以表示:等到多个子线程都完成了各自的任务,主线程再继续执行。

举例:应用启动时,需要访问很多个接口,要等他们都返回后,再决定如何加载首页。

CyclicBarrier

CyclicBarrier可以表示:等到多个子线程都完成了各自的任务,它们再各自执行一项相同的任务。

举例:把一个文件分块,用多个线程复制到一个文件夹中,所有子线程都复制完成后,再把副本分块上传。

锁带来的问题:死锁

死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个进程使用;
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何预防死锁?

  • 思路一:当多个线程需要相同的一些锁,如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
  • 思路二:线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁。
  • 思路三:每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。