线程前置知识(2.0)
线程前置知识
线程状态
- New:创建后尚未启动
- Runable:可能正在运行,也可能正在等待 CPU 时间片。包含了操作系统线程状态中的 Running 和 Ready。
- Waiting:等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
- Blocking:等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
- TimedWaiting:无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。例如 :Thread.sleep()
- Terminated:可以是线程结束任务之后自己结束,或者产生了异常而结束。
线程状态转换
- New -> Runable :调用 thread.start() 状态是 New,线程进入就绪队列,等待 CPU 调用,状态转变成 Runable
- Runable -> Blocking :运行过程中被 syncronized 或者 ReentrantLock 锁住,导致线程阻塞 等待获取锁。
- Runable -> Waiting:调用 Object.wait() 、Thread.join() 、LockSupport.park() 方法
- Runable -> TimedWaiting:调用 Thread.sleep(5000)、Thread.join(2000)、Object.wait(2000)、LockSupport.parkNanos()、LockSupport.parkUntil() 方法
- Runable -> Terminated :线程结束任务之后自己结束,或者产生了异常而结束。
线程实现方式
- thread 实现方式一: 继承 Thread
- thread 实现方式二: 实现 Runnable
- thread 实现方式三: 实现 Callable
1 | package tech.chen.juccode.a01; |
实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
线程机制
Executor
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
- 为什么使用线程池,优势?
- 线程池做的工作主要是
控制运行的线程的数量
,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果显示超过了最大数量,超出的数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行. - 它的主要特点为:
线程复用 | 控制最大并发数 | 管理线程
。
- 线程池做的工作主要是
- 线程池如何使用(Java中的线程池是通过 Executor 框架实现的,该框架中用到 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类)。
- 方法详解与代码实现。三个方法:
Executors.newFixedThreadPool(int)
: 给定线程数量的线程池Executors.newSingleThreadExecutor( )
: 只有一个线程的线程池Executors.newCachedThreadPool( )
: 一池N线程
newFixedThreadPool
示例:
1 | /** |
运行输出:
pool-1-thread-2 办理业务
!!
pool-1-thread-4 办理业务
pool-1-thread-1 办理业务!!
pool-1-thread-4 办理业务
pool-1-thread-3 办理业务!!
pool-1-thread-4 办理业务
pool-1-thread-1 办理业务!!
pool-1-thread-2 办理业务
pool-1-thread-5 办理业务!!
pool-1-thread-3 办理业务
结论:发现 定长线程池 确实最多只会有五个线程。拥有控制最大并发数的功能,超出的任务线程会在队列中等待
点进 构建方法:
1 | public static ExecutorService newFixedThreadPool(int nThreads) { |
发现使用的是 LinkedBlockingQueue() ;该队列的特点是 由链表结构组成的有界(但大小默认值 Integer.MAX_VALUE)阻塞队列.
newSingleThreadExecutor
示例:
1 | /** |
运行输出:
pool-1-thread-1 办理业务
!!
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务!!
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务!!
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务!!
pool-1-thread-1 办理业务
pool-1-thread-1 办理业务!!
pool-1-thread-1 办理业务
点进 构建方法:
1 | public static ExecutorService newSingleThreadExecutor() { |
是一个单线程化的线程池,所有任务进来都会保证按顺序进行。核心线程数和最大线程数都设为1 使用的是 LinkedBlockingQueue();
newCachedThreadPool
示例
1 | package tech.chen.juccode.a05; |
运行输出:
pool-1-thread-1 办理业务
!!
pool-1-thread-3 办理业务
pool-1-thread-2 办理业务!!
pool-1-thread-4 办理业务
pool-1-thread-2 办理业务!!
pool-1-thread-5 办理业务
pool-1-thread-6 办理业务!!
pool-1-thread-8 办理业务
pool-1-thread-7 办理业务!!
pool-1-thread-9 办理业务
点进 构建方法:
1 | public static ExecutorService newCachedThreadPool() { |
可缓存的线程池,如果将线程池的长度超过处理需要,可灵活回收空闲线程,若无可回收,则创建心的线程。核心线程数为0 ,最大线程数可以说无上限,但是使用的 是 SynchronousQueue() 阻塞队列;线程空闲 60 秒 ,则自动回收。
总结
工厂方法 | corePoolSize | maximumPoolSize | keepAliveTime | workQueue |
---|---|---|---|---|
newCachedThreadPool | 0 | Integer.MAX_VALUE | 60s | SynchronousQueue |
newFixedThreadPool | nThreads | nThreads | 0 | LinkedBlockingQueue |
newSingleThreadExecutor | 1 | 1 | 0 | LinkedBlockingQueue |
newScheduledThreadPool | corePoolSize | Integer.MAX_VALUE | 0 | DelayedWorkQueue |
工作中我们一般不会使用这些,我们生产上只能使用自定义的。
原因如下:
FixedThreadPool 和 SingleThreadPool
:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。`CachedThreadPool 和 ScheduledThreadPool
:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
Daemon
线程有两种调度模型:
- 分时调度模式:所有线程轮流使用 CPU 的使用权,平均分配每个线程占有 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU ,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些 [ Java使用的是抢占式调度模型 ]
设置和获取线程优先级:
1 | thread.setPriority(int newPriority);//设置 |
线程默认优先级是5;线程优先级范围是:1-10; 线程优先级高仅仅表示线程获取的 CPU 时间的几率高,但是要在次数比较多,或者多次运行的时候才能看到效果。
sleep()
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
1 | public void run() { |
yield()
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有 相同优先级
的其它线程可以运行。
1 | public void run() { |
线程中断
什么是中断
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止
,所以, Thread.stop 、Thread.suspend 、Thread. resume 都已经被废弃了- 在 Java 中没有办法立即停止一条线程 , 然而停止线程却显得尤为重要 , 如取消一个耗时操作。因此 , Java提供了一种用于停止线程的机制 — 中断
- 中断只是一种协作机制,Java 没有给中断增加任何语法,中断的过程完全需要程序员自己实现
- 若要中断一个线程,你需要手动调用该线程的 interrupt 方法,该方法也仅仅是将线程对象的中断标识设为 true
- 每个线程对象中都有一个标识,用于标识线程是否被中断 ; 该标识位为 true 表示中断,为 false 表示未中断;通过调用线程对象的 interrupt 方法将线程的标识位设为 true ;可以在别的线程中调用,也可以在自己的线程中调用
InterruptedException
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞
、限期等待
或者无限期等待
状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
1 | public class InterruptExample { |
1 | public static void main(String[] args) throws InterruptedException { |
1 | Main run |
实现中断
void interrupt( )实例方法
interrupt( ) 仅仅是设置线程的中断状态为 true,不会停止线程,(如果这个线程因为wait()、join()、sleep() 方法在用的过程中被打断 (interupt),会抛出 Interrupted Exception )
源码解读
1
2
3
4
5
6
7
8
9
10
11
12
13
14public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}Just to set the interrupt flag : 只是更改中断标志
boolean isInterrupted( )实例方法
返回当前 线程的终端标志
static boolean interrupted( )静态方法
判断线程是否被中断,并清除当前中断状态
假设有两个线程A、B ,线程B调用了 interrupt 方法,这个时候我们连接调用两次 isInterrupted 方法,第一次会返回 true , 然后这个方法会将中断标识位设置位 false ,所以第二次调用将返回 false
1
2
3
4
5
6
7System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());// main---false
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());// main---false
System.out.println("111111");// 111111
Thread.currentThread().interrupt();///----false---> true
System.out.println("222222");// 222222
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());//main---true
System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());//main---false
使用中断标识停止线程
- volatile 变量实现
- AtomicBoolean 变量实现
- interrupt 实现
1 | package tech.chen.juccode.a10; |
Executor 的中断操作
调用 Executor 的 shutdown()
方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow()
方法,则相当于调用每个线程的 interrupt() 方法。
以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。
1 | public static void main(String[] args) { |
1 | Main run |
如只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
1 | Future<?> future = executorService.submit(() -> { |
线程互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
synchronized
8锁问题
- 标准访问有ab两个线程,请问先打印邮件还是短信
- sendEmail方法暂停3秒钟,请问先打印邮件还是短信
- 新增一个普通的hello方法,请问先打印邮件还是hello
- 有两部手机,请问先打印邮件还是短信
- 两个静态同步方法,同1部手机,请问先打印邮件还是短信
- 两个静态同步方法, 2部手机,请问先打印邮件还是短信
- 1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信
- 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信
1 | package tech.chen.juccode.a06; |
1-2:
- 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
- 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些 synchronized 方法
- 锁的是当前对象 this ,被锁定后,其它的线程都不能进入到当前对象的其它的 synchronized 方法
3-4:
- 加个普通方法后发现和同步锁无关
- 换成两个对象后,不是同一把锁了,情况立刻变化。
5-6 :都换成静态同步方法后,情况又变化
三种 synchronized 锁的内容有一些差别:
- 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——实例对象本身,
- 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
- 对于同步方法块,锁的是 synchronized 括号内的对象
7-8:
当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。
所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
1 | public class LockExample { |
比较
锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
性能:新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。
公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象。
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
线程之间的协作
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。
join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
1 | public class JoinExample { |
1 | A |
wait() notify() notifyAll()
- 调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
- 它们都
属于 Object 的一部分
,而不属于 Thread。 只能用在同步方法或者同步控制块中使用
,否则会在运行时抛出 IllegalMonitorStateExeception。- 使用 wait() 挂起期间,线程会释放锁。
这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁
。
wait() 和 sleep() 的区别
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会。
await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
使用 Lock 来获取一个 Condition 对象。
1 | public class AwaitSignalExample { |