线程
如何创建和使用线程
- 继承 Thread 类。
- 实现 Runnable 接口。
- 使用 Callable 和 Future 创建线程。
实现 Runnable 和 Callable 接口的优缺点
优点:
- 还可以继承其他类
- 多个线程可以共享一个 target 对象,非常适合多个相同的线程来处理同一份资源的情况。
缺点:
- 编程稍微复杂,如果需要访问当前线程,则必须使用 Thread.currentThread() 方法。
采用继承 Thread 类的方式创建多线程的优缺点
优点:
- 编写简单,直接使用 this 即可获得当前线程。
缺点:
- 线程类已经继承了 Thread 类就不能再继承其他类。
线程的生命周期
新建(New),就绪(Runnable),运行(Running),阻塞(Blocked),死亡(Dead)。
- 新建:当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态。
- 就绪:当线程对象调用了 .start() 方法之后,该线程就处于就绪状态。
- 运行:处于就绪状态的线程获得了 CPU,开始执行 run() 方法中的线程执行体,则该线程处于运行状态。
- 阻塞:
- 程序调用 sleep() 方法主动放弃所占有的处理器资源,该线程被阻塞。|sleep 过了指定时间,进入就绪。
- 程序调用了一个阻塞式 IO 方法,在该方法返回之前,该线程被阻塞。|方法已经返回,进入就绪。
- 线程试图获得一个同步监视器,但该同步监视器被其他线程所持有。|线程成功获得了同步监视器,进入就绪。
- 线程在等待某个 notify。| 其他线程发出了 notify。
- 程序调用了线程的 suspend() 方法将线程挂起。此方法很容易导致思索,尽量避免使用。 | 该线程调用了 resume() 恢复方法。
被阻塞的线程会在合适的时候转变为就绪状态,而不是运行状态。被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。
- 死亡:
- run() 或者 call() 方法执行结束,结束后线程处于死亡状态。
- 线程直接抛出一个未捕获的 Exception 或 Error。
- 直接调用线程的 stop() 方法来结束该线程,此方法容易导致死锁,不建议使用。
控制线程
Join 线程
- Join() 方法 - 一个线程等待另一个线程完成的方法。当在某个程序执行流中调用其他线程的 join() 方法时,
调用线程将被阻塞,知道被 join() 方法加入的join线程执行完为止。 - join(long millis) - 在 millis 毫秒内被 join 的线程还没结束,则不再等待。
后台线程(Daemon Thread)
为其他线程提供服务的线程叫做后台线程,又称为“守护线程”或“精灵线程”。例如,JVM 的垃圾回收线程就是典型的后台线程。
特点:所有的前台线程都死亡,后台线程会自动死亡。
调用 Thread.setDeamon(true) 方法可将指定线程设置为后台线程。必须在 start() 方法之前调用。
线程睡眠(Sleep)
sleep(long millis),让当前正在执行的线程暂停 millis 毫秒,并进入阻塞状态,该方法收到系统计时器和线程调度器的精度与准确度的影响。
线程让步(Yield)
也可以让当前正在执行的线程暂停,但不会阻塞该线程,只是将线程转到就绪状态。
sleep 和 yield 的区别:
- sleep() 方法暂停线程后,会给其他线程执行机会,不理会程序的优先级。yield() 方法只会给优先级相同,或优先级更高的线程执行机会。
- sleep() 方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。而 yield() 不会将线程转入阻塞状态,只是强制当前线程进入就绪状态。可能调用 yield 之后,立即再次获得处理器资源被执行。
- sleep() 方法显式的抛出了 IntteruptedException 异常,所以调用 sleep() 方法时要么捕捉该异常,要么显式声明抛出该异常。而 yield 方法没有抛出异常。
- sleep() 方法比 yield() 方法有更好的移植性,通常不建议使用 yield() 方法来控制并发线程的执行。
改变线程的优先级
- Thread.setPriority(int newPriority) 设置线程的优先级。1-10之间。
- getPriority() 获取程序的优先级。
- 这些优先级需要系统的支持。应避免直接为线程指定优先级,使用 Thread 定义的常量。
Thread 提供了三个常量如下:
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
线程同步
同步代码块
synchronized(obj){
//这就是同步代码块
}
目的:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发的共享资源充当同步监视器。
线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
案例:package com.dzzchao.thread.sync; 包中代码。
同步方法
同步方法就是用 synchronize 来修饰一个方法,则该方法为同步方法,无需显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
案例:package com.dzzchao.thread.sync2; 包中代码。
通过同步方法可以实现线程安全的类,线程安全的类特征如下:
- 该类的对象可以被多个线程安全的访问。
- 每个线程调用该对象的任意方法之后都将得到正确结果。
- 每个线程调用该对象的任意方法之后,该对象状态依然是合理状态。
synchronized 可以修饰方法,可以修饰代码块,但不能修饰构造器,成员变量。
释放同步监视器的锁定
会释放:
- 当前线程的同步方法,同步代码块执行结束,当前线程即释放同步监视器。
- 当前现在在同步代码块、同步方法中遇到了 break、 return 终止了该代码块或该方法的继续执行,当前线程会释放同步监视器。
- 出现了未处理的 Error 或 Exception,导致了该代码块该方法异常结束时,当前线程会释放同步监视器。
- 程序执行了同步监视器对象的 wait() 方法,则当前线程暂停,并释放同步监视器。
不会释放: - 程序调用 Thread.Sleep() 方法 Thread.yield() 方法来暂停当前线程的执行。
- 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起。当然,程序应该尽量避免使用 suspend() 和 resume() 方法来控制线程。suspend 会导致死锁。
同步锁
- Lock、ReadWriteLock 是 Java 5 提供的两个根接口,并为 Lock 提供了 ReentrantLock (可重入锁)实现类,为 ReadWriteLock 提供了 ReentrantReadWriteLock 实现类。
- Java 8新增了新型的 StampedLock 类,在大多数场景中它可以替代传统的 ReentrantReadWriteLock。
- ReentrantReadWriteLock 为读写操作提供了三种锁模式: Writing、ReadingOptimistinc、Reading。
ReentrantLock
- 常用的是 ReentrantLock (可重入锁)。 使用该 Lock 对象可以显式地加锁,释放锁。
- ReentrantLock 具有可重入性,也就是说,一个线程对已被加锁的 ReentrantLock 锁再次加锁。
- ReentrantLock 对象会维持一个计数器来追踪 lock() 方法的嵌套调用,线程在每次调用 lock 加锁后,必须显式的调用 unlock() 来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的代码。
案例: package com.dzzchao.thread.ReentrantLock; 包中代码。
死锁
- 两个线程相互等待对方释放同步监视器的时候就会发生死锁。
案例: package com.dzzchao.thread.Dead; 包中代码。
线程通信
传统的线程通信
使用 Object 类提供的 wait(),notify(),notifyAll() 三个方法,
- 对于使用 synchronized 类提供的同步方法,该类的默认实例this就是同步监视器,所以可以直接使用这三个方法。
- 对于使用 synchronized 修饰的同步代码块,必须使用括号内的对象调用这三个方法。
方法解释:
- wait: 导致当前线程等待,直到其他线程调用该同步监视器的 notify() 方法或 notifyAll() 方法来唤醒该线程。
- notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用 wait() 方法),才可以执行被唤醒的线程。
- notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
案例:package com.dzzchao.thread.notify 包下代码。
使用 Condition 控制线程通信
如果程序不适用 synchronized 关键字来保持同步,而是直接用 lock 对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用传统的线程通信方法了。
Condition
- await() 类似于隐式同步监视器上的 wait() 方法,导致当前线程等待,知道其他线程调用该 Condition 的 singal() 方法或 singalAll() 方法来唤醒线程。
- signal() 唤醒在此 Lock 对象上等待的单个线程。如果所有线程都在该 Lock 对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该 Lock 对象的锁定后(使用 await() 方法),才可以执行被唤醒的线程。
- signalAll() 唤醒在此 Lock 对象上等待的所有线程。只有当前线程放弃对该 Lock 对象的锁定后,才可以执行被唤醒的线程。
使用阻塞队列(BlockingQueue)控制线程通信
- 在队列尾部插入元素。包括add()、offer()、put() 方法,当该队列已满时,这三个方法会抛出异常、返回 false、 阻塞队列。
- 在队列头部删除并返回删除的元素。 包括 remove()、poll()、take()方法。当队列已空时,这三个方法分别会 抛出异常、返回 false、阻塞队列。
- 在队列头部取出元素但不删除元素。 包括 element()、peek() 方法。当队列已空时,这两个犯法分别抛出异常、返回false。
BlockingQueue 的实现类
- ArrayBlockingQueue: 基于数组实现的 BlockingQueue 队列。 案例:package com.dzzchao.thread.blockingqueue; 包中代码。
- LinkedBlockingQueue: 基于链表实现的 BlockingQueue 队列。
- PriorityBlockingQueue: 该队列调用 remove()、pull()、take() 等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。判断元素大小的即可根据元素的本身大小来自然排序。
- SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。
- DelayQueue: 一个特殊的 BlockingQueue,底层基于 PriorityBlockingQueue 来实现。要求集合元素都实现 Delay 接口,根据几何元素的 getDaley() 方法的返回值进行排序。
线程组和未处理的异常
线程池
ThreadPoolExecutor (int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
Parameters
-
corePoolSize
线程池中核心线程的数量,默认情况下,即使核心线程没有任务在执行它也存在的,我们固定一定数量的核心线程且它一直存活这样就避免了一般情况下CPU创建和销毁线程带来的开销。我们如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程就会有超时策略,这个时间由keepAliveTime来设定,即keepAliveTime时间内如果核心线程没有回应则该线程就会被终止。allowCoreThreadTimeOut默认为false,核心线程没有超时时间。 -
maximumPoolSize
线程池中的最大线程数,当任务数量超过最大线程数时其它任务可能就会被阻塞。最大线程数=核心线程+非核心线程。非核心线程只有当核心线程不够用且线程池有空余时才会被创建,执行完任务后非核心线程会被销毁。 -
keepAliveTime
非核心线程的超时时长,当执行时间超过这个时间时,非核心线程就会被回收。当allowCoreThreadTimeOut设置为true时,此属性也作用在核心线程上。 - unit 枚举时间单位,TimeUnit
- workQueue 线程池中的任务队列,我们提交给线程池的runnable会被存储在这个对象上
线程池的分类
-
FixedThreadPool
它是个线程数量固定的线程池,该线程池的线程全部为核心线程,它们没有超时机制且排队任务队列无限制,因为全都是核心线程,所以响应较快,且不用担心线程会被回收。 -
CachedThreadPool
它是一个数量无限多的线程池,它所有的线程都是非核心线程,当有新任务来时如果没有空闲的线程则直接创建新的线程不会去排队而直接执行,并且超时时间都是60s,所以此线程池适合执行大量耗时小的任务。由于设置了超时时间为60s,所以当线程空闲一定时间时就会被系统回收,所以理论上该线程池不会有占用系统资源的无用线程。 -
ScheduledThreadPool
ScheduledThreadPool线程池像是上两种的合体,它有数量固定的核心线程,且有数量无限多的非核心线程,但是它的非核心线程超时时间是0s,所以非核心线程一旦空闲立马就会被回收。这类线程池适合用于执行定时任务和固定周期的重复任务。 -
SingleThreadExecutor
它内部只有一个核心线程,它确保所有任务进来都要排队按顺序执行。它的意义在于,统一所有的外界任务到同一线程中,让调用者可以忽略线程同步问题。
//使用举例
ExecutorService executorService = Executors.newCachedThreadPool();