注 :示例图片来源于网络
1. 线程的基本概念
- 进程
一个程序启动,Linux系统会给程序分配一个进程,并且分配给进程一些内存资源,启动一个jar包,就会创建这个jar包的进程;线程可以看做进程的一个顺序执行的指令流,一个进程可以创建多个线程,同一个进程创建的线程共享进程的内存资源,不同进程创建的线程是相互隔离的。
进程是系统资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
- 线程
线程可以看做一段顺序的指令序列,每个线程都有自己独立的堆栈和局部变量,多个线程之间共享父进程的地址空间和资源;创建线程的资源开销少,线程间的切换损耗少,所以java中采用多线程形式实现并发。
线程是CPU调度的基本单位,依附于进程。
- java 并发编程
java实现的并发编程,即多个线程可以同时执行工作,提高cpu的使用效率,减少任务执行的总耗时;但多线程编程,也会带来缓存不一致,操作顺序难以控制的线程安全问题。
根据运行环境CPU的不同,可以分为单核CPU并发(时间片轮转)、多核CPU并行两种。
- 多线程的应用场景
web服务器多个线程处理请求、后台任务、异步处理等场景。
2. 如何创建一个线程
java中主要通过下面三种方式创建一个线程:
注:无论通过哪一种方式创建一个线程,java中都是使用Runnable接口的run()方法做线程的执行体,最后都需要new一个Thread对象来创建线程。
- 继承Thread类
java创建一个线程是通过新建一个Thread对象实现的,继承Thread类创建线程,会存在java单继承的局限性。
public class MyThread extends Thread {
public MyThread(){}
public MyThread(ThreadGroup group, String name){
super(group,name);
}
@Override
public void run() {
System.out.println("This is Mythread, name is "+ getName() + ",ThreadGroup is " + getThreadGroup());
}
}
- 实现Runnable接口
实现一个Runnable接口创建线程,重写run方法,缺点是没有返回值。
public class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("This is MyRunnable name is "+Thread.currentThread().getName());
}
}
- 实现Callable接口
实现一个call接口创建线程,需要和Future接口及相关的实现工具类FutureTask来实现一个有返回值的线程。
public class MyCallable implements Callable<String> {
@Override
public String call() {
System.out.println("This is MyCallable name is "+Thread.currentThread().getName());
return "MyCallable";
}
}
MyCallable callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<String>(callable);
new Thread(futureTask, "futureTask").start();
try {
System.out.println("拿到callable返回值:" + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
- 三者区别
1、Runnable和Callable采用接口形式执行线程任务,比直接继承Thread执行线程任务更加灵活。
2、Runnable是一个没有返回值的线程任务,Callable是有返回值的线程任务。
3、Callabe接口一般和Future接口的实现类来组合实现一个有返回值的异步编程,Future接口的实现类有:FutureTask、CompletableFuture。
3. 线程的一些关键属性
-
name:线程名称,可以重复,若没有指定会自动生成。
-
id:线程ID,一个全局唯一的正long值,创建线程时指定,终生不变,线程终结时ID可以复用。
-
priority:线程优先级,取值为1到10,线程优先级越高,执行的可能越大(跟CPU的线程调度模式有关),优先级还跟运行环境有关,若运行环境不支持优先级分10级,如只支持5级,那么设置5和设置6有可能是一样的。
线程调度模式:java中采用抢占式调度
1、分时调度模式:所有线程轮流使用CPU,平均分配每个线程占用的CPU时间片。
2、抢占式调度模式:优先让优先级高的线程使用CPU,优先级高的线程获取CPU的时间片会多一些。
-
state:线程状态,Thread.State枚举类型,有NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 6种,后续第4点详细介绍。
-
daemon:布尔值,是否为守护线程,java中线程分为两类:守护线程(true)和用户线程(false)。
1、用户线程:一般我们创建的线程都是用户线程,当JVM中的用户线程全部执行完毕,JVM也会关闭。
2、守护线程:只要JVM尚有一个非守护线程没有结束,守护线程就进行工作,守护线程随着JVM关闭而结束,典型例子:GC。
-
ThreadGroup:所属线程组,一个线程必然有所属线程组,线程组被设定为管理线程的一些属性,提供了一些管理线程组旗下线程的api方法和统一异常设置等。
线程组以树形结构存在,每个线程组都有一组子线程和子线程组。
- UncaughtExceptionHandler:未捕获异常时的处理器,默认没有,线程出现错误后会立即终止当前线程运行,并打印错误。
4 线程的状态转换
- 线程定义的状态有六种:
Thread.State | Thread中定义的几种状态 |
---|---|
NEW | 尚未启动的线程状态,即线程创建,还未调用start方法 |
RUNNABLE | 就绪状态ready(调用start,等待调度)+正在运行running |
BLOCKED | 等待监视器锁时,陷入阻塞状态 |
WAITING | 等待状态的线程正在等待另一线程执行特定的操作(如notify) |
TIMED_WAITING | 具有指定等待时间的等待状态 |
TERMINATED | 线程完成执行,终止状态 |
-
状态转化图:
-
锁池和等待池
其实线程竞争锁都是在线程处于运行态尝试获取CPU资源执行线程任务的时候发生锁的竞争。
锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中
- wait()方法和sleep()方法
wait()和sleep()区别
1.wait()来自Object类,sleep()来自Thread类
2.调用 sleep()方法,线程不会释放对象锁。而调用 wait() 方法线程会释放对象锁;
3.sleep()睡眠后不出让系统资源,wait()让其他线程可以占用 CPU;
4.sleep(millionseconds)需要指定一个睡眠时间,时间一到会自然唤醒。而wait()需要配合notify()或者notifyAll()使用
5.notify()随机释放一个线程从等待池到锁池,notifyAll()释放所有的线程从等待池到锁池。
6.wait()方法只能在持有该对象monitor对象的时候使用。
5. Thread类的一些相关方法的源码
5.1 Thread(...args[])构造函数(实际调用init()方法,构造方法不涉及到初始化逻辑,需要的话进行统一封装)
init()方法(源码省略):可以看出子线程继承父线程的优先级、是否守护线程、父线程的线程组、可继承ThreadLocal等属性。