CPU核心数与线程数的关系
在计算机早期的时候一个核心对应一个线程,它们之间的关系是1:1,也就是说如果你的CPU是4核的,那么在同一时间片只能运行4个线程。后期随着技术的发展出现了超线程技术,可以使一个CPU核心对应两个线程,比列为1 : 2。
CPU时间片轮转机制
在还没有出现多核CPU的时候,CPU都是单核的。如果想要执行多线程,那么就采用了一种名为 RR调度 的随机算法指导CPU在线程之间高速切换。但是如果线程的数量过多的话,就会出现明显的卡顿。假设以前只有5个线程,那么理想情况下每个线程每秒中都能获得200ms的执行时间。但是现在有1000个线程同时存在(线程状态必须是可执行的),那每个线程分到的时间就只剩下1ms了。上述的也仅仅是在理想情况下,实际上并不是这样的。CPU在线程之间来回切换我们称之为 上下文切换 。每一次上下文切换的开销是很大的。假设执行一个简单的 1+1 需要消耗一个时间周期,但是每进行一次上下文切换需要消耗大约5000~20000个时间周期。假设现在要从线程A切换到线程B上,首先需要把线程A的临时数据进行保存,然后再进行切换,当下次重新切换到A线程的时候还需要把临时数据重新读取出来。所以当我们的电脑上打开的程序多了就会感觉卡顿。
进程和线程
进程:操作系统所管理的最小单元,一个进程至少有一个线程。如果一个线程中的线程还有一个存活,那么这个进程就还会存活。
线程:CPU调度的最小单元
并发和并行
并行
可以看到上图中的运动员在各自的跑道中在进行赛跑,这种行为就是并行。也就是说统一时刻有多个相同的动作(跑步)存在执行。
并发
并发更强调的是吞吐量。即在某一个时间段内执行了多少。
按照图片来进行解释的话就是在半个小时内这个路口通过了多少车。而这个吞吐量就代表了并发能力。也就是说并发是需要有一个时间单位存在。
Java多线程编程
Java默认就是多线程的,这一点我们可以通过代码来进行验证:
public static void main(String[] args) {
// 虚拟机线程管理的接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 获取所有的线程信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadId() + "-----" + threadInfo.getThreadName());
}
}
运行结果
可以看到即使我们只运行了一个主线程,但是还是有其他的几个线程在执行。可以看到并没有GC线程。也就是说在程序刚开始运行的时候GC线程是不会启动的,当系统检测到有垃圾需要回收的时候才会去启动GC线程。
启动线程的方式
- 继承Thread类
public class ThreadImpl extends Thread{
@Override
public void run() {
super.run();
// do something
}
}
// 使用方式
ThreadImpl thread1 = new ThreadImpl();
thread1.start();
- 实现Runnable接口
public class ThreadImpl2 implements Runnable{
@Override
public void run() {
// do something
}
}
// 使用方式
ThreadImpl2 runnable = new ThreadImpl2();
Thread thread2 = new Thread(runnable);
thread2.start();
- 实现Callable接口
public class ThreadImpl3 implements Callable<String>{
@Override
public String call() throws Exception {
return "hello";
}
}
// 使用方式
ThreadImpl3 callable = new ThreadImpl3();
FutureTask<String> task = new FutureTask<>(callable);
Thread thread3 = new Thread(task);
thread3.start();
// 获取返回值
String s = task.get();
需要说明一下的是这种方式其实和第二种实现Runnable是同一种方式,因为首先Thread类的构造方法中是看不见任何有关Callable的身影的,其次Callable需要使用FutureTask包装一下才能给Thread使用,而FutureTask是Runnable接口的子类,也就是说FutureTask就是一个Runnable。严格意义上来说只有两种方式新启一个线程,这在Thread源码中的注释里就已经说明了:There are two ways to create a new thread of execution.,翻译过来就是有两种方式去创建一个新的线程去执行。只不过使用Callable和FutureTask是可以允许有返回值的。Android当中的AsyncTask就是使用的这种方式。
线程的状态
线程的状态也叫线程的生命周期,一共有6种:
- 初始(NEW):新创建了一个线程,但还没调用start()方法
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法,该状态的线程位于可运行线程池中,等待被线程调度选中,获得CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变成运行中状态(running)
- 阻塞BLOCKED:表示线程阻塞于锁
- 等待WAITING:进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
- 超时等待TIMED_WAITING:该状态不同于WAITING,它可以在一定时间后自行返回
-
终止TERMINATED:表示该线程已经执行完毕
线程状态的变迁
需要注意的是在Java中有且仅有一种方式能够让线程进入阻塞状态的是使用synchronized关键字。而使用Lock接口下的这种显式锁的方式不属于阻塞,因为它们的底层实现是用的上图中的LockSupport.park()的这种方式,那么使用显式锁的时候进入的是等待或者等待超时。区别就在于阻塞时被动的,而等待是主动的。也就是说当线程去拿锁的时候没有锁而不得不阻塞。而显式锁的方式是由于条件不满足而主动等待。
常见的面试题
1. run()和start()的区别?
答:run()函数只是一个普通的函数,和线程没有任何关系,只不过是执行逻辑写在这里。start()则会调用native方法,最终又会回调run()函数。
2. 如何控制线程按顺序执行?
答:使用join()函数可以实现。下面通过例子来测试一下:
public class Test {
public static class JoinThread extends Thread {
JoinThread(@NonNull String name) {
super(name);
}
@Override
public void run() {
super.run();
for (int i = 0; i < 20; i++) {
System.out.println(getName() + "-----" + i);
}
}
}
public static void main(String[] args) throws InterruptedException {
JoinThread t1 = new JoinThread("aaa");
JoinThread t2 = new JoinThread("bbb");
t1.start();
t1.join();
t2.start();
}
}
join()函数的意思是使得当前正在执行的线程放弃执行权,并返回对应的线程。根据上述例子描述就是程序在main()函数中调用t1线程的join()方法,则main线程放弃CPU的控制权,并返回t1线程继续执行直到t1线程执行完毕,所以结果是t1线程执行完后才到主线程执行。相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会。
3. 在Java中能不能指定CPU去执行某个线程?
答:不可以,Java做不到。唯一能够干预的是C语言调用内核的API去指定才行。
4. 在项目开发中,会考虑使用Java线程的优先级吗?
答:不会考虑。因为线程的优先级很依赖系统平台,没有办法做到对号入座。如果这样做是有风险的,不稳定。例如Java的优先级有十级,但是系统平台只有二级,这样就无法对应。
5. sleep()和wait()的区别?
答:首先来讲sleep()是线程的方法,而wait()属于Object的方法。其次sleep()是休眠,等休眠时间一过,线程就又会进入就绪状态,表示有资格被执行,但不一定会立即执行,因为无法得知CPU什么时候会调用。而wait()则是等待,需要别人来唤醒,被唤醒之后则和sleep()一样进入就绪状态等待CPU执行。最后sleep()可以无条件休眠,而wait()则是出于某种原因,例如条件不满足才会去等待。
6. 在Java中能不能强制中断线程的执行?
答:虽然提供了stop()函数,但是此函数的不推荐使用,因为太过于暴力,非常容易出现问题,很危险。建议使用interrupt()的方式来处理线程的停止。需要注意的是interrupt()只是协作的方式,不能绝对保证中断。
public static class InterruptThread extends Thread{
@Override
public void run() {
super.run();
while (!isInterrupted()){
System.out.println("running ----- " + isInterrupted());
}
System.out.println("end -----"+ isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
InterruptThread interruptThread = new InterruptThread();
interruptThread.start();
Thread.sleep(10);
System.out.println("-----request interrupt");
interruptThread.interrupt();
}
7. 如何让出当前线程的执行权?
答:使用yield()函数。不过基本用不到,只在JDK的某些实现才能看到。
8. sleep()和wait()哪个函数会清楚中断标记?
答:sleep()函数。因为调用sleep()函数的时候需要抛出一个InterruptedException异常,那么在抛出异常的时候就会清除调用interrupt()设置的标记。
网友评论