一. 进程与线程
1. 概念
进程:
进程可以视为程序的一个实例。当一个程序被运行,从磁盘加载这个程序的代码至内存,就开启了一个进程。
进程是资源分配的最小单位。
线程:
线程是调度的最小单位,一个进程内可以有多个线程。线程由一系列指令组成。
二者对比:
进程间相互独立,线程存在于进程内,是进程的一个子集;
进程拥有共享的资源,如内存空间,供内部的线程共享;
进程间通信较为复杂,不同计算机之间的进程通信需要通过网络和协议;
线程通信较为简单,因为它们共享进程间的内存,如多个线程可以访问进程中同一个共享变量;
线程更家轻量,线程的上下文切换成本一般比进程上下文切换更低。
2. 并发与并行
并发(concurrent):
操作系统使用任务调度器将cpu的时间片分给不同的线程使用。线程轮流使用CPU的做法称为并发。
微观串行,宏观并行。
并行(parallel):
多核cpu下,多个线程同时运行。
并发是同一时间应对多件事情的能力;
并行是同一时间动手做多件事情的能力。
3. 线程的应用
(1)异步调用
同步:方法调用时需要等待结果返回,才能继续运行;
异步:方法调用时不需要等待结果返回,就能继续运行。
多线程的应用:让方法执行由同步变成异步。如长时间的文件操作,可以通过新开一个线程进行处理,避免阻塞主线程。
(2)提升效率
充分利用多核cpu的优势,提高运行效率。
注意:只有多核cpu才能提高效率,单核时即使是多个线程仍然是轮流执行。单核cpu下,多线程只是用于并发进行多个任务。
二. Java 线程
1. 创建和运行线程
方法一:
//new 一个线程对象
Thread thread = new Thread() {
@Override
public void run() {
int i = 10;
while (i > 0) {
System.out.println(this.getName() + ": I am " + i--);
}
}
};
thread.setName("线程1");
thread.start(); //执行线程
方法二(推荐):
Thread + Runnable,把线程和任务代码分开
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("runnable……");
}
};
Thread thread = new Thread(runnable);
thread.start();
Runnable 还可以使用 lambda 表达式进行简化(只有一个方法的类可以使用lambda进行简化):
Runnable runnable = () -> System.out.println("runnable……");
Thread thread = new Thread(runnable, "线程2");
thread.start();
方法三:FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("running");
Thread.sleep(5000);
return 100;
}
});
futureTask.run();
System.out.println(futureTask.get()); //get方法会进行阻塞,等futureTask线程走完以后再获取值
2. 查看进程线程
Windows
tasklist #查看所有进程
tasklist | findstr java #带关键字查找
jps #查看所有Java进程
taskkill /F /PID 进程号 #杀死进程
Linux
ps -ef #查看所有进程
ps -ef | grep java
kill 进程号 #杀死进程
top #查看动态进程情况
top -H -p 进程号 #查看该进程中的所有线程信息
3. 线程上下文切换
含义:因为某些原因导致 cpu 不再执行当前线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当进行上下文切换时,程序计数器会记录下一条jvm指令的执行地址,频繁的上下文切换会影响性能,线程数尽量小于 cpu 核数。
4. 常见方法
(1) start 与 run 方法
thread.start() 与 thread.run() 的区别:前者是启动了一个线程,由thread线程执行run方法,后者直接调用 run 其实是主线程调用了 run 方法,不能起到提高效率或者并发执行的效果。
(2)sleep 与 yield 方法
- sleep 方法会让当前线程从 running 进入到 timed waiting(阻塞)状态::
Thread.sleep(1000) //写在哪个线程里就表示休眠哪个线程
- 其他线程可以使用 interrupt 方法唤醒正在睡眠的线程,此时睡眠的线程会抛出 InterruptedException 异常。
- 睡眠结束后的线程未必会立刻得到执行(时间片未到)。
- 建议用 TimeUnit 的sleep代替 Thread 的 sleep,可读性更高。
yield 将当前线程从 running 进入 runnable 就绪状态,然后调度执行其他线程。
区别:任务调度器会将时间片分给就绪状态的线程,但不会分给阻塞状态的线程。
sleep 应用:在 while(true) 中添加 sleep,避免空转浪费 cpu。
(3)线程优先级
thread.setPriority()
线程优先级会提示调度器优先调度该线程,但调度器可以忽略它。
如果cpu比较忙,优先级高的线程会获得更多的时间片,如果cpu比较闲,那优先级几乎没作用。
(4)join 方法
join() 方法用于等待某个线程运行结束后再继续执行。
public class ThreadTest {
static int a = 0;
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread thread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1);
a = 2;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.setName("线程1");
thread.start();
thread.join(); // 不加 join a = 0; 加了 join a = 2
System.out.println("a = " + a);
}
join 应用:
1)线程同步:当等待多个线程执行结束时,等待时间取决于用时最长的那个;
2)限时同步:join方法可以添加等待时间。
(5)interrupt 方法
a. 唤醒处于阻塞状态的线程:会抛出InterruptedException异常,打断标记为 false;
b. 打断正常运行的线程:打断标记为true,可以用于使得线程优雅地退出
两种状态interrupt打断原理
Thread thread = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("执行中……");
//获取打断标记,可以优雅地退出线程
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted) {
System.out.println("被打断了,退出循环");
break;
}
}
}
};
thread.setName("线程1");
thread.start();
Thread.sleep(2000);
thread.interrupt(); //不能真正让线程退出,只是使得打断标记为 true,真正线程退出还得靠线程自己
System.out.println("打断标记 " + thread.isInterrupted()); // true
两阶段终止模式:
问题:如何在一个线程 T1 中如何优雅地(给T2料理后事的机会)终止线程 T2?
错误方法:1)调用线程的stop方法,stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就没有机会释放锁,其他线程将永远无法获取锁;2)使用 System.exit(int) 方法停止线程,这种方法会将整个进程都停止。
设计一个程序实现下列功能:
public static void main(String[] args) throws InterruptedException {
Thread monitor = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if (interrupted) {
System.out.println("料理后事,退出循环");
break;
}
try {
Thread.sleep(1000); // 此时被打断会抛异常,打断标记为false
System.out.println("执行监控记录"); //此时被打断,打断标记为true
} catch (InterruptedException e) {
System.out.println("抛异常");
e.printStackTrace();
current.interrupt(); //需要重新设置打断标记,此时为true
}
}
});
monitor.start();
Thread.sleep(3500);
monitor.interrupt();
}
小知识:
isInterrupted 与 interrupted(static方法) 的区别:前者判断完不会清除打断标记,后者判断完会将打断标记重新置为false。
interrupt 还可以打断 park 的线程,使之继续运行:
public static void main(String[] args) throws InterruptedException {
Thread monitor = new Thread(() -> {
System.out.println("park……");
LockSupport.park();
System.out.println("unpack……");
});
monitor.start();
Thread.sleep(2000);
monitor.interrupt(); //打断park线程使之继续运行
}
(6)不推荐使用的方法
stop(停止)、suspend(暂停)、resume(恢复) 方法
(7)守护线程
默认情况下,Java进程需要等待所有线程结束都运行结束才会结束。守护线程是只要等其他非守护线程都运行结束了,即使守护线程还在运行也会被强制结束。
public static void main(String[] args) throws InterruptedException {
Thread monitor = new Thread(() -> {
while(true) {
}
});
monitor.setDaemon(true); // 将monitor线程设置为守护线程
monitor.start();
Thread.sleep(2000);
System.out.println("结束"); //主线程结束后monitor线程也会停止
}
垃圾回收线程是典型的守护线程,当主线程结束时垃圾回收线程会被强制结束。
5. 线程的状态
(1). 操作系统层面
初始状态:在语言方面创建了线程对象,还未与操作系统线程关联;
可运行状态(就绪状态):线程已经被创建(与操作系统线程关联),可以由CPU调度执行;
运行状态:得到cpu运行时间片;
阻塞状态:如果调用了阻塞API,如读写文件,这时线程会让出CPU,导致线程上下文切换,进入阻塞状态;等IO操作完毕,由操作系统唤醒阻塞的线程,转至可运行状态;
终止状态:线程执行完毕,生命周期结束。
(2). JAVA层面
Thread.sleep(1000) // timed_waiting
join() // waiting
等待锁释放 // blocked