文章目录
一、线程和进程
1.1. 什么是进程和线程
进程 :进程是受操作系统管理的基本运行单元;进程是操作系统的结构基础;他是系统进程资源分配和调度的一个独立单元。
线程 :线程可以理解为进程独立运行的子任务;线程是异步的,被调度的时机是随机的。线程是CPU调度的最小单元
1.2. 线程的优缺点
优点 :在进程内创建、终止线程比创建、终止进程要快;同一进程内的线程间切换比进程间的切换要快,尤其是用户级线程间的切换;程序的运行效率可能会提高。
缺点 :如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换;更多的线程需要更多的内存空间;线程中止需要考虑对程序运行的影响;通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。
二、串行、并行和并发
2.1. 三者的含义
串行: 执行一个任务,在执行一个任务
并发: 在单核CPU中,轮流(时间片)处理多个任务,但是同一时刻,只有一个线程在执行。
并行: 在多核CPU中(多核CPU也会出现并发的情况),同时处理多个任务,在同一时间内,有个多线程在执行。
2.2. 计算机和多线程
单核CPU: 对于单核来说实现多线程主要是依赖操作系统内核的进程调度算法(也可以说是线程),例如有三个线程运行,操作系统会让单核CPU轮流运行这些线程,每个线程执行一个固定的时间片,但是由于CPU切换频率太,在我们的认知上认为这三个线程是同时运行的。但这其中由于操作系统需要频繁的切换线程,处理时间可能比串行的时候花费时间长,这就会出现多线程导致执行效率,但是从另一方面确实减少了用户响应时间。
多核CPU :对于多核CPU来说,虽然进程是操作系统进行资源分配和调度的一个独立单元,但是进程中包含的一系列的线程,线程是CPU调度和分配的基本单位。现在我们的笔记本是4核8线程/8核16线程(这里的4核、8核表示的是真正和物理核心,8线程、16线程是通过超线程技术,用一个物理核模拟两个虚拟核,但是操作系统看来就是8核心/16核心,通过超线程技术可以实现单个物理核实现线程级别的并行计算,性能和真实两核差距还是比较大的),这时会把线程1234分配到线程1234,其他的线程现在就需要等待分配;这个时候对于1234线程就是并行的,但是其他的线程就会先出现并发的情况。
详细解析文章链接: 多CPU/多核/多进程/多线程/并发/并行之间的关系
2.3. 多核CPU与内存
为了提高程序运行的性能,现代CPU在很多方面会对程序进行优化。CPU的处理速度是很快的,内存的速度次之,硬盘速度最慢。在cpu处理内存数据中,内存运行速度太慢,就会拖累cpu的速度。为了解决这样的问题,cpu设计了多级缓存策略。
CPU分为三级缓存: 每个CPU都有L1,L2缓存,但是L3缓存是多核公用的。
L1 Cache: CPU第一层高速缓存,分为数据缓存和指令缓存。它是封装在CPU芯片内部的高速缓存,用于暂时存储CPU运算时的部分指令和数据,存取速度与CPU主频相近。内置的L1高速缓存的容量和结构对CPU的性能影响较大,一级缓存容量越大,则CPU处理速度就会越快,对应的CPU价格也就越高。
L2 Cache: CPU外部的高速缓存,由于L1高速缓存的容量限制,为了再次提高CPU的运算速度,在CPU外部放置一高速存储器,即二级缓存。像一级缓存一样,二级缓存越大,则CPU处理速度就越快,整台计算机性能也就越好。一级缓存和二级缓存都位于CPU和内存之间,用于缓解高速CPU与慢速内存速度匹配问题。
L3 Cache: 它的作用是进一步降低内存延迟,同时提升大数据量计算时处理器的性能。具有较大L3缓存的处理器,能提供更有效的文件系统缓存行为及较短的消息和队列长度。一般多核共享一个L3缓存。
CPU查找数据的顺序为: CPU -> L1 -> L2 -> L3 -> 内存 -> 硬盘
推荐一个写的不错的博客文章:多线程之CPU多核缓存架构与内存屏障
2.3. 总结
多核CPU对于内存控制,我们还需要了解缓冲一致性协议,CPU性能优化的运行时指令重排序以及内存屏障。而Java的JMM内存模型就是建立在此基础上。
三、创建线程
创建线程有两种方式,一种是通过继承Thread类,重写run方法;另一种是实现Runnable接口,实现Run方法。
3.1. 第一种
class ThreadChile extends Thread {
@Override
public void run() {
super.run();
System.out.println("子线程 :" + Thread.currentThread().getName());
}
}
public class Thread1 {
public static void main(String[] args) {
ThreadChile chile = new ThreadChile();
chile.start();
System.out.println("主线程 :" + Thread.currentThread().getName());
}
}
3.2. 第二种(推荐)
class ThreadChild2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程 . " + Thread.currentThread().getName() + " 输出 " + i);
}
}
}
/**
* @author long
*/
public class Thread3 {
public static void main(String[] args) {
Thread child2 = new Thread(new ThreadChild2());
child2.setName("Thread-child");
child2.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程 . " + Thread.currentThread().getName() + " 输出 " + i);
}
}
}
实现Runnable接口所具有的优势:
-
避免Java单继承的问题
-
适合多线程处理同一资源
-
代码可以被多线程共享,数据独立,很容易实现资源共享
-
线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
3.3. 其他
我们也可以通过lambda和匿名内部类创建线程,这是上面的两种方式的变形:
第一个:lambda创建线程
public class Thread4 {
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程 . " + Thread.currentThread().getName() + " 输出 " + i);
}
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程 . " + Thread.currentThread().getName() + " 输出 " + i);
}
}
}
第二个:匿名内部类
public class Thread5 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("基于子类的方式 => 子线程 . " + Thread.currentThread().getName() + " 输出 " + i);
}
}
}).start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("基于接口的实现 => 子线程 . " + Thread.currentThread().getName() + " 输出 " + i);
}
}
}.start();
}
}
3.4. 衍生方式
下面介绍的方式,本质上还是上面两种的衍生版本,真正创建线程方式只有两种!但是我们会发现不管是继承Thread类还是实现Runnable接口,都有两个问题:
-
第一个是无法抛出更多的异常;
-
第二个是线程执行完毕之后并无法获得线程的返回值
第一种方法:实现Callable接口,并结合Future实现。
-
定义一个Callable的实现类,并实现call方法,这个call方法是带返回值的
-
接着通过FutureTask的构造方法,把这个Callable实现类传进去
-
然后TutureTask作为Thread类target,创建Thread线程对象
-
通过FutureTask的get方法获取线程的执行结果
/**
* @author yuelong
*/
public class ThreadCall implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "......");
Thread.sleep(1000);
return new Random().nextInt(100);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new ThreadCall());
new Thread(task).start();
Integer integer = task.get();
System.out.println(integer);
}
}
第二种是通过线程池创建线程,此处JDK自带的Exectors来创建线程池对象。
这里需要定义一个Runnable接口的实现类,接着创建固定数量的线程池,最后通过ExecutorSerivce对象execute方法传入线程对象执行。
/**
* @author yuelong
*/
public class ThreadPool implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行......");
}
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
newFixedThreadPool.execute(new ThreadPool());
}
newFixedThreadPool.shutdown();
}
}
3.5. run和start方法的区别
我们先看一个start
函数的源码:这个方法是Thread
类的方法,用来异步启动一个线程,然后主动立即返回。该启动的线程不会马上运行,会放到等待队列中等待CPU
调度,只有线程真正被CPU
调度用run()
方法执行。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
// 真正启动线程
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
接着run方法的源码:这个方式是实现Runnable的run方法,
@Override
public void run() {
if (target != null) {
target.run();
}
}
总结来说:调用start
方法方可启动线程,而run
方法只是thread
的一个普通方法调用,还是在主线程里执行。
3.6. 总结
创建线程只有两种方法:实现Runnable和继承Thread类重写run方法。这两个方法最终都会调用Thread.start方法,而start方法最终会调用run方法。所以本质上来说创建线程的方法只有一种,就是构建一个Thread类。
四、线程的生命周期
线程一共有五种状态:新建(New
)、就绪(Runnable
)、运行(Running
)、阻塞(Bolocked
)和死亡(Dead
)。
4.1. 线程状态转换
4.2. 状态详解
新建(New):代码通过new Thread()创建一个线程之后,该线程就处于新建状态,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。不会执行线程的线程执行体。
就绪(Runnable):线程对象调用start()方法后,该线程就处于就绪状态。但是线程并没有开始运行,只是表示可以运行,线程何时运行取决于JVM中线程调度其的调度。
运行(Running):处于就绪状态的线程获得CPU时间片,开始执行run方法的线程执行体,则该线程处于运行状态,这里需要注意线程只能从就绪状态进入到运行状态。
阻塞(Boloked):线程因为某种原因放弃了CPU的使用权,暂时停止运行,知道线程进入就绪状态,才有机会转到运行状态。
死亡(Dead):当run或者call方法执行完成,线程正常结束;线程抛出一个未捕获的Exception或者Error;直接调用该线程的stop()方法来结束该线程,但是容易造成死锁(11已经删除该方法)
五、线程常用方法
5.1. 静态方法
方法名 | 备注 |
---|---|
activeCount | 返回当前执行的线程所在的线程组中活动的线程数目 |
currentThread | 返回当前正在执行的线程 |
holdsLock | 返回当前执行的线程是否持有指定对象的锁 |
interrupted | 返回当前执行的线程是否已经被中断 |
sleep | 使当前执行的线程睡眠多少毫秒 |
yieId | 使当前执行的线程自愿暂时放弃对处理器的使用权并允许其他线程执行 |
5.2. 实例方法
函数名 | 备注 |
---|---|
getId | 返回该线程的Id |
getName | 返回线程的名字 |
getPriority | 返回该线程的优先级 |
getState | 返回该线程状态 |
interrupt | 使线程中断 |
isInterrupted | 返回该线程是否被中断 |
isAlive | 返回该线程是否处于活动状态 |
isDaemon | 返回该线程是否为守护线程 |
join | 等待该线程终止 |
start | 使该线程开始执行 |
toString | 返回线程的信息 |
setDaemon | 将该线程标记为守护线程或者用户线程 |
setName | 设置线程的名字 |
setPriority | 改变线程的优先级 |
六、守护线程
6.1. 守护线程和用户线程
在Java
中通常有两种线程:用户线程和守护线程(也被称为服务线程)。
通常情况下,我们使用Thread创建的线程在默认情况下都属于用户线程。
通过Thread.setDaemon(false)
设置为用户线程,通过Thread.setDaemon(true)
设置为守护线程线程。
属性的设置要在线程启动之前,否则会报IllegalThreadStateException
异常。
6.2. 守护线程特点
-
程序中的所有的用户线程结束之后,不管守护线程处于什么状态,
java
虚拟机都会自动退出; -
调用线程的实例方法
setDaemon()
来设置线程是否是守护线程; -
setDaemon()
方法必须在线程的start()
方法之前调用,在后面调用会报异常,并且不起效; -
在守护线程中启动的子线程也是守护线程。
6.3. 守护线程的使用场景
针对于守护线程的特点,java 守护线程通常可用于开发一些为其它用户线程服务的功能。比如说心跳检测,事件监听等。Java 中最有名的守护进程当属GC(垃圾回收)
七、线程优先级
Java 中的线程优先级的范围是1~10,默认的优先级是5。10级最高。
在一个线程中开启另外一个新线程,则新开线程称为该线程的子线程,子线程初始优先级与父线程相同。
线程的优先级是为了在多线程环境中便于系统对线程的调度,优先级越高先执行机会越大,并不是一定先执行。