基础概念
什么是进程和线程?
进程:一个正在运行的应用程序;操作系统管理的最小单元(操作系统进行资源分配的最小单位),一个进程至少一个线程,或者多个线程;进程与进程之间是相互独立的;
资源:运行一个程序需要 CPU、内存空间,磁盘IO,这些都属于运行一个程序所需要的资源;
线程:进程的运行方法;CPU 调度的最小单元,如果一个进程还有一个线程没有杀掉,那么进程就还是处于存活状态(线程依附与进程而存在);线程之间是可以共享资源的,包括CPU、内存空间,磁盘IO;
CPU核心数和线程数的关系?
早期一个核心一个线程,后期超线程技术之后 一个核心两个线程;
什么是多核?早期的计算机芯片上面只能放一个逻辑核心,随着摩尔定律的失效,大家发现在CPU上在缩短这个宽度3纳米,在往小缩短就要受到量子物理的约束,此时提高晶体管的密度是不可行的了;那么把多个物理核心集成到一块芯片上,也就是说在一块物理芯片上会有多块处理器,这就是所谓的CPU多核;
为什么CPU的核心数限制,但是我们在启动一个应用程序之后 启动了很多线程(超过了线程数)但是我们的程序还是能正常执行?
CPU时间片轮转机制?
CPU 运行过程中分时间的机制;
CPU 调度,会把 1秒钟 分成好多小的单位,假设每个单位都是 1 纳秒,所有线程都会去操作系统抢占这个 1纳秒,谁抢到了谁就去执行,假设有三个线程 A B C,A 抢到了这一纳秒的时间,那么就去运行 A 线程的代码,当运行到A线程的某个代码位置的时候,这 1纳秒 用完了,就要释放 CPU 出来,然后和其他线程继续去抢第二个 1纳秒,假如第二个时间片被其他线程抢到了,就执行其他线程,依次往后所有线程继续抢,当 A 再次抢到的时候,要接着 A 执行到的地方继续执行,谁来记录 A 执行到了什么地方,就由程序计数器来负责;
并发、并行?
并行:同时有多个线程在执行,可以同时运行的任务数;
并发:既有前台 又有后台,交替执行;总结的同一个时间点的吞吐量(讨论并发的时候不能脱离时间单位,否则没有意义);
举例子:假设咖啡机在 1分钟 之内,有四个人拿到了咖啡,那么咖啡机在一分钟内的并发量就是 4
综合来说:并发就是说我们的应用能交替的执行不同的任务,只不过 CPU 或者操作系统使用了不同的技术,使人以不可察觉的速度在运行这些任务。看起来好像达到了同时执行的效果,但其实不是;
线程存在并行和并发 协程只有并发没有并行;
高并发编程的意义,好处是什么?
平衡使用我们的线程,不要 CPU 过累;
Java程序默认是多线程还是单线程?
Java 程序默认是多线程的;执行一个 Java 程序,可能会有 6 个 也可能会有 8 个;
ini
public class OnlyMain {
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());
}
}
}
多次执行,会开启 6 - 8 个线程;
为什么要用线程池来管理线程,是否可以无限制的创建线程?
Linux 下一个进程所能开的线程数最多 1000 个;Windows 下一个进程所能开的线程数最多 2000个;
那么线程创建会分配栈空间,缺省是 1MB,只单纯的 new 1000 个线程,所占用的内存就达到了 1G;
资源回收的动作放在 finalize 方法中为什么会不靠谱?
因为很有可能不会被执行,因为执行这个方法是一个单独的 Finalizer 线程,这个线程是守护线程,当主线程一旦退出,这个线程就跟着退出了,那么这个方法很有可能来不及调用,这个线程就结束了,相关的资源释放工作还没做完;
Thread
线程实现有几种方式?
两种方式,一是 extends Thread,一是 implements Runnable,实现 Callable 接口严格意义上来说不是
源码中的注释 【There are two ways to create a new thread of execution】说明只有两种方式来创建线程;
继承 Thread 类
scala
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
}
}
PrimeThread pThread = new PrimeThread(1000L);
pThread.start();
实现 Runnable 接口
java
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
}
}
PrimeRun pRun = new PrimeRun(1000L);
Thread thread = new Thread(pRun);
thread.start();
实现 Callable 接口 本质上还是实现了 Runnable 接口,所以它其实可以和 Runnable 归位一类
typescript
public class ThreadCallable implements Callable<String> {
@Override
public String call() throws Exception {
return null;
}
}
public static void main(String[] args) {
ThreadCallable threadCallable = new ThreadCallable();
FutureTask<String> futureTask = new FutureTask<>(threadCallable);
futureTask.run();
}
本质上:Callable 的实例会交给 FutureTask,而 FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 接口继承自 Runnable 接口;
所以:实现线程的方式只有两种
继承 Thread 和 实现 Runnable 的区别 ?
Thread 是对线程的抽象,Runnable 是对任务(业务逻辑)的抽象;
线程的安全终止
Thread 源码中有一个 stop 方法,但是这个 stop 方法被 JDK 打上了一个废弃的注解 @Deprecated
Thread 源码中还有一个 destory 方法,但是这个 destory 方法被 JDK 打上了一个废弃的注解 @Deprecated
Thread 源码中有一个 suspend 方法,但是这个 suspend 方法被 JDK 打上了一个废弃的注解@Deprecated
JDK并不建议使用这几个方法,这些方法带有很高的强制性;例如 suspend 方法,会让线程发生一次上下文切换,从当前的一个可运行状态切换到一个挂起状态,但是 suspend 被调用后,相关的线程并不会释放锁,而是占着这个资源进入一个睡眠状态,这样就很容易发生死锁的问题;同样的 stop 在终结一个线程的时候,比较野蛮会强制把当前线程一把干掉,不关心当前线程有没有释放资源;
例如:开启一个线程,写一个10K的文件,当写到4K的时候,调用了 stop,那么这个线程就会被野蛮停掉,那么这个文件就是缺省的,不能被使用的;
所以说 stop、suspend、destory等 为什么不建议使用?因为它可能导致线程所占用的资源不会释放;
Thread 中提供了比较和谐的方式,thread.interrupt() 发起停止信号,和 isInterrupted() 接收停止信号结合使用;
Thread 源码中,有 4 个关于 interrupt 的方法
interrupt 对线程进行终断,但并不是真真正正的终止一个线程,而是发给线程的一个终断标志位,当调用这个方法的时候,其实就是给线程打了一个招呼,不代表这个线程就要立即停止工作,而且这个线程完全可以不理会这个终断请求(也就是说 JDK 中,线程是协作式的,而不是所谓的抢占式);
isInterrupted 判断当前线程是否被终断;
java
public static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
super.run();
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " interrupt flag = " + isInterrupted());
while (!isInterrupted()) {
// while (true) {
// while (Thread.interrupted())
System.out.println("thread is running");
System.out.println(threadName + " inner interrupt flag = " + isInterrupted());
}
System.out.println(threadName + " interrupt flag = " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UseThread useThread = new UseThread("endThread");
useThread.start();
Thread.sleep(10);
useThread.interrupt(); // 终断线程,设置一个标志位为 true
}
当调用了 interrupt 的时候,如果不用 isInterrupted 接收的话,也就是 while(true) 放开,那么线程还是会一直执行
interrupted 这个方法是 Thread 的一个静态方法,判断当前线程是否被终断,与 isInterrupted 在使用上有区别,区别如下:
Thread.interrupted 会将是否终断的标志位清除,如果设置成了 true,那么它就会重置回 false
那么,在 Runnable 中 如何终断线程呢?
通过 Thread.currentThread().isInterrupted();
csharp
public class ThreadRunnable implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " interrupt flag = " + Thread.currentThread().isInterrupted());
while (!Thread.currentThread().isInterrupted()) {
System.out.println("thread is running");
System.out.println(threadName + " interrupt flag = " + Thread.currentThread().isInterrupted());
}
System.out.println(threadName + " interrupt flag = " + Thread.currentThread().isInterrupted());
}
}
Thread.sleep() 方法 会抛出一个 InterruptedException,那么当我们 catch 住这个异常之后,获取当前终断标志位的时候 是 true 还是 false ?
scala
public static class HasInterruptException extends Thread {
@Override
public void run() {
super.run();
while (!isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "in InterruptedException interrupt flag is " + isInterrupted());
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "I am extends Thread");
}
System.out.println(Thread.currentThread().getName() + " interrupt flag is" + isInterrupted());
}
}
结果是 false ,也就是说线程抛出这个异常的同时,把标志位又重置成了 false;
所以 需要我们在 catch 的时候,手动的调用一下 interrupt() 方法;
scss
public static class HasInterruptException extends Thread {
@Override
public void run() {
super.run();
while (!isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "in InterruptedException interrupt flag is " + isInterrupted());
interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "I am extends Thread");
}
System.out.println(Thread.currentThread().getName() + " interrupt flag is" + isInterrupted());
}
}
所以:JDK 针对 sleep、wait 等增加异常抛出的原因是什么?有什么好处?
修改终断标志位为 false,可以让我们及时的释放资源,并手动的调用 interrupt 来终断线程,防止死锁的发生;
run 和 start 的区别?
run 是方法,和线程没有任何关系,start 会走底层,最终调度到 run 方法;run方法一般是业务实现的方法,可以多次调用,也可以直接通过声明的 thread 对象来调用 run 方法;
如果我们直接调用 run 方法的时候 会发生了什么?
java
public class ThreadRun {
public static void main(String[] args) {
ThreadRunTest runTest = new ThreadRunTest();
runTest.run();
}
static class ThreadRunTest extends Thread {
@Override
public void run() {
super.run();
int i = 90;
while (i > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("CurrentThread: " + Thread.currentThread().getName() + ", and i = " + (i--));
}
}
}
}
直接调用 run 方法,当前逻辑会在主线程执行,不会在子线程执行,只有调用 start 方法才会在子线程中执行;
所以:start 才是真正意义上的启动了一个线程并和操作系统里面的线程进行一个挂钩。run 本质上就是普通类里的一个普通方法而已。对于 new 出来的一个线程 start 只能调用一次,多次调用就会抛出异常;
调用 start0 方法 真真正正的开始执行线程,调度到 native 层;
start 方法调用两次会抛出异常;通过 threadStatus != 0 的判断,会抛出 IllegalThreadStateException 异常;
线程状态(生命周期)
新建(初始):new 一个新的线程对象,就称为线程的初始,但是初始不代表这个线程就开始执行了,只有调用了 start 方法之后才会进入运行态;
运行态:
-
就绪(可运行状态)
-
CPU 时间片用完了或者被操作系统剥夺了或者自动放弃了,线程就会出于就绪中,等待下次的时间片;
-
调用 join 获得执行权,通过系统调度进入运行中
-
运行中
-
拿到了时间片,被分配了 cpu,就处于运行中;
-
调用 yield 就会进入就绪状态;
-
yield 方法 让当前线程让出 cpu 的执行权(仅仅只是让出 cpu 执行权,但是不会让出锁),当前线程就会由运行态进入可运行状态(就绪);
-
join 方法 让cpu放弃当前正在执行的线程,转而来执行调用 join 方法的线程;
等待态:
-
等待
-
线程调用了 wait、join、LockSupport.park() 方法,进入等待态
-
当线程调用了 notify 或者 notifyAll、LockSupport.unpark(Thread) 之后,线程就会重新进入运行态
-
等待超时
-
线程调用 wait(long)、sleep(long)、 join(long)、LockSupport.parkNanos()、LockSupport.parkUntil() 方法,传入时间,当到达这个时间阈值之后,就是等待超时态;
-
超时之后,或者当线程调用了 notify、notifyAll、LockSupport.unpark(Thread) 之后,线程就会重新进入运行态;
阻塞态:
- 线程调用了synchronized 修饰的代码或者代码块,如果没有拿到锁,就会处于阻塞状态。只有 synchronized 修饰的才会进入阻塞状态,其他都不是;
- 获取到锁之后,线程就会重新进入运行态(属于被迫进入);
执行完成(终止)态:
- run方法跑完了,就会进入完成态;
- 调用了 stop 方法,强制关闭,也会进入完成态
- 调用了 setDeamon 方法,将当前线程变成守护线程,当进程中的所有非守护线程都执行完毕之后,那么守护线程就会跟着死亡进入完成态;
如何控制线程的执行顺序?
join 关键方法,join 的意思是使得 cpu 放弃当前线程的执行,并返回对应的线程,例如 调用 A 线程的 join 方法,则主线程放弃 cpu 控制权,并返回 A 线程继续执行,直到 A 线程执行完毕,A 执行完毕释放之后主线程继续执行调用 b.start();
ini
ThreadJoinTest a = new ThreadJoinTest("A");
ThreadJoinTest b = new ThreadJoinTest("B");
a.start();
a.join();
b.start();
// 会在 a 执行完毕之后 才执行 b,一定要注意这个调用顺序
能不能控制线程的优先级?
不会考虑,setPriority() 方法能不能发挥作用完全由操作系统来决定,因为线程的优先级很依赖于系统的平台,所以这个优先级无法对号入座,无法做到你想象中的优先级,属于不稳定,有风险;因为某些开源框架,也不可能依赖线程优先级来设置自己想要的优先级顺序,这个是不可靠的;
守护线程
先来看一组示例
上面这些除了 main(主线程) 之外都可以认为是守护线程,守护线程也包括GC;守护线程其实是一种支持类的线程,主要是对程序后台做一些调度、支持性的工作(比如内存的回收等等);
当我们启动了一个进程的时候,这个进程中会通过 new Thread 启动了很多的线程,那么这些通过 new Thread 启动的线程,如果不设置 setDameon 都是用户线程(非守护线程),对于 JDK 内部启动的线程或者通过参数配置(setDameon)启动的线程都是守护线程;
在一个进程里面,如果所有的用户线程(非守护线程)都结束之后,这个进程也就跟着停止了,那么所有的守护线程也就结束了;
java
public class DaemonThread {
public static void main(String[] args) throws InterruptedException {
UseThread useThread = new UseThread();
useThread.setDaemon(true);
useThread.start();
Thread.sleep(5);
}
static class UseThread extends Thread {
@Override
public void run() {
super.run();
try {
while(!isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " extends Thread");
}
} finally {
// 守护线程的 finally 不一定起作用
System.out.println("finally");
}
}
}
}
执行之后,因为 UseThread 被设置成了守护线程,当我们的主线程执行完毕(Thread.sleep(5) 执行之后)之后,这个 UseThread 就会停止执行,不需要我们调用 useThread.interrupt();
但是,通过 Console 中的打印结果来看,finally 并没有执行;
也就是说我们不能通过 finally 代码块来确保执行关闭、清理资源;finally 能否执行完全看操作系统的调度;
用户线程不用担心,finally 一定会执行;
多线程间数据如何共享(锁)
多个线程之间访问同一个对象,如果我们不做操作会发生什么?
java
public class SyncThread {
private int count = 0;
public void addCount() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SyncThread syncThread = new SyncThread();
AddCount addCount1 = new AddCount(syncThread);
AddCount addCount2 = new AddCount(syncThread);
addCount1.start();
addCount2.start();
Thread.sleep(5);
System.out.println(syncThread.count);
}
static class AddCount extends Thread {
private SyncThread simpleOper;
public AddCount(SyncThread simpleOper) {
this.simpleOper = simpleOper;
}
@Override
public void run() {
super.run();
for (int i = 0; i < 10000; i++) {
simpleOper.addCount();
}
}
}
}
执行多次,都不是想要的结果(20000),多线程操作同一个全局变量,导致结果不正确;
如何解决?加锁!
arduino
synchronized // 内置锁。保证在某一个时刻只有一个线程访问这个变量
synchronized 作用在方法上,把这个方法变成同步方法
arduino
public synchronized void addCount() {
count++;
}
synchronized 作用在代码块上,把这段代码变成同步代码块
typescript
Object object = new Object();
public void addCount1() {
synchronized (object) {
count ++;
}
}
这些也都可以叫作 对象锁 ,锁的作用是在同一个对象上;
如果锁在两个对象上,则这个就会失效;
synchronized 作用在静态方法上,会被叫作 类锁(class对象锁)
arduino
public static synchronized void addCount() {
count++;
}
每一个类在进行类加载的时候,都会在 JVM 中有一个 class 对象,static 方法上进行加锁的时候,意味着加锁的是一个 class 对象,本质上还是一个对象锁。这个对象就是每个类在虚拟机中所拥有的唯一的一个 class 对象;
类锁和对象锁之间也是互相不干扰的,因为不是同一个对象;
但是:
这种情况下是可以并行的,因为是两个不同的锁对象,一个是 class 对象,一个是 object 对象;
能不能指定 CPU 去执行某个线程?
不能。Java 做不到,唯一能够去干预的就是 C 语言调用内核 API 去指定才行;
简历润色
简历上可写:深度理解Java多线程、线程安全、并发编程;
下一章预告
带你玩转CAS原理;