1 概念

通常来说,我们编写的Java代码是以进程的形式来运行的,所编写的代码就是“程序”,而执行中的程序就是“进程”。进程是系统进行资源分配和调度的独立单位。

线程是位于进程的下一级,是系统中的最小的执行单位。但是线程本身不拥有资源,线程本身通常只拥有寄存器数据以及执行时的堆栈。同一个进程内的多个线程共享属于当前进程的资源,在需要资源的时候要抢占。

多线程编程的目的就是使得程序能够最大限度的利用CPU等资源,当某一线程的处理不需要占用CPU而只和I/O等资源打交道时,让需要占用CPU资源的其它线程有机会获得CPU资源。相对于进程间的通信,线程间的通信占用的资源更少,实现起来也更加方便,而且能够充分利用CPU的空闲时间,同时,这也是多线程编程的目的。

2 线程的调度和状态

2.1 线程的调度

  由于线程是系统调度和执行任务的基本单位,因此对一个进程来说,至少会有一个默认的线程,这个线程通常称之为主线程。线程的执行是需要CPU的。CPU把自己的时间进行切片,然后以时间片为单位向外提供服务。比如在单位时间内,有50个切片,当前的线程有A、B、C,那么可能的结果便是给A几个时间片,B几个时间片,C几个时间片,表面上看,在单位时间内,所有的线程都是“同时”执行的,但是围观的对于单个CPU(单核)来说,还是有先后顺序的串行执行的。这种线程的调度方式是由操作系统来决定的。

目前来说,调度方式主要有两大类:

(1)非抢占式:一个线程一旦被选择在CPU上运行,就会一直运行下去,直到阻塞或者自动退出。这种方式,可能会导致整个系统挂起。

(2)抢占式:一个线程被选中在CPU上运行,允许运行的时间长度是有限制的,系统可能会中途把CPU交给其他的线程运行,这种控制,是通过时钟中断来完成的。目前抢占式的调度算法主要有三种:先到先服务算法、时间片调度算法、优先级调度算法。

2.2 线程的状态

有了上面的线程调度,自然就会提到线程在不同时期的状态。

线程的状态通常来说有五种,分别是新建状态、就绪状态、运行状态、阻塞状态及死亡状态。但是,扩展来说,加上锁的状态,可以更好的理解线程。

按照JDK中的解释,线程的状态分为六种:

(1)NEW:线程刚被创建,但是还没有启动;

(2)RUNNABLE:正在JVM中被运行的线程的状态,有可能因为缺少CPU等资源进入等待状态;

(3)BLOCKED:阻塞状态,等待其他线程释放同步锁或者IO;

(4)WAITING:当线程调用了wait方法(无参)、join方法(无参)、LockSupport.park方法之后,进入等待WAITING状态,等待被唤醒;

(5)TIMED_WAITING:当线程调用了sleep方法、wait方法(有参)、join方法(有参)、LockSupport.parkNanos、LockSupport.parkUntil;

(6)TERMINATED:结束执行;

当代码在linux上运行的时候,可以使用jstack命令查看当前进程内所有线程的状态,就是以上六种。

3 线程的实现

本文都是针对Java语言来说的。针对Java来说,线程的实现有两种方式,一种是当前类继承Thread类,另一种是类实现Runnable接口。

3.1 继承Thread方式

这种方式,只需要当前类extends Thread,然后实现其中的run()方法即可。于是在类的构造方法中,不可避免的会涉及到super超类Thread的构造。

Thread也是默认实现的Runnable接口:

public class Thread implements Runnable

Thread类的构造方式有两种。一种是无参,自然,另一种是有参的。

3.1.1 Thread无参构造

Thread内部有个init方法,用来初始化线程相关的信息,例如线程的名字、所属的线程组和分配线程栈的大小等。

private void  init(ThreadGroup g, Runnable target, String name, long stackSize) 

  对于无参的Thread构造方法来说,上述信息都是默认的。

public Thread() {

        init(null, null, "Thread-" + nextThreadNum(), 0);

}

这里面有个nextThreadNum方法,其实现的源码为:

 /* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
  return threadInitNumber++;
}

threadInitNumber是个全局的变量,会被当前进程内的所有的线程所共享,这就是为什么当我们打印多个线程的名字时,后面有个变化的阿拉伯数字的原因,这个变量值和字符串"Thread-"一起构造了当前线程的名字。

3.1.2 Thread有参构造

来看下有参数的Thread构造情况。

有参数的情况一共有七种,其实也就是对init方法中的参数进行选择性的赋值。

private void  init(ThreadGroup g, Runnable target, String name,long stackSize) {
  Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
  /* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager what to do. */
if (security != null) {
  g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter use the parent thread group. */
if (g == null) {
  g = parent.getThreadGroup();
}
} /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */
g.checkAccess(); /** Do we have the required permissions?*/
if (security != null) {
  if (isCCLOverridden(getClass())) {
  security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}

g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.name = name.toCharArray(); if (security == null || isCCLOverridden(parent.getClass()))
   this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext = AccessController.getContext();
this.target = target;
setPriority(priority);
if (parent.inheritableThreadLocals != null)
  this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;

/* Set thread ID */
tid = nextThreadID();
}
}

3.2 实现Runnable接口方式

而实现Runnable接口的方式,相当于上面继承Thread类来说有一点好处,就是Runnable是接口,当前类可以实现多个接口,但是继承的话,就是硬伤了,Java不允许多继承,这点来说,实现Runnable在实际应用中更为方便。

就像上面的Thread类一样,实现Runnable接口,其实就是为了实现其中的run方法,实际上,这个接口中也只有这么一个run方法。

4 线程的常用方法

使用线程时有几个常用的方法,下面对这几个常用的方法做个小结,这些方法分别是:start()、run()、wait()、notify()、notifyAll()、sleep()、join()、interrupt()、yield()、suspend()、setDaemon()。

4.1 start方法

一个线程的启动,是由start开始的。其源码如下:

 public synchronized void  start() {
  if (threadStatus != 0)
    throw new IllegalThreadStateException();
  group.add(this);
  start0();
  if (stopBeforeStart) {
    stop0(throwableFromStop);
  }
}

首先会判断当前线程的状态是不是出于NEW状态,如果不是新创建的线程,则会抛出异常,否则,就将当前线程加入到指定的线程组中。

4.2 run方法

通常需要重载此方法来完成我们所需要的功能,因为原生的run方法中实在没什么可说的。

 public void run() {
if (target != null) {
target.run();
}
}

4.3 notify方法

此方法原本是Java的顶级父类Object中的自有方法,用于唤醒一个处于等待对象锁的线程。如果有多个线程等待,则会随机挑选一个唤醒。而等待对象锁的方法是通过wait方法来实现。

但是被唤醒的线程不能马上执行到当前线程放弃它的对象锁时的状态。被唤醒的这个线程会和其他排队的线程再次展开竞争锁的状态。

顺便说一下,一个线程获得对象锁的三种方法:

(1)通过执行那个Object对象的synchronized实例方法;

(2)通过执行synchronized代码块;

(3)通过类的静态synchronized方法;

4.4 notifyAll方法

同上面的notify方法一样,notifyAll方法也是唤醒处于等待对象锁的线程。不同的是,notifyAll方法是要唤醒所有的处于等待状态的线程。需要注意的是,当线程数增加的时候,此方法的耗时也会随之增加,因为工作量大了嘛。

4.5 sleep方法

相当于是使得当前线程进行停滞状态,即BLOCKED状态。但是即使在“梦中”,此线程也不会放弃已经获取到的对象锁。睡眠的时间单位是ms。

4.6 join方法

join方法可以有参数,也可以没有参数。其本质上还是调用的wait方法,关于wait方法,在下面会有说明。先看下有一个参数的join方法。

 public final synchronized void  join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

可见,所传入的参数值,只不过是需要wait的时间而已。当使用join方法的时候,比如在A线程中,有个B的子线程,A的方法中调用了B线程的join方法,那么此时当前方法会进入等待状态,直到B线程执行结束。换句话说,当主线程调用子线程的join方法后,只有当子线程执行完毕后,主线程才能继续往下执行。

4.7 interrupt方法

中断当前线程的执行。当前线程调用的此方法之后:

(1)如果该线程处在可中断状态下(调用了wait、sleep、join、Selector.select方法等),那么该线程会被立即唤醒,同时会收到一个InterruptedException。如果是阻塞在io上,对应的资源会被关闭,同时会收到ClosedByInterruptedException。

(2)如果当前线程处在不可中断状态下,则Java只是设置下该线程的interrupt状态,如果之后继续调用阻塞函数,则会抛出InterruptedException,如果不掉用阻塞的方法,则线程会继续往下执行。当调用了interrupt方法之后,程序还继续往下执行,发生这种情况的原因就在这里了。

现在我们就知道了如何正确的对一个线程做出中断处理了,大致可以有三种方式来处理:

(1)调用Thread.interrupted()方法来判断是否已经中断;

(2)捕获InterruptedException异常;

(3)上述两种方式的结合;

4.8 yield方法

暂停当前线程的执行,当前线程退出运行状态,进入可运行状态。此时不会释放已持有的锁。

此方法与sleep有点类似,都是暂停当前的执行,并且不释放锁,不同的是,在sleep线程的指定时间内,当前线程是不会再被执行的,而yield却有可能立即被再次执行,并且,sleep可使得低优先级的线程得到执行的机会,而yield只能使得同优先级的线程有执行机会。

4.9 suspend方法

此方法已经不被推荐使用了。它的作用是使得当前线程直接进入阻塞状态,并且不会自动恢复,必须调用resume方法后才能恢复。但是可能会引起死锁。

4.10 wait方法

这里的wait方法实际上还是Object类的自有方法。当执行此方法的时候,就会进入到一个和当前对象相关的等待池中,同时会释放掉已持有的锁。直到另外一个线程调用了当前这个对象的notify或者notifyAll方法之后,当前等待的线程才会继续往下执行。wait必须使用在synchronized代码块中,并且只能由当前对象锁的拥有者的线程所调用。当恢复执行之后,从wait的下一条语句继续执行,因而wait方法总是在while方法中被调用。

wait方法也分为有参和无参两种形式。

(1)有参形式:传入的参数是需要等待的时间的长度。此时,当前线程除了能够被notify和notifyAll方法唤醒外,当到达指定的等待时间之后,也会自动的重新加入到锁的竞争中。

(2)无参形式:实际上,此时也是调用的有参的wait方法,只不过时间值被设置为默认的0。

4.11 setDaemon方法

用于设定当前线程是守护线程还是普通用户线程。如果是守护线程的话,这个方法必须在线程被启动之前就调用。

实际上,守护线程指的是用来服务用户线程的线程,如果没有其他用户线程在运行,那就没有可服务的对象,这个线程也就退出了。比如垃圾回收线程就是典型的守护线程。

如果在一个守护线程内创建了子线程,那么这些子线程默认也是守护线程。

关于Java线程的更多相关文章

  1. Java线程并发:知识点

    Java线程并发:知识点   发布:一个对象是使它能够被当前范围之外的代码所引用: 常见形式:将对象的的引用存储到公共静态域:非私有方法中返回引用:发布内部类实例,包含引用.   逃逸:在对象尚未准备 ...

  2. Java线程的概念

    1.      计算机系统 使用高速缓存来作为内存与处理器之间的缓冲,将运算需要用到的数据复制到缓存中,让计算能快速进行:当运算结束后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了. 缓 ...

  3. Java 线程池框架核心代码分析--转

    原文地址:http://www.codeceo.com/article/java-thread-pool-kernal.html 前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和 ...

  4. 细说进程五种状态的生老病死——双胞胎兄弟Java线程

    java线程的五种状态其实要真正高清,只需要明白计算机操作系统中进程的知识,原理都是相同的. 系统根据PCB结构中的状态值控制进程. 单CPU系统中,任一时刻处于执行状态的进程只有一个. 进程的五种状 ...

  5. 【转载】 Java线程面试题 Top 50

    Java线程面试题 Top 50 不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题.Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员 的欢迎.大多数待遇丰厚的J ...

  6. 第24章 java线程(3)-线程的生命周期

    java线程(3)-线程的生命周期 1.两种生命周期流转图 ** 生命周期:**一个事物冲从出生的那一刻开始到最终死亡中间的过程 在事物的漫长的生命周期过程中,总会经历不同的状态(婴儿状态/青少年状态 ...

  7. 第23章 java线程通信——生产者/消费者模型案例

    第23章 java线程通信--生产者/消费者模型案例 1.案例: package com.rocco; /** * 生产者消费者问题,涉及到几个类 * 第一,这个问题本身就是一个类,即主类 * 第二, ...

  8. 第22章 java线程(2)-线程同步

    java线程(2)-线程同步 本节主要是在前面吃苹果的基础上发现问题,然后提出三种解决方式 1.线程不安全问题 什么叫线程不安全呢 即当多线程并发访问同一个资源对象的时候,可能出现不安全的问题 对于前 ...

  9. 第21章 java线程(1)-线程初步

    java线程(1)-线程初步 1.并行和并发 并行和并发是即相似又有区别: 并行:指两个或者多个事件在同一时刻点发生. 并发:指两个或多个事件在同一时间段内发生 在操作系统中,并发性是指在一段事件内宏 ...

  10. [转]Java线程安全总结

    最近想将java基础的一些东西都整理整理,写下来,这是对知识的总结,也是一种乐趣.已经拟好了提纲,大概分为这几个主题: java线程安全,java垃圾收集,java并发包详细介绍,java profi ...

随机推荐

  1. struts2 标签的使用之一 s:if

    struts2 的web 项目中为了方便的编写jsp,标签是最好的选择 1:struts2 标签库的定义在**-core-版本号.jar META-INF 路径下找到struts-tags.tld文件 ...

  2. tomcat安全设置

    1.关闭服务器端口:server.xml默认有下面一行: <Server port="8005" shutdown="SHUTDOWN"> 这样允许 ...

  3. c++ string assign =

    C++ string类的成员函数,用于拷贝.赋值操作,它们允许我们顺次地把一个string 对象的部分内容拷贝到另一个string 对象上. string &operator=(const s ...

  4. editplus的配置文件来支持sql语法高亮【转】

      editplus默认是没有sql语法高亮的,原因是它的内部没有sql.stx的这样一个语法文件 我们自己在 EditPlus 的安装目录下面新建一个文件名为sql.stx,然后打开editplus ...

  5. POJ1850——Code(组合数学)

    Code DescriptionTransmitting and memorizing information is a task that requires different coding sys ...

  6. UVA548——Tree(中后序建树+DFS)

    Tree You are to determine the value of the leaf node in a given binary tree that is the terminal nod ...

  7. Android在java代码中设置margin

    我们平常可以直接在xml里设置margin,如: <ImageView android:layout_margin="5dip" android:src="@dra ...

  8. 在windows下使用git需要反复输入用户名和密码的问题

    节选自我还在写的git文档中的一部分,用md写的,博客园竟然还不支持markdown,完全没有格式啊,懒得弄了,不过解决方法是没有问题的 在win下使用git,如果没有任何设置,一定会反复输入用户名和 ...

  9. java socket nio编程

    上次写了一个socket的基本编程,但是有个问题,阻塞特别严重,于是小编便去找了nio学习了一下... public class TimeServer { public static void mai ...

  10. Exynos 4412的启动过程分析[2]

    做实验时我们是把 bin 文件烧入SD卡,比如前面做的汇编流水灯实验. 问:是谁把这些指令从 SD 卡读出来执行? 答:是固化在芯片内部ROM上的代码---它被称为iROM ,iROM是厂家事先烧写在 ...