进程与线程

  • 进程(资源管理最小单位):

    程序的一次执行过程,运行程序的基本单位,进程是动态的。系统执行一个程序即进程从创建,运行到死亡的过程。

    在Java中,启动main函数即启动了一个JVM的进程,main所在的线程是进程中的一个线程。

  • 线程(任务调度最小单位):

    线程是比进程更小的执行单位,一个进程在执行过程中可以产生多个线程,同类线程之间共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈、本地方法栈

    系统在产生一个线程,或在多个线程之间切换工作时,其负担比操作进程小得多,因此线程也称为轻量级进程。

线程状态

  • 新建(NEW):线程创建未启动,此时还未调用start。
  • 可运行(RUNABLE):线程处于运行状态或等待资源调度进入运行状态。
  • 阻塞(BLOCKED):线程等待获取锁。
  • 无限期等待(WAITING):执行Object.wait()或Thread.join()进入,没有主动唤醒则一直等待。
  • 限期等待(TIMED_WAITING):自动唤醒,即等待时设置了时间,sleep() 或 wait() 或 join()设置时间。
  • 死亡(TERMINATED):线程结束或异常结束。

并发与并行

  • 并发:同一时间段,可以多个任务执行,但单位时间不一定同时执行。
  • 并行:单位时间内,多个任务同时执行。

线程的生命周期和状态

  • NEW:新建,线程被构建,但还没有调用start()方法
  • RUNNABLE:可运行,一般操作系统细分为READY(就绪)和RUNNING(运行),Java中3统称位运行中
  • BLOCKED:阻塞,线程因为锁被阻塞
  • WAITING:等待,线程进入等待状态,进入该状态的线程需要依靠其他线程的通知才可返回运行状态
  • TIME_WAITING:超时等待,该状态在等待基础上增加了超时限制,超时后线程自行返回运行状态
  • TERMINATED:终止,表示线程已执行完毕

上下文切换

线程执行过程中会有自己的运行条件和状态(上下文),一般出现以下情况,线程会从占用CPU状态中退出:

  • 主动让出CPU,例如调用sleep()、wait()进入睡眠、等待状态
  • 时间片用完,操作系统防止一个进程或线程长时间占用CPU,使其他进程后线程无用。
  • 调用阻塞类型的系统中断,例如请求IO,线程被阻塞
  • 线程终止/结束运行

前三种都会发生线程切换,线程切换时要保存当前线程的上下文。当下次线程占用CPU时恢复现场,并加载下一个将占用CPU的线程上下文。这就是上下文切换

死锁

简述

多个线程循环阻塞,循环等待其他线程释放资源,而产生死锁必须具备四个条件:

  • 互斥条件:资源任何时刻只有一个线程占用,不能多个线程共用
  • 请求与保持条件:一个进程因请求资源阻塞时,对已获取资源保持不放
  • 不可剥夺条件:进程已获取的资源在未使用完之前,是不能被其他进程强行剥夺的,只可使用完后自行释放
  • 循环等待条件:若干进程之间的循环等待资源关系形成环

预防与避免线程死锁

只需破坏任意一个死锁的必要条件即可预防死锁。

  • 破坏互斥:硬件资源实现同时访问可破坏,软件资源一般无法打破
  • 破坏请求与保持:一次性申请全部的资源
  • 破坏不可剥夺:占用部分资源的进程进一步申请其他资源时,若申请不到,可主动释放自己占用的资源
  • 破坏循环等待:通过按序申请资源来预防。按规定顺序申请资源,释放时则反序执行。

避免死锁:一般可在资源分配时,通过算法(银行家算法等)对资源进行合理分配来避免死锁。

线程调度的方法

  • sleep():线程定时休眠,进入阻塞状态,不会释放锁。
  • wait():线程进入阻塞状态,释放持有的对象的锁。
  • join():在A线程中调用B线程的join(),A线程进入阻塞状态,直到B线程结束。
  • yield():线程让步,当前线程由运行状态进入就绪状态,让出资源,所有线程重新争取资源。

sleep与wait区别

  • wait()是Object方法,sleep()是Thread方法
  • sleep不释放锁,wait释放锁
  • wait没有设置时间,只能被notify()、notifyAll()主动唤醒。

synchronized

synchroized解决多线程间访问资源的同步性,它可以保证被修饰的方法或代码块在任意时刻都只有一个线程执行。

synchronized的使用

  • synchronized加到静态方法和代码块上,都是给当前类上锁
  • synchronized加到实例方法给对象实例上锁

锁升级过程

锁主要存在无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态这四个状态。它们随着竞争逐渐升级。锁可以升级但不可以降级,这种策略是为了提高获得锁和释放锁的效率。

volatile与synchronized区别

  • volatile效率比synchronized效率高,volatile只能修饰变量,synchronized可修饰方法和代码块。
  • volatile保证数据可见性,但不保证数据原子性。synchronized二者都可以保证。
  • volatile用于解决变量在多线程之间的可见性,synchronized可解决多个线程之间访问资源的同步性。
  • volatile防止指令重排。

ThreadLocal

ThreadLoacal让每一个线程绑定自己的值,在里面存储每个线程的私有数据。

每一个访问ThreadLocal变量的线程都会有这个变量的本地副本,通过get、set方法对变量进行操作,从而避免线程安全问题。

使用线程

Runnable接口(推荐)

实现Runnable接口,重写run方法,在执行类中声明一个Thread对象,并传入自定义的Runnable对象,然后Thread对象执行start方法启动线程。

1
2
3
4
5
6
7
8
9
10
11
public class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}

public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}

Callable接口(有返回值,使用FutureTask封装)

实现Callable接口,重写call方法,和Runnable不同,可以有返回值,执行线程需要先通过FutureTask封装,然后声明Thread对象传入FutureTask对象,执行start方法启动线程。

注意Thread对象参数是Runnable型,而FutureTask是继承Runnable的,所以通过Callable实现线程,需要FutureTask封装。

FutureTask的get方法可以取出Callable的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 250;
}
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}

Thread类(实现Runnable接口)

Thread类实现了Runnable接口,我们也是通过重写run方法。然后直接声明自定义的Thread对象,执行它的start方法就可以启动线程。

1
2
3
4
5
6
7
8
9
10
11
public class MyThread extends Thread {
@Override
public void run() {
// ...
}
}

public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}

实现与继承区别

Java特性多实现,单继承,肯定是实现接口更好,且继承整个Thread类开销过大。

线程池(ThreadPoolExecutor)

FixedThreadPool

该线程池中线程数始终不变,当有一个新任务提交时,若线程池有空闲线程就立刻执行,没有则暂存到一个任务队列中,等到有空闲线程再执行任务。

SingleThreadExecutor

只有一个线程的线程池,若一个多余任务被提交,则保存到一个任务队列,等到有空闲线程时,队列按先进先出执行任务。

CaheedThreadPool

可根据情况调整线程数量的线程池,线程池数量不确定。若全部线程都在工作,此时又有新任务提交,则会创建一个新线程来处理任务,所有线程在执行完当前线程后会返回线程池等待复用。