实战java高并发程序设计之线程的各种状态(源码)

版权声明:转载注明出处 https://blog.csdn.net/nobody_1/article/details/82962399

上篇文章讲到并发问题的由来,并发世界中的几个容易混淆的概念,并发级别以及衡量并发性能的两个定律。这篇博文将解决三个问题:
1. 线程和进程的区别?为什么调度线程而不调度进程?
2. 调度的线程在代码中是如何创建的?如何终止的?如何阻塞的?
3. 线程的类别和优先级?

我们经常说的“高并发”是我们要达到的目的,即系统的高可用、高响应、高吞吐量等。而实现“高并发”最底层的途径就是并发编程,并发编程简单理解就是通过编码使得多个线程正确运行。
用过电脑的人都打开过Windows的任务管理器,管理器界面上的Tab页会出现“应用程序”、“进程”、“服务”、“性能”等;“应用程序”是我们打开的一个程序,比如打开QQ,打开word文档,打开chrome等,每一个应用都是一个程序,这些都是我们看得见的程序,电脑运行还有很多我们开不见的程序,比如系统的运行、杀毒软件的运行、代理的运行等,这些都显示在“进程”中。

那么进程和线程之间有什么联系?

我们知道,“进程”是暴露给用户的,让用户知道这个程序的运行情况,比如进程的名称,状态,作用(描述),端口等,是指整个程序的运行状况;而程序(系统)是由线程来执行的,即程序的功能实现则由线程来操作;高并发也就是程序的各个功能能够快速且稳定的响应用户的操作,每个功能通过独立的线程来完成,进而造就高并发系统。举个例子理解进程和线程:一个公司的运作可以比作为一个进程的执行,公司内各个部门,如财务部门,人事部门,业务部门等组成进程,他们的运作才支撑起公司的运作,各个部门则可以理解为各个线程,每个线程用于完成不同的工作,进而使得进程能正确的运转;即线程组成进程,进程囊括线程,是一个组成和被组成的关系。
用术语来讲就是,进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。线程就是轻量级进程,是程序执行的最小单位。
使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
线程和进程的区别

进程的状态有两种:已停止和正在运行;由于线程的粒度较细,因此线程的状态有多种:创建,运行,阻塞,等待和结束。(生命周期)
线程生命周期

线程的各种状态

2.1 创建并启动

java是面向对象的语言,所以它把任何操作的实体抽象成一个对象,所执行的动作抽象成一个方法;java语言中用Thread类来表示线程,创建线程就是new一个对象:

Thread thread = new Thread();   // 不带参数默认名字为Thread-n
Thread thread = new Thread(“t1”);  // t1 表示线程的名字

java语言中通过.来调用对象及其方法,线程类创建完毕需要通过start()方法来启动线程;即

thread.start();

才能表示启动了一个线程;完整实例如下:

public static void main(String[] args) {
		Thread thread = new Thread();
		thread.start();
		System.out.println(thread.getName());
		
		Thread thread2 = new Thread("t2");
		thread2.start();
		System.out.println(thread2.getName());   // 打印线程名字
		
		System.out.println(Thread.currentThread().getName());   // 主线程
}

结果:

Thread-0
t2
main

这里表示创建并启动了一个线程。
new Thread()过程,实际调用init()方法

private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        init(g, target, name, stackSize, null, true);
}

通过上面的方法可知;
->init方法为私有方法,只能在Thread类内部调用,保证Thread类构造的安全性;
-> Thread类暴露给程序员的构造方法有很多种,以便满足不同场景下的需要;但实际调用的方法则是同一个方法;
->构造线程要给线程赋予名字name,组别threadGroup,运行内容target,栈堆大小stackSize,其中第五、六个参数分别表示保证线程安全的对象、是否为本地线程赋值来自构造函数的初始值(涉及到ThreadLocal,默认为true);

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        // ①首先确定线程的名字,要么构造传进来,要么使用默认的名字
if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        this.name = name;

        // ② 确认线程的组别 要么构造传进来 要么归属于安全框架设置的线程组 要么归属于当前线程的线程组;因为线程分级别,所以需要确定线程组;
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        // 确保正在运行的线程(主线程)是否具有修改该线程组的权限
        g.checkAccess();

        // ③ 如果存在安全对象,则还需要判断线程的相关运行时权限
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        // ④ 线程组中未启动线程数加1
        g.addUnstarted();

        // ⑤ 判断是否是守护线程,得到线程的优先级
        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        // ⑥ 确定对象上下文加载器(来自运行线程的加载器)
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        // ⑦ 获取当前调用上下文的快照,并将其至于AccessControllerContext对象中
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        // ⑧ 运行内容和设置优先级
        this.target = target;
        setPriority(priority);
        // ⑨ 设置当前线程的私有局部变量ThreadLocal
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        // ⑩设置线程的堆栈大小,初始构造默认为0; 设置线程id
        this.stackSize = stackSize;
        tid = nextThreadID();
    }

.start()启动线程如下:

public synchronized void start() {
        /**
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                
            }
        }
    }

start方法实则调用本地方法start0():

// 实则为本地方法,用c或者c++实现;
private native void start0();

经过断点调试,start0()方法实则调用Thread类的run()方法,而run()方法调用target对象的run()方法。若在初始化过程中没有指定target对象,则为空不执行run(),会立即执行exit()方法结束线程;

private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

exist()方法意在清除掉该线程的所有变量或者引用。

到这里创建并启动了一个线程,但是线程的运行还没有开始。运行run()方法是调用Runnable类的run方法,Runnable类是一个接口,提供一个抽象run方法供实体类重写。其实Thread类本身也继承Runnable接口,Thread类本身也可以实现run方法,如下:

	Thread thread = new Thread(){
			@Override
			public void run(){
				System.out.println(" I am thread");
			}
	};

但是项目中都会定制线程,可以通过继承Thread类来自定义线程,重载run()方法执行想要执行的内容;但考虑到java语言是单继承的,继承特性比较宝贵,因此一般通过实现Runnable接口和Callable接口来定制化线程;实例如下:

public class CreateThread {

	public static void main(String[] args) {
		ThreadCallable thread = new ThreadCallable();
		try {
			thread.call();
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		Thread thread2 = new Thread(new ThreadRunnable(), "t2");
		thread2.start();
		
		System.out.println(Thread.currentThread().getName());
	}
}

class ThreadRunnable implements Runnable{

	@Override
	public void run() {
		System.out.println("I am ThreadRunnable");
		
	}
	
}

class ThreadCallable implements Callable<Integer>{

	@Override
	public Integer call() throws Exception {
		System.out.println("I am ThreadCallable");
		return null;
	}	
}

观察上面代码可能会有以下几个问题:
1. Callable接口的call()和Runnable接口的run()有什么区别?
2. 实现call()方法的类对象为何不需要调用start()就能直接运行线程?

首先Callable和Runnable接口都是实现线程功能的接口,唯一区别就是Callable接口的call()可以返回线程执行的结果且能抛出异常,而Runnable接口的run()则不返回任何内容;可以说两者在功能上互补。
然后,Callable类型的对象,直接调用call()就可运行线程;而Runnable类型的对象,则需要调用start()启动线程;通过call()的源码注释可知,call是放在Executor中执行,由执行器来调用,run()则依赖start()方法。(为什么要这样设计呢?)

2.2 停止线程

stop()是Thread类中定义的停止线程的方法,但stop方法太过暴力,在调用时直接停掉线程,可能会造成数据不一致性问题,因此已经被废弃掉。在实际开发中,不会单个的创建线程,而是由线程池来创建并管理线程;上面的例子中显示,线程执行完毕后会自定执行exist方法停止线程。

private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

如果由线程池来管理线程,则是另外一种情况。

2.3 线程阻塞

线程阻塞是暂停线程的一种方法,关于线程中断有以下三个方法:

public void interrupt();   // 中断线程
public static boolean interrupted();  // 判断当前线程是否被中断,并清除中断状态
public boolean isInterrupted();  // 判断当前线程是否被中断

实例:

public class InterruptTest {

	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 5; i++) {
					
					System.out.println("my name is " + Thread.currentThread().getName());
					if (Thread.currentThread().isInterrupted()) {
						System.out.println("i have interrupted");
						break;
					}
				}
				
			}
		});
		
		thread.start();
		thread.interrupt();
	}
}

大意:启动一个线程,循环打印当前线程的名字;在主线程中启动thread线程然后调用interrupt()方法中断线程thread,当thread线程检测到自身被中断了则退出循环。
interrupt()

public void interrupt() {
        // ① 调用该方法是否是线程本身,如果不是则进行安全检查;checkAccess()可能会抛出一个异常
        if (this != Thread.currentThread())
            checkAccess();
        // ②blockerLock是一个object类型的对象,利用内置锁来保证中断的同步性
        synchronized (blockerLock) {
            Interruptible b = blocker;  // IO操作过程的中断标志
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        // ③通过native方法实现线程中断
        interrupt0();
    }

isInterrupted()方法也是native方法;
interrupted()方法调用native方法;
在中断线程后,一般要在线程内部对中断后的线程进行响应。

2.4 线程等待

为了支持线程之间的协作,JDK在对象上设置了wait()和notify();在线程A中调用对象的wait()可使得该线程在该对象上等待,当线程B调用同一对象的notify(),则线程A等待结束,继续执行。
wait和notify为什么是对象的两个方法,而不是线程?这是因为对象有内置锁,通过内置锁使得线程等待。

public class WaitTest {

	private static Object object = new Object();
	public static void main(String[] args) {
		Thread t1 = new Thread(new Runnable() {
			
			@Override
			public void run() {
				synchronized (object) {
					System.out.println(System.currentTimeMillis() + ": t1 start!");
					try {
						object.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("notify...");
					System.out.println(System.currentTimeMillis() + ": t1 end!");
				}
				
			}
		});
		
		Thread t2 = new Thread(new Runnable() {
			
			@Override
			public void run() {
				synchronized (object) {
					System.out.println(System.currentTimeMillis() + ": t2 start!");
					System.out.println("wait...");
					object.notify();
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(System.currentTimeMillis() + ": t2 end!");
				}
				
			}
		});
		
		t1.start();
		t2.start();
	}
}

结果:

1538915095285: t1 start!
1538915095285: t2 start!
wait...
1538915097285: t2 end!
notify...
1538915097285: t1 end!

从代码中可以看出,要想调用object对象的wait和notify方法,需要首先获得object对象的锁;因此线程在object对象上等待时,会释放object对象的内置锁。这点和Thread类的sleep方法不一样,sleep方法让线程睡眠一定时间,睡眠期间不能做任何事情。
wait和notify方法都属于native方法。

2.5 线程的加入和谦让

在线程执行过程中,有些线程依赖其他线程的结果,因此需要等待其他线程执行完成才执行本线程。JDK提供的join方法就可以实现这样的功能;

public class JoinTest {
	private static volatile int i = 0;

	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(new Runnable() {
			
			@Override
			public void run() {
				for (i = 0; i < 1000000; i++) ;
			}
		});
		
		t1.start();
		t1.join();
		System.out.println(i);
	}
}

join方法有很多个:

join();   // 无限等待
join(long);   // 等待一定时长
join(long, int); // 等待一定时长,int参数为纳秒
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(0),可知join方法本质是让调用线程wait在当前线程对象实例上;当被等待线程全部执行完毕,会notify等待线程继续执行。

yield()方法是一种谦让的方法,意思是尽量不会参与CPU的竞争,是native本地方法。

参考文献

《实战java高并发程序设计》

猜你喜欢

转载自blog.csdn.net/nobody_1/article/details/82962399