Java线程的基本概念

在之前的章节中,我们都是假设程序中只有一条执行流,程序从main方法的第一条语句逐条执行直到结束。从本节开始,我们讨论并发,在程序中创建线程来启动多条执行流,并发和线程是一个复杂的话题,本节,我们先来讨论Java中线程的一些基本概念。

创建线程

线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈。下面,我们通过创建线程来对线程建立一个直观感受,在Java中创建线程有两种方式,一种是继承Thread,另外一种是实现Runnable接口,我们先来看第一种。

继承Thread

Java中java.lang.Thread这个类表示线程,一个类可以继承Thread并重写其run方法来实现一个线程,如下所示:

public class HelloThread extends Thread {
 @Override
 public void run() {
 System.out.println("hello");
 }
}

HelloThread这个类继承了Thread,并重写了run方法。run方法的方法签名是固定的,public,没有参数,没有返回值,不能抛出受检异常。run方法类似于单线程程序中的main方法,线程从run方法的第一条语句开始执行直到结束。

定义了这个类不代表代码就会开始执行,线程需要被启动,启动需要先创建一个HelloThread对象,然后调用Thread的start方法,如下所示:

public static void main(String[] args) {
 Thread thread = new HelloThread();
 thread.start();
}

我们在main方法中创建了一个线程对象,并调用了其start方法,调用start方法后,HelloThread的run方法就会开始执行,屏幕输出:

hello

为什么调用的是start,执行的却是run方法呢?start表示启动该线程,使其成为一条单独的执行流,背后,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行,执行的起点就是run方法。

如果不调用start,而直接调用run方法呢?屏幕的输出并不会发生变化,但并不会启动一条单独的执行流,run方法的代码依然是在main线程中执行的,run方法只是main方法调用的一个普通方法。

怎么确认代码是在哪个线程中执行的呢?Thread有一个静态方法currentThread,返回当前执行的线程对象:

public static native Thread currentThread();

每个Thread都有一个id和name:

public long getId()
public final String getName()

这样,我们就可以判断代码是在哪个线程中执行的,我们在HelloThead的run方法中加一些代码:

@Override
public void run() {
 System.out.println("thread name: "+ Thread.currentThread().getName());
 System.out.println("hello");
}

如果在main方法中通过start方法启动线程,程序输出为:

thread name: Thread-0
hello

如果在main方法中直接调用run方法,程序输出为:

thread name: main
hello

调用start后,就有了两条执行流,新的一条执行run方法,旧的一条继续执行main方法,两条执行流并发执行,操作系统负责调度,在单CPU的机器上,同一时刻只能有一个线程在执行,在多CPU的机器上,同一时刻可以有多个线程同时执行,但操作系统给我们屏蔽了这种差异,给程序员的感觉就是多个线程并发执行,但哪条语句先执行哪条后执行是不一定的。当所有线程都执行完毕的时候,程序退出。

实现Runnable接口

通过继承Thread来实现线程虽然比较简单,但我们知道,Java中只支持单继承,每个类最多只能有一个父类,如果类已经有父类了,就不能再继承Thread,这时,可以通过实现java.lang.Runnable接口来实现线程。

Runnable接口的定义很简单,只有一个run方法,如下所示:

public interface Runnable {
 public abstract void run();
}

一个类可以实现该接口,并实现run方法,如下所示:

public class HelloRunnable implements Runnable {
 @Override
 public void run() {
  System.out.println("hello");
 }
}

仅仅实现Runnable是不够的,要启动线程,还是要创建一个Thread对象,但传递一个Runnable对象,如下所示:

public static void main(String[] args) {
 Thread helloThread = new Thread(new HelloRunnable());
 helloThread.start();
}

无论是通过继承Thead还是实现Runnable接口来实现线程,启动线程都是调用Thread对象的start方法。

线程的基本属性和方法

id和name

前面我们提到,每个线程都有一个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()

返回值类型为Thread.State,它是一个枚举类型,有如下值:

public enum State {
 NEW,
 RUNNABLE,
 BLOCKED,
 WAITING,
 TIMED_WAITING,
 TERMINATED;
}

关于这些状态,我们简单解释下:

  • NEW: 没有调用start的线程状态为NEW
  • TERMINATED: 线程运行结束后状态为TERMINATED
  • RUNNABLE: 调用start后线程在执行run方法且没有阻塞时状态为RUNNABLE,不过,RUNNABLE不代表CPU一定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,只是它没有在等待其他条件
  • BLOCKED、WAITING、TIMED_WAITING:都表示线程被阻塞了,在等待一些条件,其中的区别我们在后续章节再介绍

Thread还有一个方法,返回线程是否活着:

public final native boolean isAlive()

线程被启动后,run方法运行结束前,返回值都是true。

是否daemo线程

Thread有一个是否daemo线程的属性,相关方法是:

public final void setDaemon(boolean on)
public final boolean isDaemon()

前面我们提到,启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才退出,但daemo线程是例外,当整个程序中剩下的都是daemo线程的时候,程序就会退出。

daemo线程有什么用呢?它一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。在我们运行一个即使最简单的"hello world"类型的程序时,实际上,Java也会创建多个线程,除了main线程外,至少还有一个负责垃圾回收的线程,这个线程就是daemo线程,在main线程结束的时候,垃圾回收线程也会退出。

sleep方法

Thread有一个静态的sleep方法,调用该方法会让当前线程睡眠指定的时间,单位是毫秒:

public static native void sleep(long millis) throws InterruptedException;

睡眠期间,该线程会让出CPU,但睡眠的时间不一定是确切的给定毫秒数,可能有一定的偏差,偏差与系统定时器和操作系统调度器的准确度和精度有关。

睡眠期间,线程可以被中断,如果被中断,sleep会抛出InterruptedException,关于中断以及中断处理,我们后续章节再介绍。

yield方法

Thread还有一个让出CPU的方法:

public static native void yield();

这也是一个静态方法,调用该方法,是告诉操作系统的调度器,我现在不着急占用CPU,你可以先让其他线程运行。不过,这对调度器也仅仅是建议,调度器如何处理是不一定的,它可能完全忽略该调用。

join方法

在前面HelloThread的例子中,HelloThread没执行完,main线程可能就执行完了,Thread有一个join方法,可以让调用join的线程等待该线程结束,join方法的声明为:

public final void join() throws InterruptedException

在等待线程结束的过程中,这个等待可能被中断,如果被中断,会抛出InterruptedException。

join方法还有一个变体,可以限定等待的最长时间,单位为毫秒,如果为0,表示无期限等待:

public final synchronized void join(long millis) throws InterruptedException

在前面的HelloThread示例中,如果希望main线程在子线程结束后再退出,main方法可以改为:

public static void main(String[] args) throws InterruptedException {
 Thread thread = new HelloThread();
 thread.start();
 thread.join();
}

过时方法

Thread类中还有一些看上去可以控制线程生命周期的方法,如:

public final void stop()
public final void suspend()
public final void resume()

这些方法因为各种原因已被标记为了过时,我们不应该在程序中使用它们。

共享内存及问题

共享内存

前面我们提到,每个线程表示一条单独的执行流,有自己的程序计数器,有自己的栈,但线程之间可以共享内存,它们可以访问和操作相同的对象。我们看个例子,代码如下:

public class ShareMemoryDemo {
 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);
 }
}

在代码中,定义了一个静态变量shared和静态内部类ChildThread,在main方法中,创建并启动了两个ChildThread对象,传递了相同的list对象,ChildThread的run方法访问了共享的变量shared和list,main方法最后输出了共享的shared和list的值,大部分情况下,会输出期望的值:

[Thread-0, Thread-1]

通过这个例子,我们想强调说明执行流、内存和程序代码之间的关系。

该例中有三条执行流,一条执行main方法,另外两条执行ChildThread的run方法。

  • 不同执行流可以访问和操作相同的变量,如本例中的shared和list变量。
  • 不同执行流可以执行相同的程序代码,如本例中incrShared方法,ChildThread的run方法,被两条ChildThread执行流执行,incrShared方法是在外部定义的,但被ChildThread的执行流执行,在分析代码执行过程时,理解代码在被哪个线程执行是很重要的。
  • 当多条执行流执行相同的程序代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份。

当多条执行流可以操作相同的变量时,可能会出现一些意料之外的结果,我们来看下。

竞态条件

所谓竞态条件(race condition)是指,当多个线程访问和操作同一个对象时,最终执行结果与执行时序有关,可能正确也可能不正确,我们看一个例子:

public class CounterThread extends Thread {
 private static int counter = 0;
 @Override
 public void run() {
  try {
   Thread.sleep((int)(Math.random()*100));
  } catch (InterruptedException e) {
  }
  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 CounterThread();
   threads[i].start();
  }
  for(int i=0; i<num; i++){
   threads[i].join();
  }
  System.out.println(counter);
 }
}

这段代码容易理解,有一个共享静态变量counter,初始值为0,在main方法中创建了1000个线程,每个线程就是随机睡一会,然后对counter加1,main线程等待所有线程结束后输出counter的值。

期望的结果是1000,但实际执行,发现每次输出的结果都不一样,一般都不是1000,经常是900多。为什么会这样呢?因为counter++这个操作不是原子操作,它分为三个步骤:

  • 取counter的当前值
  • 在当前值基础上加1
  • 将新值重新赋值给counter

两个线程可能同时执行第一步,取到了相同的counter值,比如都取到了100,第一个线程执行完后counter变为101,而第二个线程执行完后还是101,最终的结果就与期望不符。

怎么解决这个问题呢?有多种方法:

  • 使用synchronized关键字
  • 使用显式锁
  • 使用原子变量

关于这些方法,我们在后续章节再介绍。

内存可见性

多个线程可以共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就能看到,甚至永远也看不到,这可能有悖直觉,我们来看一个例子。

public class VisibilityDemo {
 private static boolean shutdown = false;
 static class HelloThread extends Thread {
  @Override
  public void run() {
   while(!shutdown){
    // do nothing
   }
   System.out.println("exit hello");
  }
 }
 public static void main(String[] args) throws InterruptedException {
  new HelloThread().start();
  Thread.sleep(1000);
  shutdown = true;
  System.out.println("exit main");
 }
}

在这个程序中,有一个共享的boolean变量shutdown,初始为false,HelloThread在shutdown不为true的情况下一直死循环,当shutdown为true时退出并输出"exit hello",main线程启动HelloThread后睡了一会,然后设置shutdown为true,最后输出"exit main"。

期望的结果是两个线程都退出,但实际执行,很可能会发现HelloThread永远都不会退出,也就是说,在HelloThread执行流看来,shutdown永远为false,即使main线程已经更改为了true。

这是怎么回事呢?这就是内存可见性问题。在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能是先写到缓存中,而稍后才会同步更新到内存中。在单线程的程序中,这一般不是个问题,但在多线程的程序中,尤其是在有多CPU的情况下,这就是个严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存,二是另一个线程根本就没从内存读。

怎么解决这个问题呢?有多种方法:

  • 使用volatile关键字
  • 使用synchronized关键字或显式锁同步

关于这些方法,我们在后续章节再介绍。

线程的优点及成本

优点

为什么要创建单独的执行流?或者说线程有什么优点呢?至少有以下几点:

  • 充分利用多CPU的计算能力,单线程只能利用一个CPU,使用多线程可以利用多CPU的计算能力。
  • 充分利用硬件资源,CPU和硬盘、网络是可以同时工作的,一个线程在等待网络IO的同时,另一个线程完全可以利用CPU,对于多个独立的网络请求,完全可以使用多个线程同时请求。
  • 在用户界面(GUI)应用程序中,保持程序的响应性,界面和后台任务通常是不同的线程,否则,如果所有事情都是一个线程来执行,当执行一个很慢的任务时,整个界面将停止响应,也无法取消该任务。
  • 简化建模及IO处理,比如,在服务器应用程序中,对每个用户请求使用一个单独的线程进行处理,相比使用一个线程,处理来自各种用户的各种请求,以及各种网络和文件IO事件,建模和编写程序要容易的多。

成本

关于线程,我们需要知道,它是有成本的。创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等,创建也需要一定的时间。

此外,线程调度和切换也是有成本的,当有当量可运行线程的时候,操作系统会忙于调度,为一个线程分配一段时间,执行完后,再让另一个线程执行,一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前CPU寄存器的值、程序计数器的值等,而一个线程被切换回来后,操作系统需要恢复它原来的上下文状态,整个过程被称为上下文切换,这个切换不仅耗时,而且使CPU中的很多缓存失效,是有成本的。

当然,这些成本是相对而言的,如果线程中实际执行的事情比较多,这些成本是可以接受的,但如果只是执行本节示例中的counter++,那相对成本就太高了。

另外,如果执行的任务都是CPU密集型的,即主要消耗的都是CPU,那创建超过CPU数量的线程就是没有必要的,并不会加快程序的执行。

小结

本节,我们介绍了Java中线程的一些基本概念,包括如何创建线程,线程的一些基本属性和方法,多个线程可以共享内存,但共享内存也有两个重要问题,一个是竞态条件,另一个是内存可见性,最后,我们讨论了线程的一些优点和成本。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持我们!

(0)

相关推荐

  • 详谈java线程与线程、进程与进程间通信

    线程与线程间通信 一.基本概念以及线程与进程之间的区别联系: 关于进程和线程,首先从定义上理解就有所不同 1.进程是什么? 是具有一定独立功能的程序.它是系统进行资源分配和调度的一个独立单位,重点在系统调度和单独的单位,也就是说进程是可以独 立运行的一段程序. 2.线程又是什么? 线程进程的一个实体,是CPU调度和分派的基本单位,他是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源. 在运行时,只是暂用一些计数器.寄存器和栈 . 他们之间的关系 1.一个线程只能属于一个进程,而一个

  • Java多线程和并发基础面试题(问答形式)

    本文帮助大家掌握Java多线程基础知识来对应日后碰到的问题,具体内容如下 一.Java多线程面试问题 1. 进程和线程之间有什么不同? 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用.而线程是在进程中执行的一个任务.Java运行环境是一个包含了不同的类和程序的单一进程.线程可以被称为轻量级进程.线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源. 2. 多线程编程的好处是什么? 在多线程程序中,多个线程被并发的执行以提高程序的效率,C

  • JAVA多线程与并发学习总结分析

    1.计算机系统使用高速缓存来作为内存与处理器之间的缓冲,将运算需要用到的数据复制到缓存中,让计算能快速进行:当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了. 缓存一致性:多处理器系统中,因为共享同一主内存,当多个处理器的运算任务都设计到同一块内存区域时,将可能导致各自的缓存数据不一致的情况,则同步回主内存时需要遵循一些协议. 乱序执行优化:为了使得处理器内部的运算单位能尽量被充分利用. 2.JAVA内存模型目标是定义程序中各个变量的访问规则.(包括实例字段.静态字段和构

  • 浅谈Java线程并发知识点

    发布:一个对象是使它能够被当前范围之外的代码所引用: 常见形式:将对象的的引用存储到公共静态域:非私有方法中返回引用:发布内部类实例,包含引用. 逃逸:在对象尚未准备好时就将其发布. 不要让this引用在构造函数中逸出.例,在构造函数中启动线程,线程会包含对象的引用. 同步容器:对容器的所有状态进行穿行访问,Vector.Hashtable,Cllections.synchronizedMap|List 并发容器:ConcurrentHashMap,CopyOnWriteArrayList,Co

  • 基于线程、并发的基本概念(详解)

    什么是线程? 提到"线程"总免不了要和"进程"做比较,而我认为在Java并发编程中混淆的不是"线程"和"进程"的区别,而是"任务(Task)".进程是表示资源分配的基本单位.而线程则是进程中执行运算的最小单位,即执行处理机调度的基本单位.关于"线程"和"进程"的区别耳熟能详,说来说去就一句话:通常来讲一个程序有一个进程,而一个进程可以有多个线程. 但是"任务

  • Java线程安全基础概念解析

    Java线程安全初步了解.JAVA线程安全从总体上来说,是指Java对象在多线程运行环境下的一种特性,表现为常规(区别于特殊调用情况)情况下每次调用都能得到正确的逻辑结果.从本质上来说,将对象的方法行为加上了同步控制逻辑,而调用者无须做其他额外的同步控制就可以安全放心的使用对象. 1.线程安全的定义 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安

  • Java线程的基本概念

    在之前的章节中,我们都是假设程序中只有一条执行流,程序从main方法的第一条语句逐条执行直到结束.从本节开始,我们讨论并发,在程序中创建线程来启动多条执行流,并发和线程是一个复杂的话题,本节,我们先来讨论Java中线程的一些基本概念. 创建线程 线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈.下面,我们通过创建线程来对线程建立一个直观感受,在Java中创建线程有两种方式,一种是继承Thread,另外一种是实现Runnable接口,我们先来看第一种. 继承Thread Java中j

  • JAVA线程池专题(概念和作用)

    线程池的作用 我们在用一个东西的时候,首先得搞明白一个问题.这玩意是干嘛的,为啥要用这个,用别的不行吗.那么一个一个解决这些问题 我们之前都用过数据库连接池,线程池的作用和连接池有点类似,频繁的创建,销毁线程会造成大量的不必要的性能开销,所以这个时候就出现了一个东西统一的管理线程,去负责线程啥时候销毁,啥时候创建,以及维持线程的状态,当程序需要使用线程的时候,直接从线程池拿,当程序用完了之后,直接把线程放回线程池,不需要去管线程的生命周期,专心的执行业务代码就行. 当然,如果非要是自己想手动ne

  • Java线程安全的计数器简单实现代码示例

    前几天工作中一段业务代码需要一个变量每天从1开始递增.为此自己简单的封装了一个线程安全的计数器,可以让一个变量每天从1开始递增.当然了,如果项目在运行中发生重启,即便日期还是当天,还是会从1开始重新计数.所以把计数器的值存储在数据库中会更靠谱,不过这不影响这段代码的价值,现在贴出来,供有需要的人参考. package com.hikvision.cms.rvs.common.util; import java.text.SimpleDateFormat; import java.util.Arr

  • Java线程安全与非线程安全解析

    ArrayList和Vector有什么区别?HashMap和HashTable有什么区别?StringBuilder和StringBuffer有什么区别?这些都是Java面试中常见的基础问题.面对这样的问题,回答是:ArrayList是非线程安全的,Vector是线程安全的:HashMap是非线程安全的,HashTable是线程安全的:StringBuilder是非线程安全的,StringBuffer是线程安全的.因为这是昨晚刚背的<Java面试题大全>上面写的.此时如果继续问:什么是线程安全

  • 深入讲解java线程与synchronized关键字

    我们将会从以下的几点理解java线程的一些概念: 线程的基本概念和优劣之处 创建一个线程的两种方式 线程的属性 线程的状态 synchronized可修饰的方法 synchronized的重要特性 一.线程的基本概念 在计算机中有进程和线程这么两个概念,进程中可以有多个线程,它们是从属关系,进程往往更像是资源的占有者,线程才是程序的执行者,多个线程之间共享着进程中的资源.一个cpu同时只能运行一个线程,每个线程都有一个时间片,时间片用完了就会被阻塞并让出CPU的控制权,交给下一个线程使用.这样在

  • 详解Java线程中断知识点

    下面的这断代码大家应该再熟悉不过了,线程休眠需要捕获或者抛出线程中断异常,也就是你在睡觉的时候突然有个人冲进来把你吵醒了. try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } 此时线程被打断后,代码会继续运行或者抛出异常结束运行,这并不是我们需要的中断线程的作用. 到底是什么是线程中断? 线程中断即线程运行过程中被其他线程给打断了,它与 stop 最大的区别是:stop 是由系统强

  • 剖析Java中线程编程的概念

    Java线程的概念 和其他多数计算机语言不同,Java内置支持多线程编程(multithreaded programming). 多线程程序包含两条或两条以上并发运行的部分.程序中每个这样的部分都叫一个线程(thread),每个线程都有独立的执行路径.因此,多线程是多任务处理的一种特殊形式. 你一定知道多任务处理,因为它实际上被所有的现代操作系统所支持.然而,多任务处理有两种截然不同的类型:基于进程的和基于线程的.认识两者的不同是十分重要的. 对很多读者,基于进程的多任务处理是更熟悉的形式.进程

  • 详解Java线程池和Executor原理的分析

    详解Java线程池和Executor原理的分析 线程池作用与基本知识 在开始之前,我们先来讨论下"线程池"这个概念."线程池",顾名思义就是一个线程缓存.它是一个或者多个线程的集合,用户可以把需要执行的任务简单地扔给线程池,而不用过多的纠结与执行的细节.那么线程池有哪些作用?或者说与直接用Thread相比,有什么优势?我简单总结了以下几点: 减小线程创建和销毁带来的消耗 对于Java Thread的实现,我在前面的一篇blog中进行了分析.Java Thread与内

随机推荐