线程

本文原创,转载请标明原处!

启动与入口

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。

待续更新!

原文地址:https://www.cnblogs.com/hvicen/p/6218981.html