zoukankan      html  css  js  c++  java
  • java高并发编程--01--认识线程与Thread,ThreadGroup

    1.线程简介
    线程:
    操作系统有多任务在执行,对计算机来说每一个任务就是一个进程(Process),每一个进程内部至少有一个线程(Thread)在运行。线程是程序执行的一个路径,每一个线程都有自己的局部变量表,程序计数器及各自的生命周期。

    线程的生命周期:
    线程生命周期分以下5个阶段
    NEW:new方法创建一个Thread对象,可以通过start方法进入RUNNABLE状态,此时线程尚不存在,Thread对象只是一个普通的java对象。

    RUNNABLE:线程对象调用start方法进入此状态,此时才真正的在JVM进程中创建一个线程,线程具备执行的资格,当CPU调度到它就可以执行。

    RUNNING:线程正则执行的状态。该状态下可以进行如下切换:
      直接进入TERMINATED状态,如调用stop方法; 进入BLOCKED状态,如调用sleep、wait方法、如调用阻塞的IO操作,如为获取某个锁进入该锁的阻塞队列;进入RUNNABLE,如CPU将执行切换到其他线程,调用yield方法放弃执行权

    BLOCKED:此状态可以进行如下状态切换:
      直接进入TERMINATED状态,如调用stop方法或意外死亡(JVM Crash);进入RUANNABLE状态,如io阻塞结束,wait结束,获取到锁,如线程阻塞被打断进入RUNNABLE状态

    TERMINATED:线程生命周期结束。有以下情况进入此状态:
      正常结束
      线程运行错误意外结束
      JVM Crash

    2.线程的Start方法
    Thread 类 start方法源码:

    public synchronized void start(){
        if(threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        boolean started = false;
        try{
            start0();
            started = true;
        }finally{
            try{
                if(!started){
                    group.threadStartFailed(this);
                }
            }catch(Throwable ignore){
            }
        }
    }

    其核心是对JNI方法start0()的调用
    jdk文档对start()方法的描述是:使这个线程开始运行,jvm调用这个线程的run()方法。
    由此可以看出run()方法是被JNI方法start0()调用.

    3.Runnable 接口的引入及策略模式在Thread类中的使用

    Runnable接口的职责声明线程的执行单元
    创建线程有两种方式(构造Thread类与实现Runnable接口)的说法是错误的、不严谨的,jdk中代表线程的只有Thread类,这两种方式实际上是实现线程执行单元的两种方式。
    Thread的行为在run()方法中进行定义,run()方法是被JNI方法start0()调用,Thread类通过使用策略模式来改变线程的行为,如下所示:

        //Thread的run方法:
        @Override
        public void run() {
            if (target != null) {
                target.run();
            }
        }
        
        //Thread的target声明:    
        private Runnable target;
        
        //Thread的target赋值:
        public Thread(Runnable target) {
            this(null, target, "Thread-" + nextThreadNum(), 0);
        }
         public Thread(ThreadGroup group, Runnable target, String name,
                      long stackSize) {
            this(group, target, name, stackSize, null, true);
        }
        private Thread(ThreadGroup g, Runnable target, String name,
                       long stackSize, AccessControlContext acc,
                       boolean inheritThreadLocals) {
            。。。
            this.target = target;
            。。。
        }

    4.Thread构造函数
    4.1线程命名
    线程默认以Thread-X命名,X为jvm内维护的一个自增长整数。

        public Thread(Runnable target) {
            this(null, target, "Thread-" + nextThreadNum(), 0);
        }
        private static int threadInitNumber;
        private static synchronized int nextThreadNum() {
            return threadInitNumber++;
        }

    建议给线程指定名字,以便维护。

    4.2线程父子关系
    一个线程肯定被另外一个线程所创建
    被创建的线程的父线程是创建它的线程

    4.3Thread和ThreadGroup
    在Thread构造函数中,可以显示指定ThreadGroup,如果不指定,默认使用父线程的,Thread的初始化方法中相关代码如下:

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        this.group = g;

    4.4jvm内存结构
    jvm结构图如下

    1)程序计数器
    程序计数器为线程私有,用于存放当前线程接下来要执行的字节码指令、分支、循环、跳转、异常处理等信息,以便能够在CPU时间片轮转切换上下文后顺利回到正确执行位置。

    2)java虚拟机栈
    java虚拟机栈为线程私有,生命周期同线程相同,jvm运行时创建。
    线程中,方法执行时会创建一个名为栈帧的数据结构,用于存放局部变量、操作栈、动态链接、方法出口等信息。
    方法的调用则对应着栈帧在虚拟机栈中的压栈和弹栈过程。
    虚拟机栈可以通过-xss来配置

    3)本地方法栈
    java提供了调用本地方法的接口(Java Native Interface),也就是c/c++程序,在线程执行过程中,经常会有调用JNI方法的情况、如网络通信、问卷操作的底层,深圳String的intern。JVM为本地方法划分出来的内存区域为本地方法栈,为线程私有内存区域。

    4)堆内存
    堆内存为jvm最大的一块内存,所有线程共享,java运行时几乎所有对象都存在该内存区域。

    5)方法区
    方法区被多个线程共享的内存区域,用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的数据。

    2.5守护线程
    在正常情况下,若jvm中没有一个非守护线程,则jvm进程会退出。
    守护线程一般用于处理一些后台工作,如jdk的来讲回收线程。
    如下代码:

    public class DaemonThreadTest {
        public static void main(String[] args) throws Exception {
            //1)线程开始
            Thread t = new Thread(() -> {
                while(true) {
                    try {
                        Thread.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            //t.setDaemon(true);//2)调用setDaemon()方法将线程设置为守护线程
            t.start();//3)启动线程
            Thread.sleep(10000);
            System.out.println("main线程结束");
            //4)主线程结束
        }
    }

    代码中有两个线程,一个是jvm启动的main线程,一个自己创建的线程。
    运行上面的线程会发现main线程结束,进程仍未结束,原因是有我们自己创建的一个非守护线程还在运行。
    打开2)处的注释,会发现main线程结束,进程也就结束,因为我们创建的线程被设置为守护线程
    线程是否为守护线程默认继承其父线程。
    守护线程具有自动结束生命周期的特性,而非守护线程不具备这个特点。

    5 Thread API

    5.1 线程sleep
    sleep方法使当前线程进入指定时间长度的休眠,暂停执行,虽然给定类一个休眠的时间,但是最终要以系统的定时器和调度器的精度为准。
    jdk引入TimeUnit枚举类,可以在休眠时免去时间换算,如休眠1天2小时3分4秒可以如下写:
    TimeUnit.DAYS.sleep(1);
    TimeUnit.HOURS.sleep(2);
    TimeUnit.MINUTES.sleep(3);
    TimeUnit.SECONDS.sleep(4);

    5.2线程yield
    yield方法属于一种启发式的方法,会提醒调度器我愿意放弃当前CPU资源,如果CPU资源不紧张,则会忽略这种提醒
    sleep和yield:
    1)sleep会导致当前线程暂停指定时间,没有CPU时间片的消耗
    2)yield只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,它会导致线程上下文的切换
    3)sleep会使线程短暂block,会在给定的时间内释放CPU资源
    4)yield会使RUNNING状态的Thread进入RUNABLE状态(如果CPU调度器没有忽略这个提示)
    5)sleep几乎百分百地完成给定时间的休眠,而yield的提示不能一定担保
    6)一个线程sleep另一个线程调用interrupt会捕捉中断信号,而yield不会

    5.3线程优先级
    线程可以设置优先级,不过是一种暗示性操作
    线程设置优先级方法源码如下:

        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);
            }
        }

    由代码可以看出,优先级是是有范围的,取决于线程所在的ThreadGroup
    一般不设置线程优先级,考虑到业务需求,往往借助优先级设置线程谁先执行不可取的。

    5.4获取线程ID
    public long getId(),获取线程唯一ID,该ID在jvm进程中是惟一的

    5.5线程interrupt
    1)public void interrupt()
    线程进入阻塞状态,调用interrupt方法可以打断阻塞,打断一个线程并不等于该线程的生命周期结束,紧急是打断类这个线程的阻塞状态。
    线程阻塞被打断会报错InterruptedException异常。
    interrupt方法做了什么?在一个线程中存在着名为interrupt flag的表示,如果一个线程被interrupt,那么它的flag将被设置,但是如果当前线程正则执行可中断方法被阻塞时,调用interrupt方法将其中断,反而会导致flag被清除。对一个已经死亡的线程调用interrupt会直接被忽略。这段话比较拗口,看了2)后再看下面代码讲解:
    代码1:

        public static void main(String[] args) throws Exception {
            Thread t = new Thread(()-> {
                double d = Double.MIN_VALUE;
                while(d < Double.MAX_VALUE-1) {
                    d += 0.1;
                }
                System.out.println("HA Ha");
            });
            t.start();
            Thread.sleep(1000);//main线程休眠一会,保证线程t获得CPU执行权真正开始执行
            System.out.println(t.isInterrupted());//打断前
            t.interrupt();//打断
            System.out.println(t.isInterrupted());//查看线程t的flag
            Thread.sleep(1000);//main线程休眠一会,保证线程t获得CPU执行权继续执行
            System.out.println(t.isInterrupted());//再次查看线程t的flag
        }

    代码1输出如下:
    false
    true
    true

    代码1中,线程t内的while循环调用的方法是不会让线程t出现阻塞的,即非阻塞方法,即非可中断方法,main线程对其调用interrupt方法,为它设置类flag,flag一种存在

    代码2:

        public static void main(String[] args) throws Exception {
            Thread t = new Thread(()-> {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (Exception e) {
                    System.out.println("I'm interrepted");
                }
                double d = Double.MIN_VALUE;
                while(d < Double.MAX_VALUE-1) {
                    d += 0.1;
                }
                System.out.println("HA Ha");
            });
            t.start();
            Thread.sleep(1000);//main线程休眠一会,保证线程t获得CPU执行权真正开始执行
            System.out.println(t.isInterrupted());//打断前
            t.interrupt();//打断
            System.out.println(t.isInterrupted());//查看线程t的flag
            Thread.sleep(1000);//main线程休眠一会,保证线程t获得CPU执行权继续执行
            System.out.println(t.isInterrupted());//再次查看线程t的flag
        }

    代码2输出如下:
    false
    true
    I'm interrepted
    false

    代码2中,线程t先是休眠10秒,休眠为阻塞方法,为可中断方法,在这段时间内,main线程调用它的interrupt方法,调用前查看标志,flag为false,调用后立即查看标志,flag为ture,等一会,待t线程继续执行抛出异常后,再次查看线程t的flag,flag为false,说明线程flag被清除类,且应是抛出异常时清除的。

    2)isInterrupted
    isInterrupted是Thread类的一个成员方法,主要是判断指定线程是否被中断,该方法仅仅是对interrupt标识的一个判断,并不会影响标识发生任何改变。

    3)interrupted
    interrupted是一个静态方法,用于判断当前线程是否被中断,与isInterrupted方法有很大区别,一是interrupted方法判断的是当前线程,而isInterrepted判断的是调用的那个线程;二是interrupted方法会清除interrupt标识,而isInterrupted不会。
    如果当前线程被打断类,那么第一次调用interrupt方法会返回true,并且立即擦除interrupt标识,第二次包括以后永远都会返回false,除非再次打断。
    验证代码如下:

        public static void main(String[] args) throws Exception {
            Thread t = new Thread(()-> {
                while(true) {
                    //因为判断的是当前线程,只能将判断方法写在线程t的run方法范围内
                    System.out.println(Thread.interrupted());
                }
            });
            t.setDaemon(true);
            t.start();
            Thread.sleep(10);
            t.interrupt();
            Thread.sleep(1);
            t.interrupt();
            Thread.sleep(1);
        }

    为了避免抛出异常时清除标识的影响,t中没有用sleep,因此会有很多输出
    输出如下:
    false
    false
    ...
    true
    false
    false
    ...
    true
    false
    false
    ...
    输出中只有两个true,有很多false,因为t线程全程被打断类两次。

    4)注意事项
    Thread类中,isInterrupted方法和interrupted方法调用的是同一个JNI方法,如下:

        public boolean isInterrupted() {
            return isInterrupted(false);
        }
        public static boolean interrupted() {
            return currentThread().isInterrupted(true);
        }
        private native boolean isInterrupted(boolean ClearInterrupted);

    JNI方法isInterrupted的ClearInterrupted参数用来控制是否擦除interrupt标识,两个调用分别传入不同参数

    一个线程如果设置了interrupt标识,那么接下来执行可打断方法时会被立即打断,如sleep方法,验证代码如下:

        public static void main(String[] args) throws Exception {
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SS");
            System.out.println("测试开始:"+sdf.format(new Date()));
            Thread t = new Thread(()-> {
                try {
                    System.out.println("初次打断:"+sdf.format(new Date()));
                    Thread.currentThread().interrupt();
                    TimeUnit.MINUTES.sleep(3);
                } catch (InterruptedException e) {
                    System.out.println("休眠打断:"+sdf.format(new Date()));
                }
            });
            System.out.println("线程启动:"+sdf.format(new Date()));
            t.start();
            TimeUnit.MINUTES.sleep(1);
            System.out.println("再次打断:"+sdf.format(new Date()));
            t.interrupt();
        }

    上面代码初始设置打断只能在t的run方法内,因为start前线程t并不真正存在
    输出结果如下:
    测试开始:14:45:58.536
    线程启动:14:45:58.539
    初次打断:14:45:58.540
    休眠打断:14:45:58.540
    再次打断:14:46:58.541

    5.6线程的join
    join某个线程A,会使当前线程B进入等待,直到线程A结束生命周期或者达到给定的时间,在此期间线程B将一直处在BLOCKED状态。

    import java.util.List;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    
    public class ThreadJoin {
        public static void main(String[] args) {
            //1 定义两个线程,放到threads中
            List<Thread> threads = IntStream.range(1, 3).mapToObj(ThreadJoin::create).collect(Collectors.toList());
            //2启动两个线程
            threads.forEach(Thread :: start);
            //3执行两个线程的join
            threads.forEach(t -> {
                try {
                    System.out.println(t.getName() + ",join");
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            //4main线程自己的输出
            for(int i = 0;i < 5;i ++) {
                System.out.println(Thread.currentThread().getName() + ",#" + i);
                shortSleep();
            }
        }
        //构造一个简单线程,每一个线程只是简单的输出和休眠
        private static Thread create(int seq) {
            return new Thread(()->{
                for(int i = 0;i < 5;i ++) {
                    System.out.println(Thread.currentThread().getName() + ",#" + i);
                    shortSleep();
                }
            },"Thread=="+seq);
        }
        
        private static void shortSleep() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    结果:
    Thread==1,join
    Thread==2,#0
    Thread==1,#0
    Thread==1,#1
    Thread==2,#1
    Thread==1,#2
    Thread==2,#2
    Thread==1,#3
    Thread==2,#3
    Thread==2,#4
    Thread==1,#4
    Thread==2,join
    main,#0
    main,#1
    main,#2
    main,#3
    main,#4

    由上面的输出可以看出:1)join方法可以使当前线程永远等待下去,直到期间被另外的线程中断或者join的线程执行结束或者join指定的时间结束。2)一个线程同一段时间只可以被一个线程join,因为Thread.join()这段代码要在被join的线程中执行,而执行完这一句被join的线程就阻塞类,后面的join语句必须等前面的join结束后才能被执行到

    6关闭线程
    6.1正常关闭
    1)线程结束生命周期
    2)捕获中断信号关闭:循环执行任务,循环中调用isInterrupted方法检查中断标志,若中断结束循环
    3)使用volatile开个控制:循环执行任务,循环中检查volatile变量,volatile变量发生合适改变结束循环
    6.2异常突出
    线程的执行单元中适当地方抛出运行时异常结束当前线程

    7.ThreadGroup
    ThreadGroup也类似线程,存在父子ThreadGroup,若创建ThreadGroup时不指定父ThreadGroup,那么父ThreadGroup默认为当前线程的ThreadGroup。
    ThreadGroup也有interrupt操作,interrupt一个ThreadGroup将导致该group中所有的active线程都被interrupt,也就是该group中每一个线程的interrupt标识都被设置类。
    ThreadGroup也可以设置守护ThreadGroup,但设置daemon并不影响Group里面线程的daemon属性。

  • 相关阅读:
    PAT 1088. Rational Arithmetic
    PAT 1087. All Roads Lead to Rome
    PAT 1086. Tree Traversals Again
    PAT 1085. Perfect Sequence
    PAT 1084. Broken Keyboard
    PAT 1083. List Grades
    PAT 1082. Read Number in Chinese
    求最大公因数
    [转载]Latex文件转成pdf后的字体嵌入问题的解决
    [转载]Matlab有用的小工具小技巧
  • 原文地址:https://www.cnblogs.com/ShouWangYiXin/p/10965284.html
Copyright © 2011-2022 走看看