基础概念
什么是进程和线程
- 进程是操作系统进行资源分配的最小单位。资源包括CPU、内存空间、磁盘IO等。
- 线程是 CPU 调度的最小单位,必须依赖于进程而存在。
CPU 核心数和线程数的关系
- 多核心:也指单芯片多处理器( Chip Multiprocessors,简称 CMP),CMP 是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的 SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个 CPU 同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理。
- 多线程:Simultaneous Multithreading.简称 SMT.让同一个处理器上的多个线程同步执行并共享处理器的执行资源。
- CPU的核心数、线程数:目前主流CPU都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系,也就是说四核 CPU 一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程数形成 1:2 的关系。
CPU 时间片轮转机制
什么是CPU时间片轮转机制
我们平时在开发的时候,感觉并没有受 cpu 核心数的限制,哪怕是在单核 CPU 上,想启动线程就启动线程,为什么?
这是因为操作系统提供了一种CPU时间片轮转机制。
时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RR 调度。
每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
百度百科对CPU时间片轮转机制的原理解释
如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。
如果进程在时间片结束前阻塞或结束,则 CPU 当即进行切换。
调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。
时间片长度
时间片轮转调度中唯一有趣的一点是时间片的长度:从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切换( processwitch),有时称为上下文切换( context switch),需要 5ms,再假设时间片设为 20ms,则在做完 20ms 有用的工作之后,CPU 将花费 5ms 来进行进程切换。CPU 时间的 20%被浪费在了管理开销上了。
为了提高 CPU 效率,我们可以将时间片设为 5000ms。这时浪费的时间只有 0.1%。但考虑到在一个分时系统中,如果有 10 个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待 5s 才获得运行机会。多数用户无法忍受一条简短命令要5s才能做出响应,同样的问题在一台支持多道程序的个人计算机上也会发生。
如何设置时间片长度
时间片设得太短会导致过多的进程切换,降低了 CPU 效率。
而设得太长又可能引起对短的交互请求的响应变差。
将时间片设为 100ms 通常 是一个比较合理的折衷。
并行和并发
- 并发:指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到“同时执行效果”,其实并不是的,只是计算机的速度太快,我们无法察觉到而已。
- 并行:指应用能够同时执行不同的任务。比如吃饭的时候可以边吃饭边打电话,,这两件事情可以同时执行。
两者区别:并发是交替执行,并行是同时执行。
JAVA线程
JAVA线程的开启
- X extends Thread,然后 X.start。
- X implements Runnable,然后交给Thread运行。
Thread 和 Runnable 的区别
- Thread 才是 Java 里对线程的唯一抽象
- Runnable 只是对任务(业务逻辑) 的抽象
JAVA线程的中止
线程自然终止
run 执行完成了,或者抛出了一个未处理的异常导致线程提前结束。
suspend()、resume() 和 stop()
suspend()、resume() 和 stop()分别对应线程的暂停、恢复和停止操作。但是这些 API 是过期的,也就是不建议使用的。
比如suspend(),在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。
中断 interrupt()
安全的中止则是其他线程通过调用某个线程 A 的 interrupt()方法对其进行中断操作。中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。 因为 java 里的线程是协作式的,不是抢占式的。
线程通过检查自身的中断标志位是否被置为 true 来进行响应。线程通过方法 isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过 Thread.interrupted() 会同时将中断标识位改写为 false。
如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。
我们举个例子来理解interrupt()、 isInterrupted() 和 Thread.interrupted()。
声明一个线程,用isInterrupted()方法进行while循环,再打印循环前后的interrupt状态。
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 循环前 , interrupt = " + isInterrupted());
// 测试isInterrupted()方法
while (!isInterrupted()) {
System.out.println(threadName + " 正在循环 , interrupt = " + isInterrupted());
}
System.out.println(threadName + " 循环后 , interrupt = " + isInterrupted());
}
}
我们先运行这个线程,100毫秒后把interrupt设置为true,观察日志。
@Test
public void main() {
MyThread myThread = new MyThread("MyThread");
// 先运行线程
myThread.start();
// 100毫秒后把标志位interrupt设置为true
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
myThread.interrupt();
}
日志如下
MyThread 循环前 , interrupt = false
MyThread 正在循环 , interrupt = false
MyThread 正在循环 , interrupt = false
MyThread 正在循环 , interrupt = false
MyThread 正在循环 , interrupt = false
...
MyThread 正在循环 , interrupt = false
MyThread 正在循环 , interrupt = false
MyThread 循环后 , interrupt = true
我们发现,结束循环后,interrupt 为true。
如果我们把while循环的判断换成Thread.interrupted()。
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 循环前 , interrupt = " + isInterrupted());
// 测试Thread.interrupted()
while (!Thread.interrupted()) {
System.out.println(threadName + " 正在循环 , interrupt = " + isInterrupted());
}
System.out.println(threadName + " 循环后 , interrupt = " + isInterrupted());
}
}
日志如下
MyThread 循环前 , interrupt = false
MyThread 正在循环 , interrupt = false
MyThread 正在循环 , interrupt = false
MyThread 正在循环 , interrupt = false
MyThread 正在循环 , interrupt = false
...
MyThread 正在循环 , interrupt = false
MyThread 正在循环 , interrupt = false
MyThread 循环后 , interrupt = false
结束循环后,interrupt 为 false。
通过以上例子,我们得知:如果Thread.interrupted()为true,会清除标志位interrupt,把它设置为false。isInterrupted()方法则不会清除。
为什么建议使用interrupt而不是自己创建的一个boolean标志位?
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
boolean cancel = false;
public void setCancel(boolean cancel) {
this.cancel = cancel;
}
@Override
public void run() {
while (cancel) {
// 阻塞方法
Thread.sleep(2000);
wait();
}
}
}
MyThread myThread = new MyThread("MyThread");
// 先运行线程
myThread.start();
// 100毫秒后把标志位interrupt设置为true
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
myThread.setCancel(true);
当调用阻塞方法的时候,系统是不会检测标志位cancel的,不会响应中断。
while (isInterrupted()) {
// 阻塞方法
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
而阻塞方法会抛出中断异常InterruptedException,当发生中断的时候,可以马上感知到。
需要注意的是,抛出InterruptedException 以后,会自动把interrupt设置为false,所以下面代码的打印结果为false。
为什么要这样设计呢?
当我们从sleep方法被唤醒后,如果直接中断线程,就像直接调用stop(),没有释放资源,可能造成死锁。所以我们需要做释放资源的工作,再手动interrupt()一次,才退出循环。
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
while (isInterrupted()) {
// 阻塞方法
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
// 打印结果 interrupt = false
System.out.println(" interrupt = "+isInterrupted());
// 资源释放工作
...
// 再手动interrupt()一次,才能退出循环
interrupt();
}
}
}
}
Runnable如何使用中断
interrupt是Thread的内容,Runnable没有,所以需要先获取当前线程,再使用interrupt。
public class UserRunnable implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
...
}
}
}
线程常用方法和线程的状态
状态 | 说明 |
---|---|
新建状态 | new出一个Thread |
就绪状态 | 可运行状态 |
阻塞状态 | 当前线程被操作系统挂起,不分配资源 |
方法 | 说明 |
---|---|
start方法 | 新建一个Thread,调用start()方法之后,并不是进入运行状态,而是进入就绪状态,等待CPU分配时间片 |
join方法 | 线程调用join()方法或者CPU分配了时间片,可以获得执行权,进入运行状态 |
yield方法 | 线程调用yield()方法或者CPU分配的时间片到期了,会让出执行权,让操作系统重新分配时间片 |
run方法 | run()方法跑完了,事情做完了,线程自然死亡了 |
stop方法 | 线程调用stop()方法强制关闭,导致线程死亡 |
setDeamon方法 | 调用setDeamon()方法把当前线程设置为守护线程。当进程的所有非守护线程执行完,守护线程也死亡了 |
sleep方法 | 线程调用sleep()方法,进入阻塞状态。当sleep的时间到了,线程从睡眠中被唤醒了,不是马上进入运行状态,而是就绪状态 |
wait方法 | 线程调用wait()方法,进入阻塞状态。当被别的线程notify()通知,线程同样进入就绪状态 |
为什么调用interrupt()方法不是进入死亡状态呢?
因为interrupt()只是设置标志位。
理解 run()和 start()
Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread() 其实只是 new 出一个 Thread 的实例,还没有操作系统中真正的线程挂起钩来。 只有执行了 start()方法后,才实现了真正意义上的启动线程。
- start()方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现 的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常。
- 而run()方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方 法并没有任何区别,可以重复执行,也可以被单独调用。
yield()方法
使当前线程让出 CPU 占有权,但让出的时间是不可设定的,也不会释放锁。
并不是每个线程都需要释放锁,而且执行 yield( )的线程不一定就会持有锁,我们完全可以在释放锁后再调用 yield 方法。
所有执行 yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
join()方法
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。
比如在线程 B 中调用了线程 A 的 join()方法,线程B会被挂起,直到线程 A 执行完毕后,才会继续执行线程 B。(此处为常见面试考点)
守护线程
守护(Daemon)线程是一种支持型线程,主要被用作程序中后台调度以及支持性工作。
我们新建的线程叫做用户线程,默认是非守护线程。
系统启动的线程叫做系统线程,默认是守护线程。
当一个进程中不存在非守护线程的时候,进程将会退出,所有守护线程也跟着终结了。
我们可以调用 Thread.setDaemon(true)将线程设置为守护线程。
垃圾回收线程就是守护线程。
守护线程被用作完成支持性工作,但是在 Java 虚拟机退出时,守护线程中的 finally 块并不一定会执行。
在构建守护线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。
synchronized
概念
Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用。
synchronized主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
/**
* 用在同步块上
*/
public void incCount() {
synchronized (this) {
count++;
}
}
/**
* 用在方法上
*/
public synchronized void incCount2() {
count++;
}
上面两种都是对象锁,锁住的都是当前对象。
对象锁和类锁
- 对象锁是用于对象实例方法或者一个对象实例上的。
- 类锁是用于类的静态方法或者一个类的 class 对象上的。
类的对象实例可以有很多个,所以不同对象实例的对象锁是互不干扰的。
每个类只有一个 class 对象,所以每个类只有一个类锁,类锁其实锁的是每个类的对应的 class 对象。
类锁和对象锁之间也是互不干扰的。
错误的加锁
我们用synchronized 锁住一个Integer ,然后i++。
private static class Worker implements Runnable{
private Integer i;
private Object o = new Object();
public Worker(Integer i) {
this.i=i;
}
@Override
public void run() {
synchronized (i) {
Thread thread=Thread.currentThread();
i++;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName()+"-------"+i+"--@"
+System.identityHashCode(i));
}
}
}
这个Runnable交给5个线程运行。
public static void main(String[] args) throws InterruptedException {
Worker worker=new Worker(1);
//Thread.sleep(50);
for(int i=0;i<5;i++) {
new Thread(worker).start();
}
}
运行结果如下:
看样子,这五个线程锁住的不是同一个Integer,为什么?
我们反编译这个类的 class 文件后,可以看到 i++实际是
我们再看Integer的valueOf方法
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
原来i++本质上是返回了一个新的 Integer 对象。
也就是每个线程实际加锁的是不同 的 Integer 对象。
Volatile
Volatile 保证了不同线程对这个变量进行操作时的可见性。
Volatile 使一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
以下例子是一个死循环。
需要用Volatile修饰ready变量,线程才能正常结束。
/**
* 类说明:演示Volatile的提供的可见性
*/
public class VolatileCase {
private static boolean ready;
private static int number;
private static class PrintThread extends Thread{
@Override
public void run() {
System.out.println("PrintThread is running.......");
while(!ready);
System.out.println("number = "+number);
}
}
public static void main(String[] args) {
new PrintThread().start();
SleepTools.second(1);
number = 51;
ready = true;
SleepTools.second(5);
System.out.println("main is ended!");
}
享学课堂