区别:进程是 资源分配 的基本单位; 线程是 程序执行 的基本单位
线程调度:指按照特定机制为多个线程分配CPU的使用权。
线程调度分类:
线程的状态包括 新建状态,运行状态,阻塞等待状态和消亡状态。其中阻塞等待状态又分为 BLOCKED, WAITING 和 TIMED_WAITING 状态。
NEW:这是属于一个已经创建的线程,但是还没有调用 start 方法启动的线程所处的状态。
RUNNABLE:总体上就是当我们创建线程并且启动之后,就属于 Runnable 状态。
BLOCKED:当线程准备进入 synchronized同步块或同步方法的时候,需要申请一个监视器锁而进行的等 待,会使线程进入BLOCKED 状态
WAITING :该状态的出现是因为调用了Object.wait() 或者 Thread.join()或者LockSupport.park ()。处于该状态下的线程在等待另一个线程执行一些其余 action 来将其唤醒。
TIMED_WAITING 该状态和上一个状态其实是一样的,是不过其等待的时间是明确的。
TERMINATED :消亡状态比较容易理解,那就是线程执行结束了,run() 方法执行结束表示线程处于消亡状态了。
死锁是最常见的一种线程活性故障。死锁的起因是多个线程之间相互等待对方而被永远暂停(处于非 Runnable)。死锁的产生必须满足如下四个必要条件:
创建线程可分为四种方式:
下面重点介绍前两种:
首先创建一个类继承Thread类,重写run()方法,将所要完成的任务代码写进run()方法中
public class MyThread extends Thread{
/**
* run 方法就是线程要执行的方法
*/
@Override
public void run() {
//这里的代码 就是一条新的执行路径
//这个执行路径的触发方式,不是调用run方法,而是通过thread对象的start()来启动任务
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采用的是抢占式调度,我们不能确保哪个线程优先完成。
首先创建一个类并实现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) {
//实现Runnable
//1. 创建一个任务对象
MyRunnable m = new MyRunnable();
//2. 创建一个线程,并为其分配一个任务
Thread t = new Thread(m);
//3. 执行这个线程
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
Callable使用步骤
//编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T; }
}
FutureTask<Integer> future = new FutureTask<>(callable);
new Thread(future).start();
相同点:
不同点:
Callable获取返回值:Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
这部分为大家讲解synchronized隐式锁(包括同步代码块和同步方法),显式锁ReentrantLock。
隐式锁:具体锁的实现方法我们不关注,只需要写上相应的格式,Java会自动实现加锁和解锁。
显式锁:需要我们手动创建并且手动解锁。
我们一般来使用锁来解决线程不安全的问题。
线程不安全就是不提供加锁机制保护,也就是说多个线程同时访问一个数据,它们看到的数据和得到的数据不一致,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
同步代码块就是在代码块加关键字synchronized,然后被同步的代码块一次只能有一个线程进入,同时锁对象打上标识,其他线程等待,当该线程完成代码块中的操作时,锁对象释放锁,其他线程再进行争抢,从而实现排队的操作。
格式:
synchronized(锁对象){
//方法体
}
同步方法即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如:
public synchronized void save(){
}
//当synchronized作用于普通方法是,锁对象是this;
//当synchronized作用于静态方法是,锁对象是当前类的Class对象;
// 显式锁的使用示例
ReentrantLock lock = new ReentrantLock();
// 获取锁,这是跟synchronized关键字对应的用法。
lock.lock();
try{
// your code
}finally{
lock.unlock();
}
// 可定时,超过指定时间为得到锁就放弃
try {
lock.tryLock(10, TimeUnit.SECONDS);
try {
// your code
}finally {
lock.unlock();
}
} catch (InterruptedException e1) {
// exception handling
}
// 可中断,等待获取锁的过程中线程线程可被中断
try {
lock.lockInterruptibly();
try {
// your code
}finally {
lock.unlock();
}
} catch (InterruptedException e) {
// exception handling
}
我们需要将unlock()放在finally块里,因为显式锁不像隐式锁那样会自动释放,使用显式锁一定要在finally块中手动释放,如果获取锁后由于异常的原因没有释放锁,那么这把锁将永远得不到释放!将unlock()放在finally块中,保证无论发生什么都能够正常释放。
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内置锁和显式锁
公平锁:所有线程排队取得锁,先来先得。
不公平锁:所有线程抢占锁,谁先抢到就是谁的。
隐式锁都是不公平锁,显式锁在默认情况下也是不公平锁,那我们如何创建一个公平锁:
代码示范:
//显式锁:fair参数为true 就表示公平锁。
private lock = new ReentrantLock(true);
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
线程池的优势体现如下:
- 缓存线程池 CachedThreadPool( )
执行流程:
作用:
- 定长线程池 CachedThreadPool( )
执行流程:
- 单线程线程池 CachedThreadPool( )
执行流程:
作用:便于实现单(多)生产者-消费者模式
- 周期性任务定长线程池CachedThreadPool( )
执行流程:
周期性任务执行时:定时执行, 当某个时机触发时, 自动执行某任务
作用:定时使用的线程池,适用于定时任务。