线程
本文原创,转载请标明原处!
启动与入口
Thread对象可操纵一个线程,而Runnable对象代表一个可被运行的对象,必须使用Thread对象的start()方法启动线程。启动后,会先运行Thread对象的run()方法,这个方法未被重写时,就会执行Runnable对象的run()方法。
主线程的入口是main()静态方法,子线程的入口是Thread的run()方法。下图表示Thread与Runnable的区别:
运行与暂停
线程可以在运行中暂停,也可以从暂停中恢复运行,其暂停目的有如下列举:
- 周期性处理:如每秒更新时间的显示,就需要每次更新完时间后,线程休眠1秒。
- 定时处理:让线程休眠至指定时间运行。
- 事件处理:让线程休眠至事件触发的时候运行。
- 通行限制:在特定代码块里,只允许一定数量或具备一定条件的线程进入,被限制的线程暂停于关卡处。
- 确保实时数据:多线程共用数据时,读取数据的时间点到使用数据的时间点,会有一个时间差,因此使用数据时,该数据并不是实时数据。为了确保实时数据,就需要在使用时,只有一个线程在使用,其它需要用到该数据的线程就会暂停。
下图表示,线程基本的暂停方式与恢复运行方式:
说明:图中,虚线表示每次触发时,只允许一条线程变化。Happen表示发生于其它线程,即异步触发。
有一点不太明确,也不知道怎么去测试,就是Auto Yield,这里我猜想加上去的。猜想的依据是根据一篇文章《Java中的多线程你只要看这一篇就够了》。文中写道,并行与并发的概念与区别,思考了一下,并行是多个人同时处理各自的事,换句话说同一时刻可以处理多件事。而并发是一个人同时处理多件事,但实际上是做不到的,因为同一时刻只能处理一件事。不知道这里,我有没有思考有误,如果是这样,那么同一时刻,就只处理着一条线程。
结束与监听
线程结束分为自然结束与强制中止。
自然结束,是指线程启动时的入口方法结束,自然结束分为正常结束和异常结束。异常结束是以某个异常抛出作为起点一直往上抛出,而正常结束,需要监听到结束的标志后,处理关闭事项,需要处理上一段时间才能真正结束。
强制中止,在运行途中的任意位置结束,什么时候中止,线程就什么时候结束。可以使用stop()方法强制中止,也可以把把线程设置为目标线程的守护线程,这样线程就会以目标线程结束而强制结束。强制中止,用于不需要维护重要数据的线程,即对重要数据不产生影响。
代码示例1:主线程以子线程对象作为同步锁,然后使用wait(...)方法,当子线程结束时,就会唤醒wait(...)方法,从而达到主线程监听子线程的结束。
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(){ public void run(){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("子线程结束!"); } }; thread.start(); // 相当于thread.join(),thread线程结束时,会调用thread.notifyAll() synchronized(thread){ thread.wait(); } System.out.println("主线程结束!"); // 输出结果: // 子线程结束! // 主线程结束! }
实际上,join(...)方法里面,用的就是这种方式监听子线程的结束。因此,主线程使用join(...)方法后,子线程要切忌不能使用自己的线程对象作为同步锁,那样会造成死锁。
代码示例2:主线程调用子线程的interrupt()方法,使子线程在wait()方法里抛出异常,同时还验证了interrupt()使子线程从wait()方法中唤醒后,依然需要同步阻塞。
public static void main(String[] args) throws InterruptedException { final Object lock = new Object(); Thread[] threads = new Thread[5]; for(int i=0; i<threads.length; i++){ final int num = i + 1; Thread thread = new Thread(){ public void run(){ synchronized(lock){ try { System.out.println("线程" + num + " -> 进入等待!"); lock.wait(); } catch (InterruptedException e) { // e.printStackTrace(); System.out.println("线程" + num + " -> " + e); } System.out.println("线程" + num + " -> 退出等待!"); try { System.out.println("线程" + num + " -> 进入睡眠!"); Thread.sleep(500); } catch (InterruptedException e) { // e.printStackTrace(); System.out.println("线程" + num + " -> " + e); } System.out.println("线程" + num + " -> 睡眠结束,退出同步块!"); Thread.currentThread().stop(); System.out.println("线程" + num + " -> 结束没有?"); } } }; thread.start(); threads[i] = thread; } System.out.println("主线程 -> 等待2秒!"); Thread.sleep(2000); System.out.println("主线程 -> 中止所有线程!"); for(int i=0; i<threads.length; i++){ threads[i].interrupt(); } for(int i=0; i<threads.length; i++){ threads[i].join(); } System.out.println("主线程 -> 中止所有线程结束!"); /* 输出结果: 线程1 -> 进入等待! 线程4 -> 进入等待! 主线程 -> 等待2秒! 线程3 -> 进入等待! 线程2 -> 进入等待! 线程5 -> 进入等待! 主线程 -> 中止所有线程! 线程1 -> 退出等待! 线程1 -> 进入睡眠! 线程1 -> 睡眠结束,退出同步块! 线程4 -> 退出等待! 线程4 -> 进入睡眠! 线程4 -> 睡眠结束,退出同步块! 线程3 -> 退出等待! 线程3 -> 进入睡眠! 线程3 -> 睡眠结束,退出同步块! 线程2 -> 退出等待! 线程2 -> 进入睡眠! 线程2 -> 睡眠结束,退出同步块! 线程5 -> 退出等待! 线程5 -> 进入睡眠! 线程5 -> 睡眠结束,退出同步块! 主线程 -> 中止所有线程结束! */ }
代码示例3:子线程作为主线程的守护线程,因主线程的结束而强制中止。
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(){ public void run(){ boolean run = true; while(run){ System.out.println("子线程 -> 运行中……"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("子线程 -> 正常退出!"); } }; thread.setDaemon(true); thread.start(); System.out.println("主线程 -> 2秒后结束"); Thread.sleep(2000); System.out.println("主线程 -> 结束"); /* 运行结果: 主线程 -> 2秒后结束 子线程 -> 运行中…… 子线程 -> 运行中…… 子线程 -> 运行中…… 子线程 -> 运行中…… 子线程 -> 运行中…… 主线程 -> 结束 */ }
异常与捕获
线程如果异常结束后,还可以使用以下方式捕获:
- 使用线程对象的uncaughtExceptionHandler捕获。
- 使用线程对象所属的线程组捕获。
- 使用默认的uncaughtExceptionHandler捕获。
捕获异常的顺序如图所示:
代码示例:
public static void main(String[] args) { Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(){ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("默认UncaughtExceptionHandler -> 捕获到异常!"); e.printStackTrace(); } }); ThreadGroup group = new ThreadGroup("my group"){ public void uncaughtException(Thread t, Throwable e){ System.out.println("线程组 -> 捕获到异常!"); if(this.getParent() != null) this.getParent().uncaughtException(t, e); else Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, e); } }; Thread thread = new Thread(group, "my thread") { public void run() { try { System.out.println("应用 -> 抛出异常……"); int i = 1 / 0; } catch (RuntimeException e) { System.out.println("try catch -> 捕获到异常!"); throw e; } } }; thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("线程UncaughtExceptionHandler -> 捕获到异常!"); t.getThreadGroup().uncaughtException(t, e); } }); thread.start(); /* 运行结果: 应用 -> 抛出异常…… try catch -> 捕获到异常! 线程UncaughtExceptionHandler -> 捕获到异常! 线程组 -> 捕获到异常! 默认UncaughtExceptionHandler -> 捕获到异常! java.lang.ArithmeticException: / by zero at com.io.Test$3.run(Test.java:29) */ }
权限与控制
checkAccess():测试当前线程是否拥有修改目标线程的权限,如果没有则抛出SecurityException,具体的测试策略由SecurityManager#checkAccess(Thread)方法提供,而默认的测试策略为:如果目标线程的线程组Thread#getThreadGroup()不是根线程组,就需要拥有权限SecurityConstants.MODIFY_THREAD_PERMISSION。
getContextClassLoader()/setContextClassLoader(ClassLoader):获取和设置目标线程的类加载器。关于类加载器,可以先查阅相关文章,获取类加载器加载类,还有以下三种方式:
- Class#getClassLoader():获取目标类被加载时所使用的类加载器。
- Class.forName(...):使用当前运行方法所在的类的类加载器。
- 直接使用类名,具体是怎么加载的我就不清楚了,也会使用当前被引用位置所在的类的类加载器加载。
大概了解这些状况之后,那么好像线程的类加载器变得有些多余的,具体是不是多余的先不说,下面给出一个代码示例。
代码示例:有两个类,Test1能加载的资源不想共享给Test2用,也就说限制Test2加载自己的资源。
import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; public class Test1 { public static void main(String[] params) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException{ System.out.println("Test1 -> 我是使用类加载器" + Test1.class.getClassLoader() + "加载的!"); System.out.println("Test1 -> 但我要禁止那些被我加载进来的类里面不允许再使用" + Test1.class.getClassLoader() + "加载类,如何做到?"); URLClassLoader loader = new URLClassLoader(new URL[]{ new URL("file:/f:/test/") }, Test1.class.getClassLoader().getParent()); System.out.println("Test1 -> 创建了类加载器" + loader); System.out.println("Test1 -> " + loader + "的URLs为:" + Arrays.toString(loader.getURLs())); System.out.println("Test1 -> " + loader + "的父类加载器为:" + loader.getParent()); Class<?> clazz = loader.loadClass("Test2"); System.out.println("Test1 -> 使用" + clazz.getClassLoader() + "已加载:" + clazz); Object obj = clazz.newInstance(); clazz.getMethod("test").invoke(obj); /* 运行结果: Test1 -> 我是使用类加载器sun.misc.Launcher$AppClassLoader@56e88e24加载的! Test1 -> 但我要禁止那些被我加载进来的类里面不允许再使用sun.misc.Launcher$AppClassLoader@56e88e24加载类,如何做到? Test1 -> 创建了类加载器java.net.URLClassLoader@dd41677 Test1 -> java.net.URLClassLoader@dd41677的URLs为:[file:/f:/test/] Test1 -> java.net.URLClassLoader@dd41677的父类加载器为:sun.misc.Launcher$ExtClassLoader@3dcc0a0f Test1 -> 使用java.net.URLClassLoader@dd41677已加载:class Test2 Test2 -> 这里是test() Test2 -> 使用Class.forName()加载Test3 Test2 -> 成功使用java.net.URLClassLoader@dd41677加载:class Test3 Test2 -> 直接加载Test4 Test2 -> 成功使用java.net.URLClassLoader@dd41677加载:class Test4 Test2 -> 但我还可以通过Thread.currentThread().getContextClassLoader()拿到这个类加载器:sun.misc.Launcher$AppClassLoader@56e88e24 Test2 -> 我是不是可以干点坏事? */ } }
public class Test2 { public void test(){ System.out.println("Test2 -> 这里是我的test()方法"); try { System.out.println("Test2 -> 使用Class.forName(...)加载Test3"); String pkg = Test2.class.getPackage() != null ? Test2.class.getPackage() + "." : ""; Class clazz3 = Class.forName(pkg + "Test3"); System.out.println("Test2 -> 成功使用" + clazz3.getClassLoader() + "加载:" + clazz3); System.out.println("Test2 -> 直接加载Test4"); System.out.println("Test2 -> 成功使用" + Test4.class.getClassLoader() + "加载:" + Test4.class); } catch (ClassNotFoundException e) { e.printStackTrace(); } System.out.println("Test2 -> 但我还可以通过Thread.currentThread().getContextClassLoader()拿到这个类加载器:" + Thread.currentThread().getContextClassLoader()); System.out.println("Test2 -> 有后门!我是不是可以干点坏事?"); } }
这个示例最后的结果,Test2还能通过Tread.currentThread().getContextClassLoader()方法,获取Test1的类加载器,因此正确的做法还需要把线程的类加载器给设置掉。
现在线程的类加载器,不仅显得多余,还提供了后门。我思考了一下,可能有这种需求出现,有一个类提供了一个方法服务,这个方法使用到了当前线程的类加载器加载资源,然后有多条线程用到了这个方法,也就是说这个类并不是这多条线程的任何一个类加载器加载的,换句话说每个线程访问这个类,需要提供自己一个资源库,才能完成整个方法的服务。关于线程的类加载器,我了解并不多,有知道的小伙伴也告知我一下。
属性与状态
s+currentThread():获得当前线程对象。
getId():线程唯一的ID。
getName()/setName(String):线程名称。
isDaemon()/setDaemon(boolean):是否为守护线程,初始值继承创建时当前线程。
getPriority()/setPriority(int):优先度,初始值继承创建时当前线程,数值越大,得到运行的机会越多。最小值为MIN_PRIORITY,最大值为MAX_PRIORITY,中间值NORM_PRIORITY。
State getState():线程的运行状态。
isInterrupted()/s+interrupted():测试目标线程/当前线程是否已经调用了interrupted()或已经结束。
isAlive():测试线程是否仍在活动,即未结束。
getStackTrace()/getAllStackTraces()/s+dumpStack():获取目标线程/打印当前线程的运行栈。
public static void main(String[] args) { test1(); /* 运行结果: java.lang.Thread.getStackTrace(Thread.java:1588) Test.test3(Test.java:20) Test.test2(Test.java:16) Test.test1(Test.java:12) Test.main(Test.java:8) */ } public static void test1(){ test2(); } public static void test2(){ test3(); } public static void test3(){ for(StackTraceElement stack : Thread.currentThread().getStackTrace()){ System.out.println(stack); } }
s+holdsLock(Object):测试当前线程是否获得目标锁。
private static Object lock = new Object(); public static void main(String[] args) throws InterruptedException { test(); synchronized(lock){ test(); } // 运行结果: // 未获得锁,不安全! // 已获得锁,已安全! } public static void test(){ if(Thread.holdsLock(lock)) System.out.println("已获得锁,已安全!"); else System.out.println("未获得锁,不安全!"); }
分组与管理
线程分组ThreadGroup,是一个线程集合,同时也是一个树节点,相当于文件夹,里面可以存放文件和子文件夹。
代码示例:使用ThreadGroup#list()方法,打印出根线程组下的线程和子线程组信息。
public static void main(String[] args) { Thread thread1 = new Thread("my thread1") { public void run() { Object lock = new Object(); synchronized(lock){ try { lock.wait(); } catch (InterruptedException e) { // e.printStackTrace(); } } } }; thread1.start(); Thread thread2 = new Thread(new ThreadGroup("my group"), "my thread2") { public void run() { Object lock = new Object(); synchronized(lock){ try { lock.wait(); } catch (InterruptedException e) { // e.printStackTrace(); } } } }; thread2.start(); ThreadGroup group = Thread.currentThread().getThreadGroup(); while (group.getParent() != null) group = group.getParent(); group.list(); thread1.interrupt(); thread2.interrupt(); /* 运行结果: java.lang.ThreadGroup[name=system,maxpri=10] Thread[Reference Handler,10,system] Thread[Finalizer,8,system] Thread[Signal Dispatcher,9,system] Thread[Attach Listener,5,system] java.lang.ThreadGroup[name=main,maxpri=10] Thread[main,5,main] Thread[my thread1,5,main] java.lang.ThreadGroup[name=my group,maxpri=10] Thread[my thread2,5,my group] */ }
示例中,运行结果的分组与线程信息如下图所示:
system分组就是一个根线程组,其下就是main分组,main分组就是包含主线程的分组。
线程在创建时,可以选择提供线程组,未提供时就会使用默认的线程组,默认线程组的提供策略由SecurityManager#getThreadGroup()决定,而默认的提供策略为创建时当前线程所属的线程组。
关于线程组,在上文中的异常和权限部份都有提到,但它的主要作用是批量管理线程。
说明:图中有两种父子关系,一种是直接的父子关系,一种是包括直接与间接的父子关系。
- getParent():获取父分组,可以在创建线程组时选择提供,不提供时默认使用创建时当前线程所属的线程组。
- activeCount():查看所有活动的子线程数量。
- activeGroupCount():查看所有活动的子分组数量。
- enumerate(...):可以获取直接的或者所有的子线程或子分组集合。
- parentOf(ThreadGroup):测试目标分组是否为本分组,或者本分组的直接或间接子分组。
- list():打印所有子分组和所有线程信息。
- interrupt()/suspend()/resume()/stop():批量执行所有子线程的操作。
- getMaxPriority()/setMaxPriority(int):所有线程优先度和所有子分组最大优先度的最大值。改变后,只能对未启动的线程有效。
线程组的属性,状态和操作:
- getName():线程组名称。
- isDestroyed()/destroy():是否销毁/执行销毁。
- isDaemon()/setDaemon(boolean):true时,表示其为一个守护线程组,即如果最后一个线程结束并且最后一个子分组被销毁,那么本分组就会自动销毁。
- checkAccess():测试是否拥有修改权限。测试策略由SecurityManager#checkAccess(ThreadGroup)提供,默认的策略为:如果目标线程组为根线程组,则需要权限MODIFY_THREADGROUP_PERMISSION,否则抛出SecurityException。
待续更新!