CPU 核心数和线程数的关系
目前的 CPU 有双核,四核,八核,一般情况下,它和线程数是1:1的对应关系,也就是四核 CPU 一般就能并行执行 4 个线程。但 Intel 引入超线程技术后,使核心数与线程数形成1:2的关系,也就是我们常说的
4核8线程
线程调度与线程调度模型
任意时刻,只有一个线程占用 CPU,处于运行状态。而多线程并发执行就是轮流获取 CPU 执行权。
- 分时调用模型
轮流获取 CPU 执行权,均分 CPU 执行时间。
- 抢占式调度模型
优先级高的线程优先获取 CPU 执行权,这也是
JVM
采用的线程调度模型。
进程与线程
进程是程序运行资源分配
的最小单位。
这些资源就包括
CPU
,内存空间,磁盘 IO 等。同一进程中的多个线程共享该进程的所有资源,而不同进程是彼此独立的。举个栗子,在手机开启一个 APP 实际上就是开启了一个进程了,而每一个 APP 程序会有很多线程在跑,例如刷新 UI 的线程等,所以说进程是包含线程的。
线程是 CPU 调度
的最小单位,必须依赖于进程而存在。
线程是比进程更小的,能独立运行的基本单位,每一个线程都有一个程序计数器,虚拟机栈等,它可以与同一进程下的其它线程共享该进程的资源。
并行与并发
- 并行
指应用能够同时执行不同的任务。例如多辆汽车可以同时在同一条公路上的不同车道上并行通行。
- 并发
指应用能够交替执行不同的任务,因为一般的计算机只有一个 CPU 也就是只有一颗心,如果一个 CPU 要运行多个进程,那就需要使用到并发技术了,例如时间片轮转进程调度算。比如单 CPU 核心下执行多线程任务时并非同时执行多个任务,而是以一个非常短的时间不断地切换执行不同的任务,这个时间是我们无法察觉的出来的。
两者的区别:并行是同时执行,并发是交替执行。
高并发编程的意义
- 充分利用 CPU 资源
线程是 CPU 调度的最小的单位,我们的程序是跑在 CPU 的一个核中的某一个线程中的,如果在程序中只有一个线程,那么对于双核心4线程的CPU来说就要浪费了 3/4 的 CPU 性能了,所以在创建线程的时候需要合理的利用 CPU 资源,具体可以看看 AsyncTask 内部的线程池是如何设计的。
- 加快响应用户的时间
如果多个任务时串行执行的话,那么效果肯定不好,在移动端开发中,并发执行多个任务是很常见的操作,最常见的就是多线程下载了。
- 可以使代码模块化,异步化,简单化
在 Android 应用程序开发中的,一般 UI 线程负责去更新界面相关的工作,而一些 IO,网络等操作一般会放在异步的工作线程去执行,这样使得不同的线程各司其职,异步化。
线程之间的安全性问题
问题1:同一进程间的多个线程是可以共享该进程的资源的,当多个线程访问共享变量时,就会线程安全问题。
问题2:为了解决线程之间的同步问题,一般会引入锁机制,对于线程之间抢夺锁时也是有可能造成死锁问题。
问题3:在 JVM 内存模型中,每一个线程都会分配一个虚拟机栈,这个虚拟机栈是需要占用内存空间的,如果无限制的创建线程的话,会耗尽系统的内存。
线程的开启与关闭
线程的启动
- 派生 Thread 类
//开启一个线程
Thread thread = new Thread() {
@Override
public void run() {
super.run();
System.out.println("thread started");
}
};
thread.start();
- 实现 Runnable 接口,将其交给 Thread 类去执行
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable run invoked");
}
});
thread.start();
- 实现 Callable 接口
因为 Thread 构造中只接收 Runnable 类型的接口,需要实现将 Callable 的实现类包装为 FutureTask 之后交给 Thread 类去执行。
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(1500);
return "work done!";
}
};
FutureTask<String> futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
//get() 是一个阻塞式的操作,一直等待 call 方法执行完毕。
String resule = futureTask.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
再来看 FutureTask 的应用:我们观察到 AsyncTask 内部就使用到了 FutureTask ,因为在 doInBackground() 需要有一个返回值,而恰好 Callable 就可以实现子线程执行完有返回值。使用 FutureTask 来封装 WorkRunnable 对象,然后再交给对应的线程池去执行。具体的代码如下:
总结:对于第1,2两种方式是在线程执行完毕后,无法得到执行的结果,而第三种方式是可以获取执行结果的。
线程的终止
- 线程自然终止
也就是 run 执行完毕,或者是内部出现一个异常导致线程提前结束。
-
暴力终止
suspend() 使线程挂起,并且不会释放线程占有的资源(例如锁),resume() 使挂起的线程恢复。
stop() 暴力停止,立刻释放锁,导致共享资源可能不同步。
以上几个方法已经被 JDK 标记为废弃状态。
- interrupt() 安全终止
第一种情况:如果线程处于正常运行状态,那么线程的中断状态会被置为 true ,并且线程还是会正常执行,仅此而已。
第二种情况:如果当前线程如果是处于阻塞状态,例如调用了 wait,join,sleep 等方法,那么则会抛出InterruptedException
异常。
总结: interrupt() 并不能中断线程,需要线程配合才能实现中断操作。
示例01
public class EndThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("isInterrupted:"+isInterrupted());
while (true) {//尽管在其他线程中调用了 interrupt() 方法,但是线程并不会终止
//while (!isInterrupted()){
System.out.println("I am Thread body");
}
// System.out.println("isInterrupted:"+isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
final EndThread endThread = new EndThread();
endThread.start();
Thread.sleep(10);
//在其他线程中去调用线程的interrupt方法给线程打一个终止的标记
endThread.interrupt();
}
}
上面的代码时在线程体中执行一个 while(true)的死循环,然后在其他线程中调用 endThread.interrupt()
观察当前线程是否会执行完毕。
经测试:在调用线程的 interrupt()方法之后,while(true)是不会结束循环的,也就是线程还是一直在运行着。所以说 interrupt() 并不会应用 run 方法的执行
示例02
下面再来看看另一个关于 interrupt 方法的使用
在线程体内部 sleep(2000) 并且 try catch 对应的 InterruptedException 异常,如果在其他线程调用了 endThread.interrupt() 那么此处就会抛出 InterruptedException 异常,并且会isInterrupted() 会返回 false 。
public class EndThread2 extends Thread {
@Override
public void run() {
super.run();
System.out.println("isInterrupted:" + isInterrupted());
try {
//在其他线程调用 endThread.interrupt() 之后,会抛出 InterruptedException 异常并且线程的
// isInterrupted 会被标记为 false。因此最后输出的结果还是 false
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("catch InterruptedException");
}
System.out.println("isInterrupted:" + isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
final EndThread2 endThread = new EndThread2();
endThread.start();
Thread.sleep(300);
//在其他线程中去调用线程的interrupt方法给线程打一个终止的标记
endThread.interrupt();
}
}
执行结果:
isInterrupted:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at endthread.EndThread.run(EndThread.java:28)
catch InterruptedException
isInterrupted:false
示例03
将示例01
中的 while(true) 修改为 while(!isInterrupted()){},在外界调用了 endThread.interrupt()
之后,线程的 isInterrupted() 就会返回 true,标记着你可以结束线程了。
public class EndThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("isInterrupted:"+isInterrupted());
while (!isInterrupted()){
System.out.println("I am Thread body");
}
System.out.println("isInterrupted:"+isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
final EndThread endThread = new EndThread();
endThread.start();
Thread.sleep(10);
//在其他线程中去调用线程的interrupt方法给线程打一个终止的标记
endThread.interrupt();
)
}
}
示例04
还有一种方式是设置一个 boolean
类型的变量 mIsExit
标记,当线程体内部判断到 mIsExit
为 false 那么就跳出循环
。具体示例代码如下:
public class EndThread extends Thread {
//这个变量需要在其他线程中判断,因此需要设置为线程可见的
private volatile boolean mIsExit = true;
@Override
public void run() {
super.run();
while(mIsExit){
System.out.println("I am Thread body");
}
}
public static void main(String[] args) throws InterruptedException {
final EndThread endThread = new EndThread();
endThread.start();
Thread.sleep(3);
//设置标记为退出状态
endThread.mIsExit = false;
}
}
线程其他 API
Thread#start() 与 Thread#run()
start() 方法调用之后会让一个线程进入就绪等待队列
,当获取到 CPU 执行权之后会执行线程体 run()方法。
run() 方法只是 Thread 类中一个普通方法
,如果手动去调用,跟调用普通方法没有什么区别。
Thread#run() 与 Runnable#run()
在 Java 中只有 Thread 才能表示一个线程,而 Runnable 只能表示一个任务
,任务是需要交给线程去执行的,当出现如下代码时,你看到可能会懵逼,执行结果到底是什么?
//接受一个 runnable 接口
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable run invoked");
}
}) {
@Override
public void run() {
super.run();
System.out.println("thread started");
}
};
thread.start();
以上方式的输出结果是:
如果线程 run 方法内部调用了 super.run() 那么输出结果如下:
runnable run invoked
thread started
如果线程 run 方法内部不调用 super.run() 那么输出结果如下:
thread started
我们可以通过源码来解答这个问题:在创建线程时,如果往构造函数中传入一个
Runnable
对象,那么它会给线程target
属性赋值,并且在线程体
执行时先判断target
是否为空,不为空,则先执行Runnable
的run
方法,再执行当前线程体的子类中的 run 方法。
//Thread.java
private Runnable target;
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
init(group, target, name, stackSize);
}
@Override
public void run() {
if (target != null) {
//如果传入的 Runnable 实例,那么会调用调用 runnable 实例的 run 方法
target.run();
}
}
yield()
Java线程中的 Thread.yield()
方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它会放弃自己CPU执行权,让
自己或者
其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。
yield()的作用是
让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用
yield()`之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
public class YieldDemo {
public static void main(String[] args) {
Yieldtask yieldtask = new Yieldtask();
Thread t1 = new Thread(yieldtask, "A");
Thread t2 = new Thread(yieldtask, "B");
Thread t3 = new Thread(yieldtask, "C");
Thread t4 = new Thread(yieldtask, "D");
t1.start();
t2.start();
t3.start();
t4.start();
}
public static class Yieldtask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "-" + i);
if (i == 5) {
Thread.yield();
}
}
}
}
}
从运行结果可以看到,调用 yield()
方法的线程之后,CPU 执行权不一定会给其他线程
抢到,有可能还是当前线程
抢到 CPU 执行权。
...
A-0
D-4
D-5
D-6//在这里,还是执行 D 这个线程
D-7
D-8
D-9
B-2
B-3
B-4
B-5
C-6
B-6
A-1
A-2
A-3
A-4
...
join()
join() 把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
这里举一个栗子:午饭时间到了,老王(线程A)调用了微波炉热饭的方法,预约了4分钟,当微波炉跑了2分钟,这时老王看到一个美女(线程B)过来,这时主动调用了线程B.join()方法,此时把微波炉让给了美女,这时老王就等待美女热饭,直到热好美女的饭之后,才轮到老王去继续热饭,这就是一个 join()
方法的作用。
下面画了一个草图:
public class JoinDemo implements Runnable {
public static void main(String[] args) throws InterruptedException, ExecutionException {
JoinDemo joinDemo = new JoinDemo();
Thread thread = new Thread(joinDemo);
thread.start();
//join() 会阻塞当前线程,等待子线程执行完毕
//在这里主线程会等待子线程执行完毕之后才能往下执行。
thread.join();
System.out.println(Thread.currentThread().getName() + " " + " done!");
}
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+" done!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
Thread-0 done!
main done!
线程状态
在 Java Thread 类中定义了一个 State 枚举,内部定义以下6个线程状态。
在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(TIME_WAITING,WAITING)和死亡(TERMINATED)五种状态。
我这里绘制一个草图,描述了各个状态之前的切换:
- 新建状态(NEW)
新建一个线程对象,此时并没有执行 start() 方法,这时的线程状态就是处于新建状态。
Thread thread = new Thread(){
public void run() {...}
};
- 就绪状态(RUNNABLE)
start() 方法调用之后会让一个线程进入就绪等待队列,JVM 会为其创建对应的虚拟机栈,本地方法栈和程序计数器。处于这个状态的线程还没开始运行,需要等待 CPU 的执行权。
- 运行状态(RUNNING)
处于就绪状态的线程在抢到 CPU 执行权之后,就处于运行状态,执行该线程的 run 方法。
- 阻塞状态(BLOCKED)
TIME_WAIT/WAIT:
运行的线程执行 wait() /wait(long),join()/join(long)方法,那么这些线程放弃 CPU 执行和线程持有的锁,并且 JVM 会将这些线程放入到
等待池
中。
BLOCKED:
运行时的线程在获取同步锁时,发现锁被其他线程持有,这时 JVM 会将当前线程放入
锁池
中。
- 结束(TERMINATED)
线程
run
方法执行完毕,或者线程被异常终止。
总结
以上是关于 Java 多线程的一些基本知识点的总结,有任何不对的地方,请多多指正。
记录于2019年4月7日