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方法
用于设定当前线程是守护线程还是普通用户线程。如果是守护线程的话,这个方法必须在线程被启动之前就调用。
实际上,守护线程指的是用来服务用户线程的线程,如果没有其他用户线程在运行,那就没有可服务的对象,这个线程也就退出了。比如垃圾回收线程就是典型的守护线程。
如果在一个守护线程内创建了子线程,那么这些子线程默认也是守护线程。