按照规划,从本篇开始我们开启『并发』系列内容的总结,从本篇的线程开始,到线程池,到几种并发集合源码的分析,我们一点点来,希望你也有耐心,因为并发这块知识是你职业生涯始终绕不过的坎,任何一个项目都或多或少的要涉及一些并发的处理。
这一系列文章只能算是对并发这块基本理论知识的一个总结与介绍,想要成为并发高手,必然是需要通过大规模并发访问的线上场景应用,或许以后我有了相关经验了,再给你们做一点分享吧。
基本的进程线程概念
进程和线程算是操作系统内两个很基本、很重要的概念了,进程是操作系统中进行保护和资源分配的基本单位,操作系统分配资源以进程为基本单位。而线程是进程的组成部分,它代表了一条顺序的执行流。
系统中的进程线程模型是这样的:
进程从操作系统获得基本的内存空间,所有的线程共享着进程的内存地址空间。当然,每个线程也会拥有自己私有的内存地址范围,其他线程不能访问。
由于所有的线程共享进程的内存地址空间,所以线程间的通信就容易的多,通过共享进程级全局变量即可实现。
同时,在没有引入多线程概念之前,所谓的『并发』是发生在进程之间的,每一次的进程上下文切换都将导致系统调度算法的运行,以及各种 CPU 上下文的信息保存,非常耗时。而线程级并发没有系统调度这一步骤,进程分配到 CPU 使用时间,并给其内部的各个线程使用。
在分时系统中,进程中的每个线程都拥有一个时间片,时间片结束时保存 CPU 及寄存器中的线程上下文并交出 CPU,完成一次线程间切换。当然,当进程的 CPU 时间使用结束时,所有的线程必然被阻塞。
JAVA 对线程概念的抽象
JAVA API 中用 Thread 这个类抽象化描述线程,线程有几种状态:
- NEW:线程刚被创建
- RUNNABLE:线程处于可执行状态
- BLOCKED、WAITING:线程被阻塞,具体区别后面说
- TERMINATED:线程执行结束,被终止
其中 RUNNABLE 表示的是线程可执行,但不代表线程一定在获取 CPU 执行中,可能由于时间片使用结束而等待系统的重新调度。BLOCKED、WAITING 都是由于线程执行过程中缺少某些条件而暂时阻塞,一旦它们等待的条件满足时,它们将回到 RUNNABLE 状态重新竞争 CPU。
此外,Thread 类中还有一些属性用于描述一个线程对象:
- private long tid:线程的序号
- private volatile char name[]:线程的名称
- private int priority:线程的优先级
- private boolean daemon = false:是否是守护线程
- private Runnable target:该线程需要执行的方法
其中,tid 是一个自增的字段,每创建一个新线程,这个 id 都会自增一。优先级取值范围,从一到十,数值越大,优先级越高,默认值为五。
线程案例:
package demo.knowledgepoints.scheduledtask.run; /***
* 线程类
*/
public class ThreadTest implements Runnable { public static int ticket = 9; @Override
public void run() {
try {
System.out.println("当前线程:"+Thread.currentThread().getName());
while(true){
synchronized (this) {
Thread.sleep(1000L);
if (this.ticket > 0) {
ticket--;
System.out.println(Thread.currentThread().getName() + ":出售一张票!");
System.out.println("剩余票量:" + ticket);
} else {
System.out.println("没有票了!");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
package demo.knowledgepoints.scheduledtask.run; public class ThreadMemo {
public static void main(String[] args) {
ThreadTest threadTest1 =new ThreadTest();
new Thread(threadTest1).start();
new Thread(threadTest1).start();
}
}
运行结果
从案例中我们可以知道:ThreadTest 继承了 Runnable 接口,并且实现了run方法。
在 ThreadMemo 中,我们创建ThreadTest,并且是执行了start() 方法,并没有直接执行run方法。
从运行结果我们可以看出:我们运行了run方法里面的程序。
并且线程1和线程0 交替执行。
下面我们来看看这个线程的执行过程。
Runnable 是一个接口,它抽象化了一个线程的执行流,定义如下:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
第一步:实现接口Runnable接口。
第二步:创建一个线程,并且将实现Runnable接口的类放进去。
第三步:使用线程的start() 方法,启动线程。这个时候,Jvm会自动去找到run方法,并且执行run方法,同时不影响主进程继续往下执行。
线程案例:
public class Thread1 extends Thread { public static int ticket = 5; @Override
public void run() {
try {
System.out.println("当前线程:"+Thread.currentThread().getName());
while(true){
synchronized (this) {
Thread.sleep(2000L);
if (this.ticket > 0) {
ticket--;
System.out.println(Thread.currentThread().getName() + ":出售一张票!"+"剩余票量:" + ticket);
} else {
System.out.println("没有票了!");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
Thread thread = new Thread1();
thread.start();
System.out.println("主线程!");
}
}
运行结果:
这是第二种线程实现方式:继承 Thread 类。
看Thread 类:
class Thread implements Runnable
其实就是实现了Runnable 接口。
列举一下:Thread 方法。
1、sleep
public static native void sleep(long millis)
这是一个本地方法,用于阻塞当前线程指定毫秒时长。
2、start
public synchronized void start()
这个方法可能很多人会疑惑,为什么我通过重写 Runnable 的 run 方法指定了线程的工作,但却是通过 start 方法来启动线程的?
那是因为,启动一个线程不仅仅是给定一个指令开始入口即可,操作系统还需要在进程的共享内存空间中划分一部分作为线程的私有资源,创建程序计数器,栈等资源,最终才会去调用 run 方法。
3、interrupt
public void interrupt()
这个方法用于中断当前线程,当然线程的不同状态应对中断的方式也是不同的,这一点我们后面再说。
4、join
public final synchronized void join(long millis)
这个方法一般在其他线程中进行调用,指明当前线程需要阻塞在当前位置,等待目标线程所有指令全部执行完毕。