zoukankan      html  css  js  c++  java
  • 线程

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

    启动与入口

    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。

    待续更新!

  • 相关阅读:
    ssh反向连接配置
    综合实践
    20199323 2019-2020-2 《网络攻防实践》第12周作业
    20199323 2019-2020-2 《网络攻防实践》第10周作业
    20199323 2019-2020-2 《网络攻防实践》第8周作业
    20199323 2019-2020-2 《网络攻防实践》第6周作业
    20199323 2019-2020-2 《网络攻防实践》第五周作业
    实践三 网络嗅探与协议分析
    20199114 《网络攻防实践》 综合实践
    20199314 2019-2020-2 《网络攻防实践》第12周作业
  • 原文地址:https://www.cnblogs.com/hvicen/p/6218981.html
Copyright © 2011-2022 走看看