什么是线程
在面试中,经常会问到:进程和线程的区别。在了解线程之前,首先要了解什么是进程。进程是操作系统进行资源分配和调度的基本单位,当运行一个程序时,就是开启了一个进程。
线程是进程中的一个主体,一个进程至少有一个线程(也就是主线程),进程中的多个线程可以共享进程的资源。当执行一个程序时,CPU的资源会分配给线程,所以线程是CPU进行资源分配和调度的基本单位。
CPU核心数和线程数
最早前的电脑都是单核单线程的,也就是一个核心处理器对应一个线程去工作。后来慢慢出现了多核心技术和超线程技术。
多核心技术已经很常见了,我们买电脑时都会见到双核、四核、六核处理器,每个核心处理器对应了一个线程,多个线程同时工作。
超线程技术是2002年intel发布的一种技术,每个核心提供了两个逻辑线程,也就是一个六核处理器可同时运行12个线程,效率呈几何式增长。
![](https://img.haomeiwen.com/i18883501/f70f520469149ca4.png)
可以看到这个就是使用了双核超线程技术,可同时运行4个线程。
时间片轮转机制
先来看一段代码:
public static void main(String[] args) {
new Thread(Main::threadTest, "thread-1").start();
new Thread(Main::threadTest, "thread-2").start();
}
private static void threadTest() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---- count === " + i);
}
}
执行结果:
thread-2---- count === 0
thread-1---- count === 0
thread-1---- count === 1
thread-2---- count === 1
thread-2---- count === 2
thread-1---- count === 2
thread-1---- count === 3
thread-2---- count === 3
thread-1---- count === 4
thread-2---- count === 4
thread-1---- count === 5
thread-2---- count === 5
thread-2---- count === 6
thread-1---- count === 6
thread-2---- count === 7
thread-1---- count === 7
thread-1---- count === 8
thread-2---- count === 8
thread-1---- count === 9
thread-2---- count === 9
Process finished with exit code 0
可以看到两个线程是轮流执行的,这就是时间片轮转调度,这个时间片是很短小的,当前线程时间片用完后,要让出CPU,切换到另一个线程继续执行,来回调度。
下面拿单核单线程的CPU执行过程举例 :
![](https://img.haomeiwen.com/i18883501/51143f457bc30451.jpg)
比如当前CPU执行进程二的线程一, 当时间片用完以后就会自动切换到其他线程,这个是随机的。其他线程时间片用完以后再切换到另一个线程。这样一直轮询执行,这就是线程的上下文切换
。每次进行线程切换的时候都需要进行数据的保存和恢复,涉及到数据,就一定有额外的开销。随着运行的应用越来越多,线程也越来越多,直到CPU切换不过来了。这就是系统卡顿的原因之一。这就是为什么后来有了多核心和超线程技术,我们系统应用的性能也越来越好。
程序计数器
这里简单提一下,了解过虚拟机的都应该知道程序计数器,它为什么是线程私有的呢?当一个线程时间片用完后,切换到其他线程,当再次切换到当前线程的时,怎么知道上次执行到哪里了呢?所以程序计数器的作用就体现出来了,每个线程执行时都会记录一个执行地址,当线程再次分配到时间片时通过自己私有的计数器地址继续执行。
我们经常看到有人说尽量少开线程,为什么呢?每个线程都会预留1M的桟空间,当线程越来越多,所占的内存也就会越来越多,直到桟内存溢出(Stack Overflow)。所以后来引入了线程池的概念,这个东西后续再讲 。
并行和并发
很多人都会把并行和并发的概念搞混淆了。
并行
并行是指同一个时间段内多个任务同时执行,也就是说在单位时间内多个任务同时在执行。
并发
并发是在某一个时间段内执行了多少个任务,并发的多个任务在某个单位时间内不一定同时执行。它强调的是执行的效率,高并发,高吞吐量就是指在一段时间内执行的任务越多越好。
在以前的单核时代,一般都是并发执行的,因为一个CPU只能执行一个线程。就像上面讲时间片时说的,单核时代是多个任务共享一个CPU,当一个任务占用CPU运行时,其他任务就会被挂起,当占用CPU的线程时间片用完以后,就把CPU的资源让给其他任务。
高并发编程
随着多核超线程技术的到来,相当于更多的人分担了我们更多的任务,减少了上下文切换的开销。但现在对应用系统的性能和吞吐量的要求越来越高,需要处理大量的数据和请求,这就需要高并发编程来解决这个问题。
线程的创建与运行
线程创建有三种方式,但都离不开thread:
- 实现Runnable接口
- 继承Thread类,重写run方法
- 使用FutrueTask方式(Future实现了Runnable接口,所以官方API说这属于第一种创建方式)
这三种方式本质上都要重写一个run方法,当调用Thread实例的start()方法后,线程并不会立即执行,它实际上是进入了就绪状态,等CPU给它分配完资源后才会真正运行,当run方法执行完毕,该线程处于终止状态。
三种方式各有利弊,使用时可根据具体情况
1. 实现Runnable方式
class RunnableTest implements Runnable {
@Override
public void run() {
System.out.println("runnable方式");
}
}
RunnableTest test = new RunnableTest();
new Thread(test).start();
new Thread(test).start();
这种方式的好处是多个线程可共用一个runnable, 如果需要,也可以给runnable添加参数进行区分。Runnbale
也可以继承其他的类,复用比较灵活。
2. 继承Thread类
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread类");
}
}
MyThread myThread = new MyThread();
myThread.start();
这种方式如果想要获取当前线程直接使用this就可以了,不需要再使用Thread.currentThread()。它与Runnable的区别是不可以再继承其他的类,无法进行扩展和复用。
3. 使用FutureTask对象
public static class CallerTask implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("future 方式");
Thread.sleep(5000);
return "future task";
}
}
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
new Thread(futrueTask).start();
try {
String result = futureTask.get();
System.out.println("return result === " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 最终会打印
// future 方式
// return result === future task
前面两种方式都有一个共同的缺点,那就是任务没有返回值,如果我们需要有返回值,那么就可以使用这种方式。
这种方式创建稍微比较复杂:
- 实现Callable的call方法,并赋予方法返回值
- 创建FutureTask对象,将caller作为参数传进来
- 创建Thread,传入FutureTask
可以看到,FutureTask就是实现了Runnable接口:
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
线程状态管理
上面我们提到了线程的就绪和运行中,在java中,这都属于线程的运行态。线程一共有6个状态(初始、运行中、等待、超时等待、阻塞、终止),我们怎么去管理和控制它们的状态呢?
- 初始状态:创建了一个Thread,也就是初始化了一个线程,但还没有调用start()方法。
-
运行状态:调用了
start()
方法,进入就绪,等到CPU分配资源(也就是时间片轮询到自己时)之后进入运行中状态。 -
等待:调用了
wait()
方法之后,线程会让出时间片,进入等待状态,等到其他线程调用了notify或notifyAll方法继续执行。 - 等待超时:wait(long millis)方法可以传入一个时间戳,标识等待超时时间,如果超时之后将自动返回此线程继续执行。
- 阻塞:如果两个线程同时抢占一把锁,没抢到的线程将进入阻塞状态。
- 终止:线程执行完毕进入终止状态。
线程终止
stop
最早的时候,线程终止调用线程的stop()方法。不过这个方法已经被标记为过时了,官方文档也有说明:
/*
@deprecated This method is inherently unsafe. Stopping a thread with
* Thread.stop causes it to unlock all of the monitors that it
* has locked (as a natural consequence of the unchecked
* <code>ThreadDeath</code> exception propagating up the stack). If
* any of the objects previously protected by these monitors were in
* an inconsistent state, the damaged objects become visible to
* other threads, potentially resulting in arbitrary behavior. Many
* uses of <code>stop</code> should be replaced by code that simply
* modifies some variable to indicate that the target thread should
* stop running. The target thread should check this variable
* regularly, and return from its run method in an orderly fashion
* if the variable indicates that it is to stop running. If the
* target thread waits for long periods (on a condition variable,
* for example), the <code>interrupt</code> method should be used to
* interrupt the wait.
* For more information, see
* <a href="{@docRoot}/../technotes/guides/concurrency/threadPrimitiveDeprecation.html">Why
* are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.
*/
大意是说这个方法本质上是不安全的,停止线程会释放它所有已锁定的监视器(监视器会在桟顶抛出ThreadDeath异常,然后被解锁)。如果之前被这些监视器保护的任何对象处于不一致状态,其它线程看到的这些对象就会处于不一致状态。这种对象被称为受损的 (damaged)。当线程在受损的对象上进行操作时,会导致不可预估的行为。ThreadDeath异常可能会在用户无感知的情况下杀掉其他线程,并且难以追踪问题的所在。
interrupt
为了解决上面的问题,官方推荐使用interrupt的方式和谐的终止掉线程。
java中线程中断标志是一种线程之间的协作模式,设置了线程中断标志并不会停止掉线程,而需要在线程中根据这个标记自行处理。Thread中有三个相关的方法:
-
void interrupt()
,将线程设置为中断标记,如果线程调用了wait()
sleep()
join()
等阻塞方法,将会抛出InterrupedException
异常 -
void isInterrupetd()
, 判断某个线程是否被中断,是返回true,否则返回false -
vodi interrupetd
,判断某个线程是否被中断,是返回true,否则返回false,与isInterrupetd
不同的是interrupetd
是一个静态方法,它只会判断当前运行线程的状态。而且如果检测出当前线程如果是中断状态,那么就会清除中断标记。
为了验证以上观点,下面举两个例子:
示例一
void test1() throws Exception {
Thread thread = new Thread(() -> {
System.out.println("thread sleep ...");
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread awaking");
});
thread.start();
Thread.sleep(2000);
thread.interrupt();
thread.join();
System.out.println("finish");
}
以上方法在线程中开始睡眠,启动线程后调用interrupt方法,最终会抛出异常:
thread sleep ...
thread awaking
java.lang.InterruptedException: sleep interrupted
finish
at java.lang.Thread.sleep(Native Method)
at thread.Interrupted$Test1.lambda$test1$0(Interrupted.java:13)
at java.lang.Thread.run(Thread.java:748)
示例二
再看以下示例:
void test2() throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("thread isInterrupted:" + Thread.currentThread().isInterrupted());
System.out.println("thread interrupted:" + Thread.interrupted());
Thread.currentThread().interrupt();
System.out.println("thread interrupted:" + Thread.interrupted());
System.out.println("thread interrupted:" + Thread.interrupted());
System.out.println("thread isInterrupted:" + Thread.currentThread().isInterrupted());
});
thread.start();
thread.join();
System.out.println("finish");
}
打印结果:
thread isInterrupted:false
thread interrupted:false
thread interrupted:true
thread interrupted:false
thread isInterrupted:false
finish
想必这个结果不会意外了吧
- 刚开始调用当前线程的
isInterrupted()
方法,返回false - 调用
Thread.interrupted()
,其内部也是获取当前线程的标志,返回false - 然后设置了中断标志:
Thread.currentThread().interrupt()
- 这时再调用
Thread.interrupted()
,就会返回true了,并清除中断标记 - 所以再次调用
Thread.interrupted()
时返回了false - 再获取当前线程的标志
isInterrupted()
,也重置为了false
通知和等待
Object是所有类的父类,它有几个默认方法,其中就包含了通知和等待。注意,以下几个方法必须要持有资源的锁才可以进行调用,否则会抛出异常IllegalMonitorStateException
。
-
wait()
,当调用了共享变量的wait方法时,当前线程被阻塞挂起。直到其他线程调用了notify()或notifyAll()方法进行通知唤醒。 -
wait(long time)
,等待超时,当前线程被阻塞挂起,如果等待超时,该线程自动被唤醒。 -
notify()
,唤醒某一个线程,如果有多个线程执行,那么只会随机唤醒一个线程。 -
notifyAll()
,通知所有的线程唤醒。
来看一下代码:
Thread thread1 = new Thread(
() -> {
synchronized (sources1) {
System.out.println("thread1 begin wait");
try {
sources1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 end wait");
}
}
);
Thread thread2 = new Thread(
() -> {
synchronized (sources1) {
System.out.println("thread2 begin wait");
try {
sources1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2 end wait");
}
}
);
Thread thread3 = new Thread(
() -> {
synchronized (sources1) {
System.out.println("thread3 begin wait");
try {
sources1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread3 end wait");
}
}
);
Thread thread4 = new Thread(
() -> {
synchronized (sources1) {
System.out.println("thread4 release lock");
// 唤醒全部的锁
sources1.notifyAll();
// 随机唤醒一个。
// sources1.notify();
}
}
);
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(1000);
thread4.start();
thread1.join();
thread2.join();
thread3.join();
thread4.join();
System.out.println("thread finish");
我们创建了4个线程,前3个线程进入等待状态,第4个线程调用notifyAll进行唤醒,打印结果:
thread2 begin wait
thread3 begin wait
thread1 begin wait
thread4 release lock
thread1 end wait
thread3 end wait
thread2 end wait
thread finish
那如果线程4调用的是notify方法呢?会发生什么?
thread1 begin wait
thread3 begin wait
thread2 begin wait
thread4 release lock
thread1 end wait
...
可以看到,只是随机唤醒了一个线程, 其他线程还在阻塞状态,程序并没有结束。
应用场景
下面我们来看一个应用场景:
public class SuspendTest {
private static volatile Object sources1 = new Object();
private static volatile Object sources2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (sources1) {
System.out.println(Thread.currentThread().getName() + " --- get sources1 lock");
synchronized (sources2) {
System.out.println(Thread.currentThread().getName() + " --- get sources2 lock");
System.out.println(Thread.currentThread().getName() + " --- let sources1 wait");
try {
Thread.sleep(3000);
sources1.wait();
System.out.println(Thread.currentThread().getName() + " --- sources1 notify");
sources2.notifyAll();
// System.out.println(Thread.currentThread().getName() + " --- sources2 notify");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "thread - 1");
Thread thread2 = new Thread(() -> {
synchronized (sources1) {
System.out.println(Thread.currentThread().getName() + " --- get sources1 lock");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sources1.notifyAll();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "thread - 2");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("finish");
}
}
线程1和线程2同时执行,线程1先获取到source1和source2,然后睡眠了3秒之后,source1进入等待,这时线程2获取到资源1,等待2秒唤醒资源1,这时线程1将会继续执行。看下打印 结果:
thread - 1 --- get sources1 lock
thread - 1 --- get sources2 lock
thread - 1 --- let sources1 wait
thread - 2 --- get sources1 lock
thread - 2 --- try get sources2 lock
thread - 1 --- sources1 notify
finish
sleep() 和 wait()
我们知道sleep()让线程休眠,那么它和wait()有什么区别呢?
- sleep(),休眠,等休眠时间已过,才有执行的资格,注意:这里只是有了执行的资格,并不代表会立即执行。至于什么时间执行,要取决于CPU时间片的调度。
- wait(), 等待,需要其他人来唤醒,唤醒后才有执行的资格。
sleep无条件可以休眠, wait在某些情况下等待一下(资源不足的情况可以使用wait等待资源的释放)
join()
上面的示例中,我们用到了很多join方法,它是做什么的呢?
在实际项目中,我们很多场景都需要在线程执行完之后再继续往下执行,比如多个线程全部执行完以后在做某些事情,或者让两个线程按顺序执行,就可以使用join方法来处理。
class ThreadJoinTest extends Thread{
public ThreadJoinTest(String name){
super(name);
}
@Override
public void run(){
for(int i=0;i<2;i++){
System.out.println(this.getName() + ":" + i);
}
}
}
public class JoinTest {
public static void main(String [] args) throws InterruptedException {
ThreadJoinTest t1 = new ThreadJoinTest("A");
ThreadJoinTest t2 = new ThreadJoinTest("B");
t1.start();
/**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕
所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
*/
t1.join(); // 让t1获取执行权
t2.start();
}
}
上面T1调用了join,然后再启动t2,这样每次执行一定是t1先执行完再执行t2了:
A:0
A:1
B:0
B:1
yield()
Thread中的一个静态方法,当调用yield方法时,就是在告诉线程调度器请求让出自己的CPU执行权,进入就绪状态。但是线程调度器可以无条件忽略这个请求。所以这个方法可能不起作用,平时用的也不多。
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 5; i++) {
if (i % 5 == 0) {
System.out.println(Thread.currentThread().getName() + " use yield");
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "value === " + i);
}
}, "t1").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
if (i % 5 == 0) {
System.out.println(Thread.currentThread().getName() + " use yield");
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "value === " + i);
}
}, "t2").start();
}
执行结果:
t1 use yield
t2 use yield
t1value === 0
t1value === 1
t1value === 2
t1value === 3
t1value === 4
t2value === 0
t2value === 1
t2value === 2
t2value === 3
t2value === 4
可以看到yield方法生效了,t1先抢到执行权,然后让出了自己的时间片,由t2执行, t2也让出了自己的时间片,又开始让t1执行打印,完后t2开始打印。
结语
线程的基础也就这么点东西,如果认真看,认真推敲的话还是很好理解的。篇幅有限,后续补全进阶篇:
- 锁(sync、Lock)
- ThreadLocal、InheritableThreadLocal原理
- CAS原理
- 线程池原理
网友评论