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属性。

  • 相关阅读:
    [Git & GitHub] 利用Git Bash进行第一次提交文件
    Linux下 Unison 实现文件双向同步
    Linux SSH使用公钥私钥实现免登陆
    SSH自动断开连接的原因
    hosts.deny 和hosts.allow 配置不生效
    bind启动时提示953端口被使用
    Linux查询系统配置常用命令
    Linux 查硬件配置
    BIND rndc—使用说明
    rndc 错误解决 和 远程配置
  • 原文地址:https://www.cnblogs.com/ShouWangYiXin/p/10965284.html
Copyright © 2011-2022 走看看