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

原文地址:https://www.cnblogs.com/ShouWangYiXin/p/10965284.html