在Java 技术中,多线程依旧是一个离不开的话题,掌握多线程才能对一些高并发技术理解透彻。同时多线程也需要有一定的操作系统基础,在其理论上进行学习,会对调度情况、线程情况有更多的了解。当然这一块也常常作为Java面试的重点,必须深刻理解与掌握。
本次是关于多线程的基础部分,随后将有更多关于多线程的知识点进行整理与汇总。由于本人目前学业繁忙,故平时更新随缘。
目录
- 进程与线程
- 多线程启动
- 线程状态
- sleep 与 yield
- 线程优先级
- 守护进程
进程与线程
进程是操作系统动态执行的基本单元,运行在自己地址空间的自包容程序。进程可以看成线程的容器,而线程又可以看作进程中的执行路径。线程使得程序控制流的多个分支可以执行在一个进程中,它们共享这个进程范围内的所有资源。在大多数OS中,把线程作为时序调度基本单元。
多线程使用可以提高一个复杂应用的性能。Java 机制是抢占式的。
那么到底什么是线程呢?简单总结如下:线程(Thread)的字面意思是线路,即应用程序(进程)中的程序执行线路。Java虚拟机允 许一个应用程序中可以同时并发存在多条程序执行线路。每个线程都 有一个优先级属性,优先级别高的线程,可能会被CPU优先执行。
多线程启动
一般其有两种启动方式:实现Runnable 接口,继承Thread 类并重写run方法。
使用Runnable 接口来提供,实现runnable 接口并重写run 方法,再将Runnable 实现对象传给Thread 类。通常,这个实现接口是一个更好的选择,提高程序的灵活性和扩展性,在后面的线程池调用中也使用Runnable 来表示执行。
public class TestRun {
public static void main(String[] args) {
Runa a = new Runa();
new Thread(a).start();
}
}
// 实现接口
class Runa implements Runnable {
public void run() {
System.out.println("Execute");
}
}
当然也可以继承Thread 类并重写run方法:
public class TestTrea {
public static void main(String[] args) {
athread c = new athread();
c.start();
}
}
class athread extends Thread {
public void run() {
System.out.println("running...");
}
}
需要注意的是,start方法并不代表线程启动的顺序,顺序都是不定的!为什么呢?因为任务的执行靠CPU,而处理器采用分片轮询方式执行任务,所有任务都是抢占式执行模式,说明任务不排序。
多线程标识
Thread 类用于管理线程、如设置线程优先级、设置Daemon属性,读取线程名字和ID,中断线程等。为了管理线程,每个线程启动后都会生成一个唯一的标识符,并且在生命周期保持不变。当线程终止时候,该ID可以重用。
public static void main(String[] args) {
for(int i=0;i<5;i++) {
Runa c = new Runa();
// 启动线程,申请执行任务
Thread a = new Thread(c);
System.out.println(a.getId());
System.out.println(a.getName());
}
}
Thread 和 Runnable :Runnable 接口表示线程要执行的任务,当其中run方法执行时,表示进程就在激活状态。
public interface Runnable {
public abstract void run();
}
Thread 类默认实现Runnable 接口,构造方法的重载形式允许传入Runnable 接口对象作为任务。
class Thread implements Runnable {
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
run() 与 start ()
class Thread implements Runnable {
public void run() {
}
public synchronized void start() {}
}
调用start 方法,使对象开始执行任务,这会触发Java 虚拟机调用当前线程对象的run方法。调用start方法,将会导致两个线程并发运行,一个是调用start的当前线程,一个是执行run的线程。如果反复调用start,非法,不会产生更多的线程,导致 IllegalThreadStateException异常。
调用start 方法后,触发了JVM 底层调用run方法,如果主动调用Thread对象的run方法,并不能启动一个新线程。
创建Thread类实例,首先会执行**registerNatives()**方法,它在静态代码块中加载。线程的启动、运行、生命期管理和调度等都高度依赖于操作系统,Java本身并不具备与底层操作系统交互的能力。因此线程的底层操作都使用了native方法,registerNatives()就是用C语言 编写的底层线程注册方法。
无论通过哪种构造方法创建线程,都需要首先调用init()方法,初始化线程环境:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
在init 方法中,实现了:设置线程名称、将新线程的父线程设置为当前线程、获取系统的安全管理并获得线程组、获取权限检查。
线程状态
在不同时期有不同的状态,在Thread 类通过内部枚举类State保存:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIME_WAITING,
TERMINATED;
}
// 通过getState方法来进行获取
NEW 状态:新建状态,一个已创建但是没有启动的线程。(没有start())
RUNNABLE 状态表示一个线程正在Java 虚拟机运行,调用start方法后切换到此状态。
BLOCKED:阻塞状态,表示当前线程正在阻塞等待获得监视器锁,当一个线程要访问被其他线程synchronized 锁定的资源时候,当前线程需要阻塞等待。
Thread a = new Thread(new Runnable() {
@Override
public void run() {
synchronized (cc) {
while(true) {
}
}
}) ;
WAITING:等待状态,当调用Object类的wait方法,Thread 的join 方法 (无超时设置),LockSupport类的park()方法。处于等待状态的线程,正在等待另一个线程去完成特殊操作,等待Object 对象调用notify或notifyAll方法,一个线程对象调用join方法,则会等待线程终止任务。
TIMED_WAITING 状态:表示线程处于定时等待状态。有设置wait,join,sleep方法,parkUntil,pakNanos方法,在指定时间内没有调用Object对象的notify 就会触发超时等待结束
WAITING、TIMED_WAITING、BLOCKED这几个线程状态, 都会使当前线程处于停顿状态,因此容易混淆。下面简单总结一下这 些状态之间的区别: (1)Thread.sleep()不会释放占有的对象锁,因此会持续占用 CPU。 (2)Object.wait()会释放占有的对象锁,不会占用CPU。 (3)BLOCKED使当前线程进入阻塞后,为了抢占对象监视器锁,一般操作系统都会给这个线程持续的CPU使用权。 (4)LockSupport.park()底层调用UNSAFE.park()方法实现, 它没有使用对象监视器锁,不会占用CPU。
TERMINATED 表示线程为完结状态,当线程完成run方法中的任务,或者中断线程状态会变为terminated。
Java 线程状态转化:
sleep 与 yield
Thread 类的sleep 方法,使当前执行的线程以指定的毫秒数暂时停止执行,具体停止时间取决于系统定时器和调度程序的精度和准确性。调用sleep方法不会使线程丢失监视器所有权,因此当前线程仍用cpu 分区。
public static native void sleep(long millis) throws InterruptedException;
测试代码:
class Runa implements Runnable {
public void run() {
try{
long begin = System.currentTimeMillis();
System.out.println("Integer:");
for(int i=0;i<10;i++) {
TimeUnit.SECONDS.sleep(1); // 需要捕获异常
System.out.println(i);
}
}catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
线程让步yield
yield 方法对线程调度发出一个暗示,即当前线程愿意让出正在使用的处理器。调度程序可以响应暗示请求也可以忽略。可以从running状态为runnable。
注意:yield 是一个暗示,没有机制会保证采纳。线程调度是Java线程机制的底层对象,可以把CPU使用权从一个线程转移到另一个线程。如果计算机是多核处理器,那么分配线程到不同处理器执行任务要依赖线程调度器。
下面来进行代码测试:使用sleep作为线程延时
public class ListOff implements Runnable{
public int countDown =5;
@Override
public void run() {
while(countDown-- >0) {
String info = Thread.currentThread().getId()+"#"+countDown;
System.out.println(info);
try{
TimeUnit.MILLISECONDS.sleep(100);
}catch (Exception e) {
}
}
}
public static void main(String[] args) {
ListOff lf = new ListOff(); // 创建一个倒计时器,两个线程可以同时使用
//关键原因countDown 唯一,两个线程可能同时访问这块内存,可以通过加锁方式解决。
new Thread(lf).start();
new Thread(lf).start();
}
}
把sleep代码修改为yield ,三次结果都是正确的,起到了线程让步(此处没有使用锁)
public void run() {
while(countDown-- >0) {
String info = Thread.currentThread().getId()+"#"+countDown;
System.out.println(info);
Thread.yield();
}
}
线程优先级
每个线程都有一个优先级,具有较高优先级可以优先获得CPU使用权。实际上,JDK有10个优先级,但是这与操作系统不可以建立映射关系,比如Windows 有7个线程优先级,所以在Java一般使用下面三种优先级设置。
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
可以通过Thread类中setPriority 方法对线程优先级进行设置,参考如下:
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
注意:如果设置的线程优先级小于1或者大于10 都将抛出IllegalArgumentException异常。不应该过分依赖于线程优先级,理论上线程优先级高的会优先执行。
具有较高优先级的线程会优先得到调度系统资源分配,优先级调度和底层操作系统有密切的关系,在各个平台表现不一致。当调用yield 方法时,会给线程调度器一个暗示,即优先级高的其他线程或相同优先级的其他线程,都可以优先获得CPU分片。
例如:创建六个进程,每个进程都计算足够量级的浮点运算,目的是让线程调度来得及介入。其中将1个线程设置为最高。
public class FloatArithmetic implements Runnable{
private int pri;
public FloatArithmetic(int pri) {
this.pri = pri;
}
public void run() {
BigDecimal value = new BigDecimal("0");
// 按照参数传递优先级
Thread.currentThread().setPriority(pri);
BigDecimal pi = new BigDecimal(Math.PI);
BigDecimal e = new BigDecimal(Math.E);
//足够耗时计算,使得任务调度可以反应
for(int i=0;i<3000;i++) {
for(int j=0;j<3000;j++) {
value = value.add(pi.add(e).divide(pi,4));
}
}
Thread self = Thread.currentThread();
System.out.println("Number:"+self.getId()+" Priority"+self.getPriority());
}
public static void main(String[] args) {
new Thread(new FloatArithmetic(Thread.MIN_PRIORITY)).start();
new Thread(new FloatArithmetic(Thread.NORM_PRIORITY)).start();
new Thread(new FloatArithmetic(Thread.MAX_PRIORITY)).start();
}
}
运行结果将按照优先级高低来显示。
例:使用多线程模拟窗口售票,非常经典的案例!
public class TicketTask implements Runnable{
private Integer ticket = 30;
public void run() {
while(this.ticket>0) {
synchronized (this) {
if(ticket>0) {
System.out.println("No"+Thread.currentThread().getId()+"sell:"+ticket);
ticket--;
try{
Thread.sleep(100);
}catch (Exception ex) {
}
}
}
}
}
public static void main(String[] args) {
TicketTask task = new TicketTask();
Thread t1 = new Thread(task);
t1.setPriority(Thread.MIN_PRIORITY);
Thread t2 = new Thread(task);
Thread t3 = new Thread(task);
t3.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
System.out.println("t1"+t1.getId());
System.out.println("t2"+t2.getId());
System.out.println("t3"+t3.getId());
}
}
守护进程
在Java 线程有两种线程,分别是用户线程与守护线程(Daemon)。所谓守护进程,是指程序运行时候在后台提供一种通用服务的线程。比如,垃圾回收就是守护者。Daemon线程与用户线程在使用时候没有任何区别,唯一的不同是:当所有用户线程结束时候,程序也会终止,Java虚拟机不管是否存在守护线程,都会退出。
调用Thread 对象的setDaemon方法,可以把用户线程标记为守护者,调用isDaemon可以判断是否为一个守护线程。
在调用守护线程的时候需要注意:
(1)setDaemon()方法必须在start()方法之前设置,否则会抛 出一个IllegalThreadState-Exception异常。不能把正在运行的常规线程设置为守护线程。 (2)在守护线程Daemon中产生的新线程也是守护线程,存在 着继承性。 (3)守护线程应该永远不去访问固有资源,如文件、数据库, 因为它会在任何时候甚至在一个操作的中间发生中断。 (4)守护线程通常都使用while(true)的死循环来持续执行任务。