线程的基本概念
主要介绍一些线程的基本概念,包括创建线程、线程的基本属性和方法、共享内存及问题、线程的优点及成本。
创建线程
线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈。Java中创建线程有两种方式:一种是继承Thread;另外一种是实现Runnable接口。
1 继承Thread
Java中java.lang.Thread这个类表示线程,一个类可以继承Thread类并重写其run()方法来实现一个线程,如:
public class MyThread extends Thread {
@Override
public void run() {
//逻辑处理
}
}
MyThread类继承了Thread,并重写了run()方法。run()方法类似于单线程程序中的main()方法,线程从run()方法第一条语句开始执行直到结束。
线程是需要被启动的,先创建一个MyThread对象,然后调用Thread的start方法。
MyThread thread = new MyThread();
thread.start();
start方法表示启动该线程,使其成为一条单独的执行流,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把每个线程当作一个独立的个体来进行调度,分配时间片让他执行,执行的起点就是run()方法。
怎么确认代码是在哪个线程中执行呢,Thread有一个静态的方法currentThread,返回当前执行的线程对象,每一个Thread都有一个id和name,这样我们可以判断代码是在哪个线程中执行的。
调用了start后,就有了两条执行流,一条执行run方法,一条执行main方法,两条执行流并发执行,操作系统负责调度,在单CPU的机器上,同一时刻只能有一个线程在执行,在多CPU的机器上,同一时刻可以有多个线程同时执行,但操作系统给我们屏蔽了这种差异,给我们的感觉就是多个线程并发执行,但哪条语句先执行哪条后执行是不一定的。当所有线程都执行完毕的时候,程序退出。
2 实现Runnable接口
通过继承Thread类来创建线程虽然比较简单,但Java中只支持单继承,每个类最多只有一个父类。如果该类已经有一个父类,则不能再继承Thread类了。这时可以通过实现java.lang.Runnable接口来实现线程。
无论通过继承Thread类还是实现Runnable接口来创建线程,启动线程都是调用start方法。
线程的基本属性和方法
1.id和name
每一个线程都有一个id和name。id是一个递增的整数,每创建一个线程就加1,name的默认值是Thread-后跟
一个编号,name可以在Thread的构造方法中进行指定,也可以通过setName()方法进行设置,方便调试。
2.优先级
Java中线程有一个优先级的概念,优先级从1-10,默认是5。
public final void setPriority(int newPriority) {
throw new RuntimeException("Stub!");
}
public final int getPriority() {
throw new RuntimeException("Stub!");
}
这个优先级会被映射到操作系统中线程的优先级,不过因为操作系统不同,不一定都是十个优先级。Java中不同的优先级可能会被映射到操作系统中相同的优先级。另外,优先级对操作系统而言是一种建议和提示,而非强制,简单来说,在编程中不要过于依赖线程的优先级。
3.状态
线程有一个状态的概念,Thread有一个方法获取线程的状态。
public Thread.State getState() {
throw new RuntimeException("Stub!");
}
返回的类型是Thread.State,它是一个枚举类型,有如下值:
public static enum State {
BLOCKED,
NEW,
RUNNABLE,
TERMINATED,
TIMED_WAITING,
WAITING;
private State() {
}
}
状态解释:
① NEW:没有调用start的线程状态为NEW
② TERMINATED: 线程运行结束
③ RUNNABLE: 调用start后,线程在执行run方法并且没有阻塞时状态为RUNNABLE,不过RUNNABLE不代表CPU一定在执行该线程的代码,可能正在执行,也可能在等待操作系统分配时间片,只是它没有在等待其他条件
④ BLOCKED TIMED_WAITING WAITING: 都表示线程被阻塞了,在等待一些条件
Thread还有一个方法,返回线程是否活着。
public final boolean isAlive() {
throw new RuntimeException("Stub!");
}
线程被启动后,run方法运行结束前,返回值都是true。
4.是否daemon线程
Thread有一个是否daemon线程的属性,相关方法:
public final void setDaemon(boolean on) {
throw new RuntimeException("Stub!");
}
public final boolean isDaemon() {
throw new RuntimeException("Stub!");
}
之前提到,启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才会退出,但daemon线程是例外,当整个程序中剩下的都是daemon线程的时候,程序就会退出。
daemon线程一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。在我们运行一个最简单的程序的时候,实际上Java也会创建多个线程,除了main线程外,至少还有一个负责垃圾回收的线程,这个线程就是daemon线程,在main线程结束的时候,垃圾回收线程也会退出。
5.sleep方法
Thread有一个静态的sleep方法,调用该方法会让当前的线程睡眠指定的时间,单位是毫秒:
public static void sleep(long millis) throws InterruptedException {
throw new RuntimeException("Stub!");
}
睡眠期间,该线程会让出CPU,但睡眠时间不一定是确切的给定的毫秒数,可能有一定的偏差,偏差与系统定时器和操作系统调度器的准确度和精度有关。睡眠期间,线程可以被中断,如果被中断,sleep会抛出InterruptedException。
6.yield方法
Thread还有一个让出CPU的方法,也是一个静态方法:
public static native void yield();
调用该方法是告诉操作系统调度器:现在不着急使用CPU,可先让其他线程运行。不过这对调度器也仅仅是建议,调度器如何处理是不一定的,它可能完全忽略该调用。
7.join方法
子线程没执行完,main线程可能就执行完了,Thread有一个join方法,可以让main线程等待调用join的线程结束(即等待子线程结束)。也可以在子线程中再开启一个子线程,例如在子线程1中,启动子线程2,子线程2调用join方法,则子线程1会等待子线程2结束。join的方法:
public final void join() throws InterruptedException {
throw new RuntimeException("Stub!");
}
在等待线程结束的过程中,这个等待可能被中断,如果被中断,会抛出InterruptedException。
join方法有一个变体,可以限定等待的最长时间,单位为毫秒,如果为0,表示无期限等待:
public final void join(long millis) throws InterruptedException {
throw new RuntimeException("Stub!");
}
共享内存及可能存在的问题
每个线程表示一条单独的执行流,有自己的程序计数器有自己的栈,但线程之间可以共享内存,它们可以访问和操作相同的对象。不同的执行流可以执行相同的程序代码,在分析代码执行过程时,理解代码在被哪个线程执行是非常重要的。多条执行流执行相同的代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份。当多条执行流操作相同的变量时,可能会出现一些意料之外的结果,包括竞态条件和内存可见性问题。
1.竞态条件
所谓竞态条件是指,当多个线程访问和操作同一个对象时,最终执行结果和执行时序有关,可能正确也可能不正确。示例:
public class MyThread extends Thread {
private static int counter = 0;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter++;
}
}
public static void main(String[] args) {
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new MyThread();
threads[i].start();
}
for (int i = 0; i < 1000; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(counter);
}
}
在main方法中创建了1000个线程,每个线程对变量counter循环加1000次,main线程等所有线程执行结束后返回counter值。期望结果100W,但每次运行发现结果都不是100W, 为什么会这样,因为counter++这个操作不是原子操作,分为三步:
① 取counter当前值
② 当前值基础上加1
③ 将新值重新赋值给counter
比如有两个线程同时取到相同的counter值100,第一个线程执行完递加后,值为101,第二个同样也是101,就造成了结果不准确。
2.内存可见性
多个线程可以共享访问和操作相同的变量,但一个线程对共享变量的修改,另一个线程不一定马上就能看到,甚至永远都看不到。举例如下:
public class VisibilityDemo {
private static boolean isShut = false;
static class ChildThread extends Thread{
@Override
public void run() {
while (!isShut){
}
System.out.println("ChildThread End");
}
}
public static void main(String[] args) throws InterruptedException {
new ChildThread().start();
Thread.sleep(1000);
isShut = true;
System.out.println("mainThread End");
}
}
这段代码中静态变量isShut默认值为false,main方法中启动子线程后,会进入一个死循环,主线程睡眠1S后,将isShut变量赋值为true,此时跳出死循环,子线程结束,主线程也跟着结束。但实际执行发现子线程可能永远都不会退出,也就是说在子线程看来isShut永远为false,即使主线程中已将其更改为true。
这种情况就是内存可见性问题,在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也有可能是先缓存到内存中,稍后才会同步更新到内存中。在单线程中,这一般不是问题,但在多线程的程序中,尤其是多CPU的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时的同步到内存,二是另一线程可能根本就没从内存读取。
线程的优点及成本
优点:
① 充分利用多CPU的计算能力,单线程只能利用一个CPU,使用多线程可以使用多CPU的计算能力。
② 充分利用硬件资源,CPU和硬盘、网络是可以同时工作的,一个线程在等待网络IO的同时,另一个线程完全可以利用CPU,对于多个独立的网络请求,完全可以使用多个线程同时请求。
③ 在用户界面应用程序中,保持程序的响应性,界面和后台任务通常是不同的线程,如果所有事情让一个线程来执行,当执行一个很慢的任务时,整个界面将停止响应,也无法取消该任务。
④ 简化建模及IO处理,比如,在服务器应用程序中,对每个用户请求使用一个单独的线程进行处理,相比使用一个线程,处理来自各种用户的各种需求,以及各种网络和文件IO事件,建模和编写程序要容易得多。
关于线程我们需要知道,它是有成本的。创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等,创建也需要一定的时间。
此外线程调度和切换也是有成本的,当有大量可运行线程的时候,操作系统忙于调度,为一个线程分配一段时间执行完后,再让另一个线程执行,一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前CPU寄存器的值、程序计数器的值等,而一个线程被切换回来后,操作系统需要恢复它原来的上下文状态,整个过程称为上下文切换,这个切换不仅耗时,而且使CPU中的很多缓存失效。当然这是相对而言的,如果线程中实际执行的事情比较多,这些成本是可以接受的。另外,如果执行的任务都是CPU密集型的,即主要消耗的都是CPU,那创建超过CPU数量的线程就是没有必要的,并不会加快程序的执行。
拓展
CPU:中央处理器 central processing unit 计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单位。电子计算机的主要设备之一,核心配件。其功能主要是解释计算机指令以及处理计算机软件中的数据。CPU是计算机中负责读取指令,对指令译码并执行指令的核心部件。中央处理器主要包括两个部分,即控制器、运算器,其中还包括高速缓冲存储器及实现它们之间联系的数据、控制的总线。
我们接触的电脑基本上都只有一个CPU,CPU的个数很容易得到。为了给电脑更高的性能,一个CPU中集成了了多个内核,这样电脑的性能就能成倍的提升。随着科技发展,每个内核的性能也十分的强大,于是一个内核又被分成两个线程。我们需要注意的是,一个CPU分成多个内核,这是物理的分隔,拆开CPU是可以看到的,但是一个内核被分成两个线程是一种超线程技术,操作系统会认为一个线程也是一个内核,有点欺骗操作系统的感觉。若说CPU有四核,这时我们要提高警惕,是真四核还是四线程(假四核)