线程的生命周期
- 新建状态:创建一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start()这个线程。
- 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:如果就绪状态的线程获取到 CPU 资源,就可以执行run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态:在线程死亡前失去所占用资源,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态;同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用);其他阻塞:通过调用线程的 sleep() 或 join() 或发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
- 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到死亡状态。
启动线程的方法
继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的run方法。该run方法的方法体就代表了线程要完成的任务,因此把run()方法称为执行体;
- 创建Thread子类的实例,即创建了线程对象;
- 调用线程对象的start()方法来启动该线程。
通过Runnable接口创建线程类
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体;
- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
- 调用线程对象的start()方法来启动该线程。
通过Callable和Future创建线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值;
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
- 使用FutureTask对象作为Thread对象的target创建并启动新线程;
- 调用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个CAS操作尝试把AbstractQueuedSynchronizer的state + 1;“非公平”就体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。如果设置成功,设置AbstractOwnableSynchronizer的exclusiveOwnerThread为当前线程。
- 否则,再判断state是否为0,如果是,再次尝试CAS操作;否则,判断锁的独占线程是否是当前线程。
- 如果是,state加1,表示重入了;否则获取锁失败,准备向链表插入1个Node对象。
- 在向链表插入Node对象的过程中,使用“死循环 + CAS”实现非阻塞原子操作,最终使不同线程的Node 对象有序地插入链表。
- 用表头的元素再次请求锁,如果请求失败,把表头元素线程挂起,进入同步阻塞状态。
当有1条线程释放锁时,在NonFairSync.unlock()中:
- 用1个CAS操作把AbstractQueuedSynchronizer的state - 1;
- 判断state是否是0,如果是把AbstractOwnableSynchronizer的exclusiveOwnerThread置为null。
- 如果不为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等等)将其记下。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。