并发——线程(二)

从CPU开始

CPU并不知道线程、进程之类的概念。

CPU只知道两件事:

1)从内存中取出指令;
2)执行指令,然后回到 1)。

CPU是从寄存器中取出指令执行,寄存器中有函数编译后形成的第一条指令,既函数入口。

操作系统和进程

这里有个问题,我们将程序从磁盘在内存中找到一块合适的区域,跑起来,找到函数入口,写入PC寄存器,交给CPU执行。

这一系列过程如果让程序员自己来执行,将会是很复杂的操作,如果把以上步骤结合起来作为一个程序,可以称为操作系统

程序从磁盘加载到内存跑起来叫什么好呢?干脆就叫进程(Process)好了

从单核到多核

如果一个程序要利用到多核怎么办? 开更多进程吗?

但是同时会存在一下问题:

  • 进程需要独占空间,浪费内存
  • 进程通信麻烦,需要借助操作系统,增大了系统开销。

从进程到线程

所谓进程无非就是内存中的一段区域,这段区域中保存了CPU执行的机器指令以及函数运行时的堆栈信息,要想让进程运行,就把main函数的第一条机器指令地址写入PC寄存器,这样进程就运行起来了。

并发——线程(二)_2021-04-01-16-56-05.png

进程的缺点在于只有一个入口函数,也就是main函数,因此进程中的机器指令只能被一个CPU执行,那么有没有办法让多个CPU来执行同一个进程中的机器指令呢?

那么我们在程序中设计多个可执行函数交给CPU去执行,如下:

并发——线程(二)_2021-04-01-17-04-36.png

多个CPU可以在同一个屋檐下(进程占用的内存区域)同时执行属于该进程的多个入口函数,也就是说现在一个进程内可以有多个执行流了。

这些个执行流就是线程

操作系统为每个进程维护了一堆信息,用来记录进程所处的内存空间等,这堆信息记为数据集A。

同样的,操作系统也需要为线程维护一堆信息,用来记录线程的入口函数或者栈信息等,这堆数据记为数据集B。

显然数据集B要比数据A的量要少,同时不像进程,创建一个线程时无需去内存中找一段内存空间,因为线程是运行在所处进程的地址空间的。

注意:

由于多线程共享工作空间,出现的线程安全问题需要自己解决。

并不是多核才能使用多线程,单核也可以,方法就是将CPU的时间片在各个线程之间来回分配,这样多个线程看起来就是“同时”运行了,但实际上任意时刻还是只有一个线程在运行。

啥是多线程? 串行,并行,并发

多线程: 指的是这个程序(一个进程)运行时产生了不止一个线程。

并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行。

并行:是说在单位时间内多个任务同时在执行 。

注:在多线程编程实践中,线程的个数往往多于 CPU 的个数,所以一般都称多线程并发编程而不是多线程并行编程。

同步和异步

同步往往意味着双方要相互等待、相互依赖,而异步意味着双方相互独立、各行其是。

线程的状态

JAVA并发——线程_2020-03-19-17-36-41.png

JAVA并发——线程_2020-03-19-17-38-13.png

  1. 初始化状态:新建一个线程对象

  2. 可运行状态:其他线程调用了该线程对象的 start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权

  3. 运行状态:可运行状态的线程获得了 cpu 时间片(timeslice),执行程序代码

  4. 阻塞状态:线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程再次进入可运行状态,才有机会转到运行状态。如图所示,会有三种不同类型的阻塞状态:

    • 等待阻塞:运行中的线程执行 wait()方法,线程会进入等待队列中。等待 notify()、notifyAll()或 interrupt()对其唤醒或中断
    • 同步阻塞:运行中的线程执行在获取同步锁(注:只有 synchronized 这种方式的锁(monitor 锁)才会让线程出现 BLOCKED 状态,等待 ReentrantLock 则不会)时,若该锁已被其他线程占用,线程则会进入锁池队列。等待获取到锁
    • 其他阻塞:运行的线程执行 sleep()、join(),或触发了 I/O 请求,该该线程被置为阻塞状态。当 sleep()状态超时、join()等待线程终止或超时、I/O 处理完成,线程会重新进入可运行状态。
  5. 死亡状态:线程执行完或因异常退出 run()方法,线程生命周期结束

常见方法

  1. Thread.sleep(long millis) 静态方法。当前线程调用此方法,使当前线程进入阻塞状态(其他阻塞),但不释放任何锁资源,一定时间后线程自动进入 runnable 状态。给其它线程执行机会的最佳方式。

  2. obj.wait()obj.wait(long timeout) 当前线程调用某对象的 wait()方法,当前线程释放对象锁(wait 一定在 synchronized 代码块/方法中,故一定得到了锁,才进来的此方法),进入阻塞状态(等待队列)。等待 notify 或 wait 设置的 timeout 到期,方可进入另外一个阻塞状态(锁池)。

  3. t.join()t.join(long millis) 非静态方法。当前线程 A 执行过程中,调用 B 线程的 join 方法,使当前线程进入阻塞状态(其他阻塞),但不释放对象锁,等待 B 线程执行完后或一定时间 millis 后,A 线程进入 runnable 状态。

  4. Thread.yield() 静态方法。当前线程调用此方法,使线程由 running 态进入 runnable 态,放弃 cpu 使用权,让 cpu 再次选择要执行的线程。 注:实际过程中,yield 仅仅是让其它具有同等优先级的 runnable 线程获取执行权,但并不能保证其它具有同等优先级的线程就一定能获得 cpu 执行权。因为做出让步的当前线程,可能会被 cpu 再次选中,进入 running 状态。yield()不会导致阻塞。

线程创建

  1. 继承 Thread

JAVA并发——线程_2020-03-19-17-42-47.png

在这里我们思考一下为什么不直接调用 run()方法而是去调用 start()方法?

线程的 run()方法是由 java 虚拟机直接调用的

如果我们没有启动线程(没有调用线程的 start()方法)而是在应用代码中直接调用 run()方法,那么这个线程的 run()方法其实运行在当前线程(即 run()方法的调用方所在的线程)之中,而不是运行在其自身的线程中。

所以就是先访问 start 创建线程 jvm 去调用 run()方法!

这种方式的弊端是一个类只能继承一个父类,如果这个类本身已经继承了其它类,就不能使用这种方式了。

  1. 实现 Runnable 接口

JAVA并发——线程_2020-03-19-17-48-06.png

实现 Runnable 接口,这种方式的好处是一个类可以实现多个接口,不影响其继承体系。

  1. 实现 Callabe 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CreatingThread04 implements Callable<Long> {
@Override
public Long call() throws Exception {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId() + " is running");
return Thread.currentThread().getId();
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Long> task = new FutureTask<>(new CreatingThread04());
new Thread(task).start();
System.out.println("等待完成任务");
Long result = task.get();
System.out.println("任务结果:" + result);
}
}

FutureTask 可用于异步获取执行结果或取消执行任务的场景。通过传入 Runnable 或者 Callable 的任务给 FutureTask,直接调用其 run 方法或者放入线程池执行,之后可以在外部通过 FutureTask 的 get 方法异步获取执行结果,因此,FutureTask 非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。

  1. 定时器
1
2
3
4
5
6
7
8
9
10
11
12
public class CreatingThread05 {
public static void main(String[] args) {
Timer timer = new Timer();
// 每隔1秒执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}, 0 , 1000);
}
}

使用定时器 java.util.Timer 可以快速地实现定时任务,TimerTask 实际上实现了 Runnable 接口。

总结

上文我们讲到了创建线程的几种方法,其中最常见的是继承 Thread 类与实现 Runable 接口,其实 Thread 类的源码也是实现 Runnbale。

Thread类的start()方法调用了native方法start0,start0方法又调用了Runable的run方法,所以Thread必须实现Runable接口,实现Runnable的接口的类要交由Thread去执行。

线程执行的基本为Runble。

相比 Thread,使用 Runnale 创建线程,由以下好处:

  • 资源共享。
  • java 不允许多继承,因此实现了 Runnable 接口的类可以再继承其他类。
  • 松耦合,将真正处理线程的操作交给一个个 Thread 对象操作。
文章目录
  1. 1. 从CPU开始
  2. 2. 操作系统和进程
  3. 3. 从单核到多核
  4. 4. 从进程到线程
  5. 5. 啥是多线程? 串行,并行,并发
  6. 6. 同步和异步
  7. 7. 线程的状态
  8. 8. 常见方法
  9. 9. 线程创建
  10. 10. 总结
|