了解并发编程(一)
什么是线程
线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程中并行执行不同的任务。
例如云盘下载,云盘就是一个进程,一个正在执行的下载任务就是一个线程,多个下载任务同时进行就是一个多线程工作,而这种多个线程同时工作,在 java 中称之为并发。
并发与并行
并发(concurrency)是指两个或者多个事件在同一时刻发生
image.png
例如,朝阳小区只有一个东门可以进入小区,而这时有一群朝阳大妈蜂拥而至,大妈们不得不为一个进入小区的机会互相竞争,而在同一时刻只有一位大妈可以得到进入小区的机会,其他大妈需要等到上一个位大妈进入小区后再竞争下一个机会。
反应到程序里,假设现在只有一个 CPU 资源,程序线程之间需要竞争得到执行机会,当线程 A 得到执行机会的时候,B C D...线程不会执行,因为 CPU 资源已经被线程 A 占用了,同理线程 B 得到执行机会的时候,其他线程也一样不会执行,因为 CPU 资源已经被 B 占用了。
并发过程中 A B C D...线程可以从宏观和微观两个层面来讲,从微观角度看,假设当前电脑的 CPU 是单核,但是能不能支持多线程呢?当然是可能的,此时如果是多线程的话,那么 CPU 是通过不断分配时间片的方式来实现线程切换的,从宏观角度看线程似乎又是同时进行的只是由于切换速度太快,我们很难感知到。
并行(parallelism)指两个或两个以上事件(或线程)在同一时间点发生,是真正意义上的不同事件或线程在同一时刻,在不同CPU资源上(多核),同时执行。
image.png
例如朝阳小区由于改造升级,现在东南西北都有可以进入小区的大门了,大妈们不需要像以前一样蜂拥而至到一个小区门抢夺一个进入小区的机会了,并行没有像并发那样激烈的竞争,大妈们可以从东南西北同时进入小区。
而反映到程序中,相当于当前 CPU 是 4 核的,在同一时间点上可以执行 4 个线程,可以处理 4 个不同或相同的任务,大大提高系统的性能。
java 中实现线程的方式
继承 Thread 类,实现 run() 方法
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("这是创新的新线程,"+Thread.currentThread().getName());
}
public static void main(String[] args) {
// 创建线程类
MyThread thread = new MyThread();
thread.start(); // 启动一个创建的线程
System.out.println("这是主线程 main,"+Thread.currentThread().getName());
}
}
实现 Runnable 接口
public class MyRunable implements Runnable{
@Override
public void run() {
System.out.println("这是创新的新线程,"+Thread.currentThread().getName());
}
public static void main(String[] args) {
Runnable runable = new MyRunable();
Thread t1 = new Thread(runable);
t1.start(); // 启动线程
Thread t2 = new Thread(runable);
t2.start(); // 启动线程
/**
* java 8 中 Runable 接口被标注为函数式接口,
* @FunctionaInterface 修饰,可
* 以使用 Lombok 表达式创建对象
*/
Runnable able = () -> {
System.out.println("使用 java 8 Lombok 表达式创建线程,"+Thread.currentThread().getName());
};
Thread t3 = new Thread(able);
t3.start(); // 启动线程
System.out.println("这是主线程 main,"+Thread.currentThread().getName());
}
}
1.实现 Runnable 接口的类的实例对象仅仅作为 Thread 对象的 target,Runnable 实现类里包含的 run() 方法仅仅作为线程执行体,而实际的线程对象依然是 Thread 实例,这里的 Thread 实例负责执行其 Target 的 run() 方法
2.通过实现 Runnable 接口来实现多线程时,要获取当前线程对象只能通过 Thread.currentThread() 方法,而不能通过 this 关键字
3.从 java8 开始,Runnable 接口使用了 @FunctionalInterface 修饰,也就是说 Runnable 接口是函数式接口,可以使用 Lambda 表达式创建对象,使用 Lambda 表达式就可以不向上述代码一样还要创建一个实现 Runnable 接口的类,然后在创建类的实例
通过 Callable 和 Future 接口创建线程
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 休眠两秒
Thread.sleep(2000);
System.out.println("实现 Callable 接口 创建线程");
return "SUCCESS";
}
public static void main(String[] args) throws Exception {
Callable callable = new MyCallable();
/**
* java5 提供了 Future 接口来代表 Callable 接口里的 call() 方法的返回值
* 并为 Future 提供了一个 FutureTask 实现类,该类实现了 Future 接口,并
* 是实现了 Runnable 接口,所以 FutureTask 可以做为 Thread 类的 target
* 同时也解决了 Callable 对象不能作为 Thread 类的 target 这一问题
*/
FutureTask<String> task = new FutureTask<String>(callable);
Thread t1 = new Thread(task,"call");
t1.start();
// 获取返回值
String result = task.get();
System.out.println("返回值:"+result);
/**
* 使用 lambda 表达式创建
*/
FutureTask<Integer> task2 = new FutureTask<Integer>(() -> {
System.out.println("100 分");
return 100;
});
Thread t2 = new Thread(task2,"task2");
t2.start();
// Future 接口 get() 方法获取返回值 会阻塞线程,知道拿到返回值 其他线程才可以运行
Integer resu = task2.get();
System.out.println("返回值:"+resu);
// 使用线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
Callable<String> myCallable = new MyCallable();
// 返回 Future 实例
Future<String> submit = executorService.submit(myCallable);
// 获取返回值,会阻塞线程
String result3 = submit.get();
System.out.println(result3);
System.out.println("main 线程");
}
}
Callable 接口的 call() 方法可以有返回值,可以声明抛出异常,Callable 接口是 java 新增的接口,而他不是 Runnable 接口的子接口,所以 Callable 对象不能直接做为 Thread 的 target ,还有,call() 方法有返回值,call() 方法不是直接调用,而是作为线程执行体被调用,所以涉及获取 call() 方法返回值的问题,于是,java5 提供了 Future 接口来代表 Callable 接口里的 call() 方法的返回值,并为 Future 接口提供了一个 FutureTask 实现类,该类实现了 Future 接口,并实现了 Runnable 接口,所以 FutureTask 可以作为 Thread 类的 target ,同时也解决了 Callable 对象不能做为 Thread 类的 target 这一问题。
Future 接口了定义了几个公共方法来控制与他关联的 callable 任务
1、boolean cancel(boolean mayInterruptIfRunning):试图取消Future里关联的Callable任务;
2、V get():返回Callable任务里call()方法的返回值,调用该方法将导致程序阻塞,必须等到子线程结束以后才会得到返回值;
3、V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后,Callable任务依然没有返回值,将会抛出TimeoutException异常;
4、boolean isCancelled():如果Callable任务正常完成前被取消,则返回true;
5、boolean isDone():如果Callable任务已经完成, 则返回true;
创建线程的具体步骤
1、创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例;
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
3、使用FutureTask对象作为Thread对象的target创建并启动新线程;
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
线程的生命周期
java 的线程状态 6 中
就绪,运行,WATING,TIMED_WATING,BLOCKED,结束
image.png
调用线程的的 start 方法线程进入就绪状态(Runnable),线程调度系统将就绪状态的线程转为运行状态(Running),遇到 synchronized 语句时,有运行状态转为阻塞状态(BLOCKED),当 synchronized 获得锁后,有阻塞状态转为运行状态,当遇到 sleep ,wait 方法,线程转为挂起状态(WATING,TIMED_WATING),当线程关联的代码块执行完成后,线程变为结束状态(Dead)。
线程的启动
线程对象初始化完毕后调用线程的 start() 方法就可以启动线程,而 run() 方法是包裹线程需要执行的代码块
线程的终止
线程的终止,并不是简单的调用 stop 方法。虽然 api 仍然可以调用,但是和其他的线程控制方法如 suspend、resume 一样都是过期了的不建议使用,就拿 stop 来说,stop 方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态。正是因为 suspend()、 resume() 和 stop() 方法 带来的副作用, 这些方法才被标志位不建议使用的过期方法, 而暂停和恢复可以使用 等待/ 通知机制 来替代。
Thread.java类中提供了两个很重要的方法:
1) this.interrupted(): 判断当前线程是否已经中断
2) this.isInterrupted(): 判断线程是否已经中断
public class MyInterrupt implements Runnable {
private int i = 1;
@Override
public void run() {
/**
* Thread.currentThread().isInterrupted()
* 返回会一个中段标记,默认为 false
*/
while (!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+"="+i++);
}
}
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new MyInterrupt();
Thread t1 = new Thread(runnable,"t1");
Thread t2 = new Thread(runnable,"t2");
t1.start(); // 启动线程
t2.start();
Thread.sleep(2000);
/**
* 设置中断标记 interrupt = true
* 他会在当前线程最有一次执行完后通过判断中断标记,结束线程的执行
*/
t1.interrupt();
}
}
上述代码的 t1.interrupt() 只会让 t1 线程中断,而 t2 线程不会
public class MyInterrupt2 implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
// 睡眠 200 秒
try {
TimeUnit.SECONDS.sleep(200);
} catch (InterruptedException e) {
/**
* InterruptedException 触发了线程的复位,线程不会终止即
* Thread.currentThread().isInterrupted() 返回 初始值 false
*/
e.printStackTrace();
// 有人触发中断通知,在这里做处理
// 继续中断,线程会停止
Thread.currentThread().interrupt();
}
}
/**
* 如果没有改变中段标记为 true
* 正常情况下是 200 秒之后才会执行
*/
System.out.println("processor end");
}
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new MyInterrupt2();
Thread t1 = new Thread(runnable,"t1");
t1.start(); // 启动线程
// 为了让 t1 线程充分运行 执行到 run() 方法内
Thread.sleep(1000);
/**
* 发送中断通知 设置中段标记为 true
*/
t1.interrupt();
}
}
上述代码可以让一个正在阻塞的线程是否中断,会抛出一个InterruptedException 异常, InterruptedException 会复位中断标记,不会中断线程,而在 catch 中可以决定如何处理这个中断请求,很明显这个处理中断请求的决定方是在当前线程。
image.png