文章目录
- 基础概念
- 1. 什么是进程,什么是线程,它们之间的区别是什么?
- 2. 什么叫做线程调度,有哪些线程调度?
- 3.什么叫做同步,什么叫做异步?
- 4.什么叫做并发,什么叫做并行?
- 5.线程的状态有哪些?
- 6.线程死锁是什么?
- 创建新线程
- 1. 继承Thread类
- 2. 实现Runable接口
- 3. 实现Callable接口
- 4. 实现Runable接口和 继承Thread类相比的优势
- 5. 实现Runable接口和 实现Callable接口的区别
- 同步锁
- 1. 同步代码块
- 2. 同步方法
- 3. 显式锁
- 4. 显式锁的优势
- 5. 公平锁和不公平锁
- 线程池
基础概念
1. 什么是进程,什么是线程,它们之间的区别是什么?
-
进程 - 进程是一个“执行中的程序”,是系统进行资源分配和调度的一个独立单位
-
线程 - 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少 有一个线程, 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分 成若干个线程
区别:进程是 资源分配 的基本单位; 线程是 程序执行 的基本单位
2. 什么叫做线程调度,有哪些线程调度?
线程调度:指按照特定机制为多个线程分配cpu的使用权。
线程调度分类:
- 分时调度
所有线程轮流使用 cpu 的使用权,平均分配每个线程占用 cpu 的时间。
- 抢占式调度
优先让优先级高的线程使用 cpu,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为 抢占式调度。
3.什么叫做同步,什么叫做异步?
- 同步:排队执行,效率低但是安全
- 异步:同时执行,效率高但是数据不安全
4.什么叫做并发,什么叫做并行?
- 并发:指两个或多个事件在同一个时间段内发生
- 并行:指两个或多个事件在同一时刻发生(同时发生)
5.线程的状态有哪些?
线程的状态包括 新建状态,运行状态,阻塞等待状态和消亡状态。其中阻塞等待状态又分为 BLOCKED,WAITING 和 TIMED_WAITING 状态。
-
NEW:这是属于一个已经创建的线程,但是还没有调用 start 方法启动的线程所处的状态。
-
RUNNABLE:总体上就是当我们创建线程并且启动之后,就属于 Runnable 状态。
-
BLOCKED:当线程准备进入 synchronized同步块或同步方法的时候,需要申请一个监视器锁而进行的等 待,会使线程进入BLOCKED 状态
-
WAITING :该状态的出现是因为调用了Object.wait() 或者 Thread.join()或者LockSupport.park ()。处于该状态下的线程在等待另一个线程执行一些其余 action 来将其唤醒。
-
TIMED_WAITING 该状态和上一个状态其实是一样的,是不过其等待的时间是明确的。
-
TERMINATED :消亡状态比较容易理解,那就是线程执行结束了,run() 方法执行结束表示线程处于消亡状态了。
6.线程死锁是什么?
死锁是最常见的一种线程活性故障。死锁的起因是多个线程之间相互等待对方而被永远暂停(处于非 Runnable)。死锁的产生必须满足如下四个必要条件:
- 资源互斥: 一个资源每次只能被一个线程使用
- 请求与保持条件: 一个线程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:线程已经获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系
创建新线程
创建线程可分为四种方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 使用线程池创建
下面重点介绍前两种:
1. 继承Thread类
首先创建一个类继承Thread类,重写run()方法,将所要完成的任务代码写进run()方法中
public class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("MyThread" + i);
}
}
}
然后通过thread对象的start()来启动任务
public class Demo {
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
for(int i=0; i<10; i++){
System.out.println("MainThread"+i);
}
}
}
输出如下:
MyThread0
MyThread1
MainThread0
MyThread2
MainThread1
MyThread3
MainThread2
MyThread4
MainThread3
MyThread5
MainThread4
MyThread6
MainThread5
MyThread7
MainThread6
MyThread8
MainThread7
MyThread9
MainThread8
MainThread9
每次的输出不一样,因为Java采用的是抢占式调度,我们不能确保哪个线程优先完成。
2. 实现Runable接口
首先创建一个类并实现Runnable接口,重写run()方法,将所要完成的任务代码写进run()方法中
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("MyThread" + i);
}
}
}
然后创建实现Runnable接口的类的对象,将该对象当做Thread类的构造方法中的参数传进去,使用Thread类的构造方法创建一个对象,并调用start()方法即可运行该线程
public class Demo {
public static void main(String[] args) {
MyRunnable m = new MyRunnable();
Thread t = new Thread(m);
t.start();
for(int i=0; i<10; i++){
System.out.println("MainThread"+i);
}
}
}
输出如下:
MainThread0
MyThreadByRunnable0
MainThread1
MyThreadByRunnable1
MyThreadByRunnable2
MainThread2
MainThread3
MainThread4
MainThread5
MainThread6
MainThread7
MainThread8
MyThreadByRunnable3
MainThread9
MyThreadByRunnable4
MyThreadByRunnable5
MyThreadByRunnable6
MyThreadByRunnable7
MyThreadByRunnable8
MyThreadByRunnable9
3. 实现Callable接口
Callable使用步骤
- 编写类实现Callable接口,实现call方法 class XXX implements
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T; }
}
- 创建FutureTask对象,并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
- 通过Thread,启动线程
new Thread(future).start();
4. 实现Runable接口和 继承Thread类相比的优势
- 通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程执行相同任务
- 可以避免单继承所带来的局限性
- 任务与线程本身是分离的,提高了程序的健壮性
- 大部分线程池技术,接受Runnable类型的任务,不接受Thread类型的线程
5. 实现Runable接口和 实现Callable接口的区别
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
不同点:
- Runnable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runnable的run()不能抛出
Callable获取返回值:Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
同步锁
这部分为大家讲解synchronized隐式锁(包括同步代码块和同步方法),显式锁ReentrantLock。
隐式锁:具体锁的实现方法我们不关注,只需要写上相应的格式,Java会自动实现加锁和解锁。 显式锁:需要我们手动创建并且手动解锁。
我们一般来使用锁来解决线程不安全的问题。
线程不安全就是不提供加锁机制保护,也就是说多个线程同时访问一个数据,它们看到的数据和得到的数据不一致,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
1. 同步代码块
同步代码块就是在代码块加关键字synchronized,然后被同步的代码块一次只能有一个线程进入,同时锁对象打上标识,其他线程等待,当该线程完成代码块中的操作时,锁对象释放锁,其他线程再进行争抢,从而实现排队的操作。
格式:
synchronized(锁对象){
}
2. 同步方法
同步方法即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如:
public synchronized void save(){}
3. 显式锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try{
}finally{
lock.unlock();
}
try {
lock.tryLock(10, TimeUnit.SECONDS);
try {
}finally {
lock.unlock();
}
} catch (InterruptedException e1) {
}
try {
lock.lockInterruptibly();
try {
}finally {
lock.unlock();
}
} catch (InterruptedException e) {
}
我们需要将unlock()放在finally块里,因为显式锁不像隐式锁那样会自动释放,使用显式锁一定要在finally块中手动释放,如果获取锁后由于异常的原因没有释放锁,那么这把锁将永远得不到释放!将unlock()放在finally块中,保证无论发生什么都能够正常释放。
4. 显式锁的优势
- 可给锁加个等待时间超时时间,超时还未获得锁就放弃,不至于无限等下去;
- 可以中断的方式获取锁,这样外部线程给我们发一个中断信号就能唤起等待锁的线程;
- 可为锁维持多个等待队列,比如一个生产者队列,一个消费者队列,一边提高锁的效率。
ReentrantLock的字面意思是可重入锁,可重入的意思是线程可以同时多次请求同一把锁,而不会自己导致自己死锁。下面是内置锁和显式锁的区别:
-
可定时:RenentrantLock.tryLock(long timeout,TimeUnit unit)提供了一种以定时结束等待的方式,如果线程在指定的时间内没有获得锁,该方法就会返回false并结束线程等待。
-
可中断:你一定见过InterruptedException,很多跟多线程相关的方法会抛出该异常,这个异常并不是一个缺陷导致的负担,而是一种必须,或者说是一件好事。可中断性给我们提供了一种让线程提前结束的方式(而不是非得等到线程执行结束),这对于要取消耗时的任务非常有用。对于内置锁,线程拿不到内置锁就会一直等待,除了获取锁没有其他办法能够让其结束等待。RenentrantLock.lockInterruptibly()给我们提供了一种以中断结束等待的方式。
-
条件队列(condition queue):线程在获取锁之后,可能会由于等待某个条件发生而进入等待状态(内置锁通过Object.wait()方法,显式锁通过Condition.await()方法),进入等待状态的线程会挂起并自动释放锁,这些线程会被放入到条件队列当中。synchronized对应的只有一个条件队列,而ReentrantLock可以有多个条件队列,多个队列有什么好处呢?请往下看。
-
条件谓词:线程在获取锁之后,有时候还需要等待某个条件满足才能做事情,比如生产者需要等到“缓存不满”才能往队列里放入消息,而消费者需要等到“缓存非空”才能从队列里取出消息。这些条件被称作条件谓词,线程需要先获取锁,然后判断条件谓词是否满足,如果不满足就不往下执行,相应的线程就会放弃执行权并自动释放锁。使用同一把锁的不同的线程可能有不同的条件谓词,如果只有一个条件队列,当某个条件谓词满足时就无法判断该唤醒条件队列里的哪一个线程;但是如果每个条件谓词都有一个单独的条件队列,当某个条件满足时我们就知道应该唤醒对应队列上的线程(内置锁通过Object.notify()或者Object.notifyAll()方法唤醒,显式锁通过Condition.signal()或者Condition.signalAll()方法唤醒)。这就是多个条件队列的好处。
-
使用内置锁时,对象本身既是一把锁又是一个条件队列;使用显式锁时,RenentrantLock的对象是锁,条件队列通过RenentrantLock.newCondition()方法获取,多次调用该方法可以得到多个条件队列。
显示锁的部分内容参考深入理解Java内置锁和显式锁
5. 公平锁和不公平锁
公平锁:所有线程排队取得锁,先来先得。 不公平锁:所有线程抢占锁,谁先抢到就是谁的。
隐式锁都是不公平锁,显式锁在默认情况下也是不公平锁,那我们如何创建一个公平锁: 代码示范:
private lock = new ReentrantLock(true);
线程池
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
线程池的优势体现如下:
- 线程池可以重复利用已创建的线程,一次创建可以执行多次任务,有效 降低线程创建和销毁所造 成的资源消耗
- 线程池技术使得请求可以 快速得到响应,节约了创建线程的时间
- 线程的创建需要占用系统内存,消耗系统资源,使用线程池可以更好的管理线程,做到 统一分配、调优和监控线程,提高系统的稳定性。
Java中的四种线程池
- 缓存线程池 CachedThreadPool( )
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在,则创建线程 并放入线程池,然后使用
作用:
- 核心线程池大小为 0,最大线程池大小不受限,来一个创建一个线程
- 适合用来执行大量耗时较短且提交频率较高的任务 (短期异步,低负载的系统)
- 定长线程池 CachedThreadPool( )
执行流程:
- 判断线程池是否存在空闲线程,存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池,然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
- 单线程线程池 CachedThreadPool( )
执行流程:
- 判断线程池 的那个线程 是否空闲
- 空闲则使用
- 不空闲,则等待 池中的单个线程空闲后 使用执行流程:
作用:便于实现单(多)生产者-消费者模式
- 周期性任务定长线程池CachedThreadPool( ) 执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
周期性任务执行时:定时执行,当某个时机触发时,自动执行某任务
作用:定时使用的线程池,适用于定时任务。 (编辑:北几岛)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|