1.线程概述
大佬地址:
并发与并行的区别:
- 1.1线程相关概念
- 1.1.1进程
- 1.1.2线程
- 1.1.3主线程与子线程
- 1.1.4串行、并发与并行
- 1.2线程的创建与启动
- 1.2.1继承Thread类
- 1.2.2实现Runnable接口
- 1.2.3使用匿名内部类
- 1.2.4实现Callable接口
- 1.3线程的常用方法
- 1.3.1 currentThread()方法
- 1.3.2 setName()、getName()
- 1.3.3 isAlive()
- 1.3.4 sleep()
- 1.3.5 getId()
- 1.3.6 yield()
- 1.3.7 setPriority()
- 1.3.8 interrupt()
- 1.3.9 setDaemon()
- 1.3.10 join()
- 1.4 线程的生命周期
- 1.5 多线程编程的优势与存在的风险
1.1线程相关概念
1.1.1进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。
可以把进程简单的理解为在操作系统中运行的一个程序。
1.1.2线程
线程(thread)是进程的一个执行单元,一个线程就是进程中一个单一顺序的控制流,进程的一个执行分支。
进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程。
在操作系统中是以进程为单位分配资源,如虚拟存储空间、文件描述符等。每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储。
1.1.3主线程与子线程
JVM 启动时会创建一个主线程,该主线程负责执行 main 方法,主线程就是运行 main 方法的线程。
Java 中的线程不是孤立的,线程之间存在一些联系,如果在 A 线程中创建了 B 线程,则称 B 线程为 A 线程的子线程,相应的 A 线程就是 B 线程的父线程。
1.1.4串行、并发与并行
并发可以提高对事物的处理效率,即在一段时间内可以处理或者完成更多的事情。
并发是一种现象:同时运行多个程序或多个任务需要被处理的现象
这些任务可能是并行执行的,也可能是串行执行的,和CPU核心数无关,是操作系统进程调度和CPU上下文切换达到的结果
解决大并发的一个思路是将大任务分解成多个小任务:
可能要使用一些数据结构来避免切分成多个小任务带来的问题
可以多进程/多线程并行的方式去执行这些小任务达到高效率
或者以单进程/单线程配合多路复用执行这些小任务来达到高效率
举个??:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
所以我认为它们最关键的点就是:是否是『同时』
作者:知乎用户
链接:https://www.zhihu.com/question/33515481/answer/58849148
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
并行是一种更为严格,理想的并发。
1.2线程的创建与启动
在 Java 中,创建一个线程就是创建一个 Thread 类(子类)的对象(实例) 。
Thread 类有两个常用 的构造方法:
Thread()
Thread(Runnable target)
对应的创建线程的两种方式:
定义 Thread 类的子类
定义一个 Runnable 接口的实现类
这两种创建线程的方式没有本质的区别。
1.2.1继承Thread类
public class ThreadTest extends Thread {
/*
* 继承Thread类,重写run()方法
* run()方法体中就是子线程执行的任务
* */
@Override
public void run() {
System.out.println("子线程执行打印内容。。。");
}
}
public class MainThread {
public static void main(String[] args) {
System.out.println("主线程main()方法执行。。。");
// 创建子线程对象
ThreadTest threadTest = new ThreadTest();
/*
* 启动子线程
*
* 调用线程的start()方法来启动线程,启动线程的实质就是请求JVM运行相应的线程,
* 这个线程具体在什么时候运行由线程调度器(Scheduler)决定
*
* 注意:
* 1.start()方法调用结束并不意味着子线程开始运行
* 2.新开启的线程会执行run()方法
* 3.如果开启了多个线程,start()调用顺序并不一定就是线程启动的顺序
* 4.多线程运行结果与代码执行顺序或调用顺序无关
* */
threadTest.start();
System.out.println("主线程中其他逻辑内容。。。");
}
}
运行结果:
1.2.2实现Runnable接口
/**
* 实现Runnable接口,重写run()方法
*/
public class RunnableTest implements Runnable {
@Override
public void run() {
// 方法体中,子线程执行的任务内容
System.out.println("subThread implements Runnable...");
}
}
public class MyRunnable {
public static void main(String[] args) {
// 方式1:创建Runnable实现类对象
RunnableTest runnableTest = new RunnableTest();
// 创建线程
Thread thread = new Thread(runnableTest);
// 启动线程
thread.start();
// 方式2:使用Thread(Runnable target)构造方法,通过匿名内部类实现
Thread anonymity = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("anonymity thread...");
}
});
// 启动线程
anonymity.start();
System.out.println("main thread...");
}
}
运行结果:
上面两个哪个好?
Thread类继承存在单继承的局限性,而接口不会体现数据共享的概念(JMM内存模型图),代码可以被多个线程共享,代码和数据独立。
Runnable实现线程可以对线程进行复用,因为runnable是轻量级对象,而Thread不行,它是重量级对象
1.2.3使用匿名内部类
public static void main(String[] args) {
// 使用匿名内部类方式创建Runnable实例
Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("输出"+i);
}
}
});
t1.start();
// lambda 表达式简化语法
Thread t2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
System.out.println("输出"+i);
}
});
t2.start();
}
1.2.4实现Callable接口
可以得到执行结果
public class A3Callable {
public static void main(String[] args) {
//FutureTask包装我们的任务,FutureTask可以用于获取执行结果
FutureTask ft = new FutureTask<>(new MyCallable()); /?fju?t??(r)/ /tɑ?sk
//创建线程执行线程任务
Thread thread = new Thread(ft);
thread.start();
try {
//得到线程的执行结果
Integer num = ft.get();
System.out.println("得到线程处理结果:" + num);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
// 实现Callable接口,实现带返回值的任务
static class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
int num = 0;
for (int i = 0; i < 1000; i++) {
System.out.println("输出"+i);
num += i;
}
return num;
}
}
}
1.3线程的常用方法
方法名称 | 说明 |
---|---|
start() | 启动线程 |
getId() | 获取当前线程ID Thread-编号 该编号从0开始 |
getName() | 获取当前线程名称 |
stop() | 停止线程(已废弃) |
getPriority(); | 返回线程的优先级 |
boolean isAlive() | 测试线程是否处于活动状态 |
isDaemon(): | 测试线程是否为守护线程 |
isInterrupted(); | 测试线程是否已经中断 |
interrupt(); | 设置当前线程为终止状态 |
Thread.currentThread() | 获取当前线程对象 |
Thread.state getState() | 获取线程的状态 |
构造函数 | 说明 |
---|---|
Thread() | 分配一个新的 Thread 对象 |
Thread(String name) | 分配一个新的 Thread对象,具有指定的 name正如其名。 |
Thread(Runable runnable) | 分配一个新的 Thread对象 |
1.3.1 currentThread()方法
Thread.currentThread()方法可以获得当前线程,Java 中的任何一段代码都是执行在某个线程当中的。执行当前代
码的线程就是当前线程。
同一段代码可能被不同的线程执行,因此当前线程是相对的,Thread.currentThread()方法的返回值是在代码实际运行时候的线程对象。
public class CurrentThreadTest extends Thread {
public CurrentThreadTest() {
System.out.println("subConstructor == "+Thread.currentThread().getName());
}
@Override
public void run() {
System.out.println("subThread == " + Thread.currentThread().getName());
}
}
public class CurrentTest {
public static void main(String[] args) {
System.out.println("当前主线程:" + Thread.currentThread().getName());
// 创建子线程实例
CurrentThreadTest threadTest = new CurrentThreadTest();
// 启动线程
threadTest.start();
}
}
运行结果:
1.3.2 setName()、getName()
设置线程名称
thread.setName("线程名称")
返回线程名称
thread.getName()
通过设置线程名称,有助于程序调试,提高程序的可读性,建议为每个线程都设置一个能够体现线程功能的名称。
1.3.3 isAlive()
判断当前线程是否处于活动状态
thread.isAlive()
活动状态就是线程已启动并且尚未终止
1.3.4 sleep()
让当前线程休眠指定的毫秒数,当前线程进入阻塞状态,sleep()不会释放锁,wait()会释放锁
/*
该方法会使当前线程进入阻塞状态指定毫秒,当阻塞指定毫秒后,当前线程会重写进入Runnable状态,等待分配时间片
*/
static void sleep(long ms)
当前线程是指 Thread.currentThread()返回的线程
public class SubThread4 extends Thread {
@Override
public void run() {
try {
long before = System.currentTimeMillis();
System.out.println("run, threadname=" + Thread.currentThread().getName() + " ," +
"begin= " + before);
// 当前线程睡眠 2000 毫秒
Thread.sleep(2000);
System.out.println("run, threadname=" + Thread.currentThread().getName()
+ " ,end= " + System.currentTimeMillis()
+ ", 共执行总时长:" + (System.currentTimeMillis() - before));
} catch (InterruptedException e) {
/*
* 在子线程的 run 方法中, 如果有受检异常(编译时异常)需要处理,只有选择捕获处理,不能抛出处理
* 因为父类中 run 方法没有抛出异常
* */
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) {
SubThread4 t4 = new SubThread4();
System.out.println("main__begin = " + System.currentTimeMillis());
t4.start(); //开启新的线程
System.out.println("main__end = " + System.currentTimeMillis());
}
}
运行结果:
1.3.5 getId()
可以获得线程的唯一标识
thread.getId()
注意:
某个编号的线程运行结束后,该编号可能被后续创建的线程使用,重启的 JVM 后,同一个线程的编号可能不一样。
1.3.6 yield()
线程让步,放弃当前的 CPU 资源
// 该方法用于使当前线程主动让出当次CPU时间片,等待重新分配时间片
static void yield()
1.3.7 setPriority()
设置线程的优先级
void setPriority(int priority)
线程的切换是由线程调度控制的,我们无法通过代码来干涉,但是我们通过提高线程的优先级来最大程度的改善线程获取时间片的几率。
线程的优先级被划分为10级,值分别为1 ~ 10,其中1最低,10最高;线程提供了3个常量来表示最低、最高,以及默认优先级:
Thread.MIN_PRIORITY 1
Thread.MAX_PRIORITY 10
Thread.NORM_PRIORITY 5
java 线程的优先级取值范围是 1 ~ 10,如果超出这个范围会抛出异常 IllegalArgumentException。
在操作系统中,优先级较高的线程获得 CPU 的资源越多,线程优先级本质上是只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程。
注意:
不能保证优先级高的线程先运行
Java 优先级设置不当或者滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿。
线程的优先级并不是设置的越高越好,一般情况下使用普通的优先级即可,即在开发时不必设置线程的优先级。
线程的优先级具有继承性,在 A 线程中创建了 B 线程,则 B 线程的优先级与 A 线程是一样的。
1.3.8 interrupt()
中断线程
thread.interrupt()
注意调用 interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程。
// 判断线程的中断标志,线程有isInterrupted()方法,该方法返回线程的中断标志
thread.isInterrupted()
/**
* 子线程
*/
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i <= 300; i++) {
System.out.println("子线程执行===" + i);
}
}
}
/**
* 主线程测试
*/
public class MainThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 启动子线程
myThread.start();
for (int i = 0; i <= 300; i++) {
System.out.println("主线程执行===" + i);
}
// 中断子线程
myThread.interrupt();
}
}
实际结果:
/**
* 子线程
*/
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println("子线程执行===" + i);
// 判断线程的中断标志
if (this.isInterrupted()) {
System.out.println("子线程中断执行...");
// 中断循环, run()方法体执行完毕,子线程运行完毕
break;
}
}
}
}
根据线程中断标志运行结果:
1.3.9 setDaemon()
Java 中的线程分为用户线程与守护线程。守护线程是为其他线程提供服务的线程,如 垃圾回收器(GC)就是一个典型的守护线程。
守护线程不能单独运行,当 JVM 中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM 会退出。
设置守护线程的代码要放在启动线程之前,否则会抛出一个IllegalThreadStateException
异常。不能把正在运行的常规线程设置为守护线程。
thread.setDaemon(true);
thread.start();
/**
* 守护线程
*/
public class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("守护线程执行===");
}
}
}
/**
* 子线程
*/
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("子线程执行===" + i);
}
}
}
public class MainThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setDaemon(true);
myThread.start();
for (int i = 0; i <= 100; i++) {
System.out.println("主线程执行===" + i);
}
MyThread2 myThread2 = new MyThread2();
myThread2.start();
}
}
执行结果:
主线程执行结束后,守护线程依然在执行,子线程执行完成后,守护线程才结束。这里想验证了,是不是哪个线程创建了守护线程,当这个线程结束后守护线程就不再执行,这里得到的结果是,只要有线程在执行,守护线程就会一直执行,直到没有线程执行,守护线程才会终止执行。验证了:守护线程不能单独运行,当 JVM 中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM 会退出。
1.3.10 join()
线程调用了join方法,那么就要一直运行到该线程运行结束,才会运行其他进程,这样可以控制线程执行顺序。
void join();
void join(long millis);
void join(long millis, int nanos);
1.4 线程的生命周期
线程的生命周期是线程对象的生老病死,即线程的状态。
线程生命周期可以通过 getState()方法获得,线程的状态是Thread.State 枚举类型定义的,有以下几种:
public class Thread implements Runnable {
public static enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
private State() {
}
}
}
NEW
新建状态,创建了线程对象,线程还没有开始运行,此时线程处在新建状态,调用 start()启动之前的状态;
RUNNABLE
可运行状态,它是一个复合状态,包 含:READY 和 RUNNING 两个状态。
READY 状态(就绪状态)
该线程可以被线程调度器进行调度使它处于 RUNNING 状态;
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由运行时系统的线程调度程序(thread scheduler)来调度的。
RUNING 状态(运行状态)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。
Thread.yield()方法可以把线程由 RUNNING 状态转换为 READY 状态
BLOCKED
阻塞状态,线程发起阻塞的 I/O 操作,或者申请由其他线程占用的独占资源,线程会转换为 BLOCKED 阻塞状态;
处于阻塞状态的线程不会占用 CPU 资源,当阻塞 I/O 操作执行完,或者线程获得了申请的资源,线程可以转换为 RUNNABLE;
线程运行过程中,可能由于各种原因进入阻塞状态:
1> 线程通过调用sleep()方法进入睡眠状态;
2> 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
3> 线程试图得到一个锁,而该锁正被其他线程持有;
4> 线程在等待某个触发条件;
WAITING
等待状态,线程执行了 Object.wait()、thread.join() 方法会把线程转换为 WAITING 等待状态;
执行 Object.notify()方法,或者加入的线程执行完毕,当前线程会转换为 RUNNABLE 状态
TIMED_WAITING
与 WAITING 状态类似,都是等待状态,区别在于处于该状态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,该线程自动转换为 RUNNABLE;
TERMINATED
终止状态,线程结束处于终止状态。
有两个原因会导致线程死亡:
1)run方法正常退出而自然死亡;
2)一个未捕获的异常终止了run()方法而使线程终止;
1.5 多线程编程的优势与存在的风险
多线程编程具有以下优势:
1.提高系统的吞吐率,多线程编程可以使一个进程有多个并发,即同时进行的操作
2.提高响应性,Web 服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间
3.充分利用多核处理器资源,通过多线程可以充分的利用 CPU 资源的优势
多线程的常见应用场景:
1.后台任务,例如:定时向大量(100w以上)的用户发送邮件;
2.异步处理,例如:统计结果、记录日志、发送短信等;
3.分布式计算、分片下载、断点续传
多线程编程存在的问题与风险:
1.线程安全问题,多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读取脏数据(过期的数据),如丢失数据更新。
2.线程活性问题,由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非 RUNNABLE 状态,这就是线程活性问题,
常见的活性故障有以下几种:
(1) 死锁(Deadlock)
(2) 锁死(Lockout)
(3) 活锁(Livelock)
(4) 饥饿(Starvation)
3.上下文切换,处理器从执行一个线程切换到执行另外一个线程
4.可靠性,可能会由一个线程导致 JVM 意外终止,其他的线程也无法执行