线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。
并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。
并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
区别:并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。
通俗的来说,并行其实就是,多个任务用多个CPU同时运行。而并发则是多个任务由一个CPU通过切换时间片运行,并发并不是真的再同时运行他们,只是给人的感觉,效果上看起来像是。
Java中java.lang.Thread这个类表示线程,一个类可以继承Thread并重写其run方法来实现一个线程
public class Test extends Thread{
@Override
public void run() {
System.out.println("run");
}
public static void main(String[] args) {
Thread thread = new Test();
thread.start();
}
}
Test这个类继承了Thread,并重写了run方法。run方法的方法签名是固定的,public,没有参数,没有返回值,不能抛出受检异常。run方法类似于单线程程序中的main方法,线程从run方法的第一条语句开始执行直到结束。定义了这个类不代表代码就会开始执行,线程需要被启动,启动需要先创建一个Test对象,然后调用Thread的start方法
start表示启动该线程,使其成为一条单独的执行流,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行,执行的起点就是run方法。
//每个Thread都有一个id和name:
public class Test extends Thread{
@Override
public void run() {
System.out.println("run");
System.out.println(Thread.currentThread().getId());
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread thread = new Test();
thread.start();
}
}
通过继承Thread来实现线程虽然比较简单,但Java中只支持单继承,每个类最多只能有一个父类,如果类已经有父类了,就不能再继承Thread,这时,可以通过实现java.lang.Runnable接口来实现线程。Runnable接口的定义很简单,只有一个run方法。当然,仅仅实现Runnable是不够的,要启动线程,还是要创建一个Thread对象。
public class Test implements Runnable{
public static void main(String[] args) {
Runnable runnable = new Test();
Thread thread = new Thread(runnable);
thread.start();
}
@Override
public void run() {
System.out.println("run");
}
}
每个线程都有一个id和name。id是一个递增的整数,每创建一个线程就加一。name的默认值是Thread-后跟一个编号,name可以在Thread的构造方法中进行指定,也可以通过setName方法进行设置,给Thread设置一个友好的名字,可以方便调试。
线程有一个优先级的概念,在Java中优先级从1到10,默认为5
public final void setPriority(int newPriority)
public final int getPriority()
这种优先级会被映射到操作系统中的线程优先级,不过并不是所有操作系统都是10个优先级。Java中不同的优先级可能会被映射到操作系统中相同的优先级。另外,优先级对操作系统而言主要是给了一点建议和提示,并不是强制如此。
线程还有一个状态的概念,Thread有一个方法用于获取线程的状态。
public State getState()
public enum State {
//NEW:没有调用start的线程状态为NEW。
NEW,
/*
RUNNABLE:调用start后线程在执行run方法且没有阻塞时状态为RUNNABLE,不过,RUNNABLE不代表CPU一定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,只是它没有在等待其他条件。
*/
RUNNABLE,
/*
BLOCKED、WAITING、TIMED_WAITING:都表示线程被阻塞了,在等待一些条件
*/
BLOCKED,
WAITING,
TIMED_WAITING,
//TERMINATED:线程运行结束后状态为TERMINATED。
TERMINATED;
}
Thread中还有一个方法可以检查出线程是否还或者,当线程启动后,run方法运行结束前,它的返回值都是true
public final native boolean isAlive()
启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才退出,但daemon线程是例外,当整个程序中剩下的都是daemon线程的时候,程序就会退出。
Thread有一个是否daemon线程的属性
public final void setDaemon(boolean on)
public final boolean isDaemon()
daemon一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。在我们运行一个即使最简单的"hello world"类型的程序时,实际上,Java也会创建多个线程,除了main线程外,至少还有一个负责垃圾回收的线程,这个线程就是daemon线程,在main线程结束的时候,垃圾回收线程也会退出。
Thread有一个静态的sleep方法,调用该方法会让当前线程睡眠指定的时间,单位是毫秒
public static native void sleep(long millis) throws InterruptedException;
睡眠期间,该线程会让出CPU,但睡眠的时间不一定是确切的给定毫秒数,可能有一定的偏差,偏差与系统定时器和操作系统调度器的准确度和精度有关。睡眠期间,线程可以被中断,如果被中断,sleep会抛出InterruptedException
//可以让出CPU:
public static native void yield();
这也是一个静态方法,调用该方法,是告诉操作系统的调度器:我现在不着急占用CPU,你可以先让其他线程运行。不过,这对调度器也仅仅是建议,调度器如何处理是不一定的,它可能完全忽略该调用。
可以让调用join的线程等待该线程结束
public final void join() throws InterruptedException
在等待线程结束的过程中,这个等待可能被中断,如果被中断,会抛出Interrupted-Exception。join方法还有一个变体,可以限定等待的最长时间,单位为毫秒,如果为0,表示无期限等待
public final synchronized void join(long millis) throws InterruptedException
如果希望main线程在子线程结束后再退出,main方法可以改为
public class Test implements Runnable{
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Test();
Thread thread = new Thread(runnable);
thread.start();
thread.join();
}
@Override
public void run() {
System.out.println("run");
}
}
每个线程表示一条单独的执行流,有自己的程序计数器,有自己的栈,但线程之间可以共享内存,它们可以访问和操作相同的对象。
public class Test{
private static int shared = 0;
private static void incrShared(){
shared++;
}
static class ChildThread extends Thread {
List<String> list;
public ChildThread(List<String> list) {
this.list = list;
}
@Override
public void run() {
incrShared();
list.add(Thread.currentThread().getName());
}
}
public static void main(String[]args) throws InterruptedException {
List<String> list = new ArrayList<String>();
Thread t1 = new ChildThread(list);
Thread t2 = new ChildThread(list);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(shared);
System.out.println(list);
}
}
在这里有三条执行流,一条执行main方法,另外两条执行ChildThread的run方法。不同执行流可以访问和操作相同的变量,比如这里面的shared和list变量。不同执行流可以执行相同的程序代码,就像这里面的incrShared方法,ChildThread的run方法,被两条ChildThread执行流执行,incrShared方法是在外部定义的,但被ChildThread的执行流执行。当多条执行流执行相同的程序代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份。
当多条执行流可以操作相同的变量时,可能会出现一些意料之外的结果,包括竞态条件和内存可见性问题
当多个线程访问和操作同一个对象时,最终执行结果与执行时序有关,可能正确也可能不正确。
public class Test 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) throws InterruptedException {
int num = 1000;
Thread[]threads = new Thread[num];
for(int i = 0; i < num; i++) {
threads[i]= new Test();
threads[i].start();
}
for(int i = 0; i < num; i++) {
threads[i].join();
}
System.out.println(counter);
}
}
有一个共享静态变量counter,初始值为0,在main方法中创建了1000个线程,每个线程对counter循环加1000次,main线程等待所有线程结束后输出counter的值。 期望的结果是100万,但实际执行,发现每次输出的结果都不一样,一般都不是100万,经常是99万多。为什么会这样呢?因为counter++这个操作不是原子操作
这个问题可能就需要使用synchronized关键字,或者使用显式锁、原子变量等方法来解决了
多个线程可以共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就能看到,甚至永远也看不到。
public class Test extends Thread{
private static boolean shutdown = false;
static class My{
public void run() {
while(!shutdown){
}
System.out.println("exit hello");
}
}
public static void main(String[]args) throws InterruptedException {
new Test().start();
Thread.sleep(1000);
shutdown = true;
System.out.println("exit main");
}
}
在这个程序中,有一个共享的boolean变量shutdown,初始为false,My在shutdown不为true的情况下一直死循环,当shutdown为true时退出并输出"exit hello",main线程启动My后休息了一会儿,然后设置shutdown为true,最后输出"exit main"。 期望的结果是两个线程都退出,但实际执行时,很可能会发现HelloThread永远都不会退出,也就是说,在My执行流看来,shutdown永远为false,即使main线程已经更改为了true。
这就是内存可见性问题。在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能是先写到缓存中,稍后才会同步更新到内存中。在单线程的程序中,这一般不是问题,但在多线程的程序中,尤其是在有多CPU的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存,二是另一个线程根本就没从内存读。
解决这个问题可以使用volatile关键字或者synchronized
创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等,创建也需要一定的时间。此外,线程调度和切换也是有成本的,当有大量可运行线程的时候,操作系统会忙于调度,为一个线程分配一段时间,执行完后,再让另一个线程执行,一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前CPU寄存器的值、程序计数器的值等,而一个线程被切换回来后,操作系统需要恢复它原来的上下文状态,整个过程称为上下文切换,这个切换不仅耗时,而且使CPU中的很多缓存失效。
如果线程中实际执行的事情比较多,这些成本是可以接受的;另外,如果执行的任务都是CPU密集型的,即主要消耗的都是CPU,那创建超过CPU数量的线程就是没有必要的,并不会加快程序的执行。