本篇内容比较基础,适合初学者阅读。
线程与进程
从操作系统层面来说,进程是应用程序的动态执行过程,进程代表着一个程序的一次执行。进程同样也是操作系统资源管理和调度的基本单位。线程是对进程的进一步划分,一个进程可以有多个线程。线程是程序执行的最小单位。
区别和关系:
- 进程是操作系统分配资源和调度的最小单位,线程是程序执行的最小单位
- 进程由一个或多个线程组成,线程对应程序的一条执行路径
- 进程与进程之间独立,每个进程有自己的内存空间
- 多个线程共享进程的空间
- 线程上下文切换比进程更快
java中线程与进程
当运行一个java程序时,系统会产生一个java进程。了解jvm的一定知道,每个java进程拥有独立的内存区域,包括堆、方法区、虚拟机栈等。
java进程中的线程共享堆等区域外,拥有属于自己的虚拟机栈。也就是说java中的线程除了共享进程内存空间外,还有自己的独立空间。
使用多线程的方法
1、继承Thread,重写run方法
class MyThread extends Thread{
@Override
public void run(){
// 线程执行代码
}
}
复制代码
通过继承Thread类并重写run方法来使用线程。编写好类后,只需要创建该类的实例对象,再调用start()方法即可启动线程。
MyThread t = new MyThread();
t.start();
复制代码
2、实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run(){
// 线程执行代码
}
}
复制代码
Thread t = new Thread(new MyRunnable());
t.start();
复制代码
通过实现Runnable接口以及重写run()方法使用线程。
因为Runnable接口只有一个方法,并且有注解@FunctionalInterface,所以可以用lambda表达式简化。
Thread t = new Thread(()->{
// 线程执行代码
});
t.start();
复制代码
3、实现Callable接口
Runnable接口的run方法没有返回值,这在一些场景并不适用。所以在线程有数据返回式可以使用Callable创建线程。
FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 线程执行
return 2;
}
});
Thread t = new Thread(task);
t.start();
// 线程执行结束前在此阻塞
System.out.println(task.get());
复制代码
通过实现Callable接口并创建FutureTask对象可以创建带有返回值的线程。
**注意:**在线程结束前调用FutureTask的get方法会导致调用者阻塞,直到线程结束并返回结果。
4、使用线程池
因为创建线程和释放线程都有一定的开销,而且过多的线程会降低代码可读性导致难以管理。线程池能够很好地解决这些问题。
一个线程池中能够维持多个线程,向线程池提交一个任务后,线程池会自动分配线程执行。这些线程不会因为任务完成而消亡,这样就避免了频繁创建和销毁线程,同时由于所有线程都集中在一个池中,这样也更利于管理。
创建线程池
// 单个线程 线程池,只有一个线程
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 固定大小线程池,参数规定了线程数量
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
// 缓存线程池,没有核心线程,每次创建新线程执行任务
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
复制代码
这里暂时只介绍这三种最简单的创建方式,很明显常用的应该是FixedThreadPool。
在阿里巴巴的java编码规范中有提到尽量不使用Executors类创建线程,而是使用ThreadPoolExecutor创建。这里暂时不介绍ThreadPoolExecutor,它的详细介绍以及各种线程池的详细介绍将在另一篇文章展示。
提交任务
// Runnable
fixedThreadPool.submit(new Runnable() {
@Override
public void run() {
// 线程执行代码
}
});
// Callable
Future<Integer> future = fixedThreadPool.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 线程执行代码
return null;
}
});
System.out.println(future.get());
复制代码
与直接创建线程类似,向线程池提交任务也分为有返回和无返回两种。分别实现Callable和Runnable接口即可。
使用Callable的时候要注意,submit方法会返回一个Future对象,通过这个Future对象的get方法可以获得到线程的返回结果。不过与FutureTask相同,如果线程没有执行完成,会导致get的调用者阻塞。
线程的状态
如上图所示,java的线程有6种状态,分别是新建(NEW)、可执行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、计时等待(TIMED_WAITING)、终止(TERMINATED)。
接下来将介绍每种状态,以及状态之间的转换。
新建 NEW
新建状态是线程一开始的状态,在new Thread后线程将进入该状态。
可执行 RUNNABLE
该状态表示线程可以执行,即CPU可以调度该线程。在调用thread的start方法后线程会进入该状态。
其实RUNNABLE状态包含了两种状态,RUNNING和READY。RUNNING状态表示线程正在执行,READY状态表示线程可执行,在等待CPU调度。
这两种状态的转换如下(*):
- READY->RUNNING:CPU调度该线程,该线程获得时间片开始执行
- RUNNING->READY:线程的时间片结束或者自己调用了yield方法放弃时间片
阻塞 BLOCKED
阻塞状态时线程竞争锁失败后进入的状态,出现在synchronized竞争锁失败的时候。
等待 WAITING
等待状态与阻塞状态不同,阻塞是竞争锁失败导致的,而等待是主动进入的。比如调用了wait()、park()、join()等使线程停止的方法。
WAITING 与 BLOCKED的区别
- BLOCKED由锁竞争失败导致,WAITING往往是主动调用相关方法导致
- BLOCKED状态将在锁被释放后结束,WAITING往往需要一些特定操作,比如notify()、notifyAll()、unpark()等
计时等待 TIMED_WAITING
计时等待是在WAITING的基础上加入了超时机制。它除了通过notify等方式退出外还可以在计时到一定时间时退出。
常见的TIMED_WAITING有sleep()、wait(long timeout)、park(long timeout)、join(long timeout)等
终止 TERMINATED
当线程的run方法执行完成,或者主线程结束时,线程会进入终止状态。
线程状态转换
常见面试题
**Q:**sleep和wait的区别
- 调用方式不同:sleep使用Thread类调用,wait使用锁对象在synchronized代码块中调用。
- 作用效果不同:sleep方法使线程进入等待状态,但是不会释放已获得的锁。wait使线程等待,同时会释放锁。
- 唤醒方式不同:sleep在超时后会自动唤醒,wait需要持有锁的线程用notify或notifyAll唤醒。
- 状态不同:sleep使线程进入TIMED_WAITING状态,无参数的wait方法使线程进入WAITING状态。