Java并发——Thread类解析、线程初探

版权声明:博主GitHub地址https://github.com/suyeq欢迎大家前来交流学习 https://blog.csdn.net/hackersuye/article/details/84567750

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。 --维基百科

    线程相较于进程,它是轻量级的,并且一个进程可以包含多个线程,这多个线程之间共享该进程的资源,且每个线程自己独占一份内存,这个空间叫做线程栈,是由系统创建该线程时创建的,主要是保存线程中的自定义的数据等。注意的是,线程依托于进程,当该进程消亡时,进程内所有的线程也会跟着消亡。而Java中的线程是抢占式的,即Java中的线程是互斥的,是当一个线程"抢到"资源时(如:CPU的运行时间),那么其它的线程便会陷入等待状态。

线程状态

    在Thread类中,定义了枚举类来定义线程的几个状态:

public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
  1. NEW状态:是线程被创建时的状态,因为创建线程时需要为线程分配内存空间,从调用线程创建函数开始到为线程分配资源的结束为止,都是NEW状态;
  2. RUNNABLE状态:严格意义上说,当NEW状态结束后便是RUNNABLE状态,即就绪状态,就绪状态是指该线程还没有得到CPU的执行时间而陷入等待的状态,当线程得到CPU的执行时间时,线程就进入RUNNING状态,即运行状态,但是Java为了代码的方便将两者统称为RUNNABLE状态;
  3. TIMED_WAITING状态:该状态是让正在RUNNING状态的线程主动睡眠一段时间(sleep方法实现),一段时间后它会自动唤醒并进入RUNNABLE状态;
  4. WAITING状态:该状态也是让正在RUNNING状态的线程主动等待一段时间(利用wait方法实现),但是不会自动唤醒,需要别的线程来将其唤醒并进入RUNNABLE状态;
  5. BLOCKED状态:Java中,该状态是指线程被同步代码块所阻塞,即线程需要的资源被抢占了,而让线程陷入等待。严格意义上说,TIMED_WAITING状态、WAITING状态以及BLOCKED状态都称之为BLOCKED状态,只要线程从RUNNING状态陷入等待且是活的时,都可称之为BLOCKED状态;
  6. TERMINATED状态:Java中指线程还没有执行完成就被异常终止时的状态,线程还未完全死透;
  7. Dead状态:线程成功执行完以及异常终止后的状态,也就是线程消亡时的状态。除了Dead状态以及 NEW状态,其它状态线程都是活的(alive)。

在这里插入图片描述

上下文切换

    谈到线程,一个经常能遇到的概念不得不谈,就是线程的上下文切换,它是指当前运行的一个线程主动进入等待,而去运行另外一个线程的过程,当执行完另外一个线程的任务切换回当前线程从睡眠的地方再次开始,这里就涉及到CPU的恢复以及从上次睡眠的点再次开始,线程上下文带来的开销就是保存线程的睡眠前的状态,因为不可能让线程从新开始的。说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
    关于Java中线程的创建,我们一般使用两种形式:

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    第一种是默认的线程创建,一般来说推荐第二种的创建形式,因为第二种代码的复用性及拓展性要高。因为创建一个自定义的线程需要继承Thread类或者实现Runnable接口:

public class MyThread extends Thread {
    @Override
    public void run(){
        System.out.println("MyThread");
    }
}

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable");
    }
}

MyThread myThread=new MyThread();
myThread.start();
Thread thread=new Thread(new MyRunnable());
thread.start();

    接着介绍Thread类里的重要的成员变量:

public class Thread implements Runnable {
    private static native void registerNatives();
    static {
        registerNatives();
    }
    private volatile String name;
    private int            priority;
    private boolean     daemon = false;
    private Runnable target;
    private ThreadGroup group;
    private long stackSize;
    private long tid;
    private volatile int threadStatus = 0;
}

    Thread类是实现了Runnable接口的,它在加载的时候直接注册进jvm。

  1. name:name是指线程的名字,Java中的线程名支持自定义;
  2. priority:指线程的优先级,在Java中最大的优先级为10,最小的为1,对相同的资源抢占的两个线程,优先级高的先执行;
  3. target:指该线程需要执行的任务,当实现Runnable接口自定义线程,传进Thread类的该自定义的线程就是target;
  4. stackSize:指线程需要的内存空间,即线程栈;
  5. tid:线程的id;
  6. threadStatus :线程的状态;
  7. group:指该线程下的线程组,在该线程下创建的线程都将被纳入该线程的线程组下,具体在ThreadGroup类讨论;
  8. daemon :该布尔值表明该线程是否是守护线程,默认是false,即不是守护线程,是用户线程 。

线程的种类

    关于线程的种类,Java中分为两种,一类是用户线程(User Thread),另一类是守护线程(Daemon Thread)。守护线程是服务用户线程的,所以它又被称作服务线程,当用户线程全部消亡时,服务线程也跟着消亡,最典型的守护线程便是GC,垃圾回收器。守护线程一般优先级很低,因为它是服务用户线程的,jvm必须保持用户线程的的性能基础上才考虑守护线程的性能。在Java中,自定义的线程默认是用户线程,只有当调用Thread类的setDaemon方法将之设置为守护线程,通过下面一个例子来看看两者的关系:

public class MyThread1 extends Thread {
    @Override
    public void run(){
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("你好啊,蕾姆");
    }
}

public class MyThread2 extends Thread {
    public void run(){
        try {
            System.out.println("蕾姆进来了");
            Thread.currentThread().sleep(5000);
        } catch (InterruptedException e) {
            System.out.println("蕾姆异常退出了");
            e.printStackTrace();
        }
        System.out.println("蕾姆退出了");
    }
}
//测试
MyThread1 myThread1 =new MyThread1();
myThread1.start();
MyThread2 myThread2=new MyThread2();
// myThread2.setDaemon(true);
myThread2.start();
System.out.println("主线程退出了");

    当按测试运行时,依次打印的是:

主线程退出了
蕾姆进来了
你好啊,蕾姆
蕾姆退出了

    而当将注释的代码注释去掉,即让MyThread2成为守护线程,其余的都不变,运行后结果为:

主线程退出了
蕾姆进来了
你好啊,蕾姆

    不会打印MyThread2线程运行最后打印的那句"蕾姆退出了",也就是说MyThread2线程被异常终止了。因为在测试程序里主要有两个用户线程,一个是main主线程,另外一个是自定义的MyThread1线程,当这两者都结束后,该程序中就没有用户进程了,也就是说守护线程就没有必要存在了,就会立马被终止掉。注意的是,只要程序中有一个用户线程,那么守护线程就会执行。关于线程创建时的init方法,源码如下:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
         //线程名不能为null
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        this.name = name;
        //获取当前线程,当前线程即为新创建的线程的父线程
        Thread parent = currentThread();
        //获取安全管理器
        SecurityManager security = System.getSecurityManager();
        //如果传进来的线程组为空
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        //检查权限
        g.checkAccess();
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        //线程组里未开始的线程加1
        g.addUnstarted();
        this.group = g;
        //父线程是什么子线程是什么类的线程
        this.daemon = parent.isDaemon();
        //获取线程的优先级
        this.priority = parent.getPriority();
        //设置加载器
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
           //设置访问权限
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
         //设置要执行的任务
        this.target = target;
        //设置优先级
        setPriority(priority);
        //将父线程的ThreadLocaleMap变量信息拷贝到当前线程实例
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
         //线程栈的大小
        this.stackSize = stackSize;
        //线程的id
        tid = nextThreadID();
    }

    用户线程创建的线程默认是用户线程,而守护线程创建的线程默认是守护线程。Thread类是通过一个本地方法来获取当前执行的线程的:

public static native Thread currentThread();

    关于start方法与run方法,虽然我们继承Thread类重写的是run方法,但是要启用一个新的线程执行我们重写的方法,需要调用Thread类的start方法,倘若直接调用run方法,那么不会创建一个新的线程执行自定义的任务,而是在当前线程执行该任务:

public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
         //将自身加入线程组
        group.add(this);
        boolean started = false;
        try {
        	//如果start0抛出异常,started 为false
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                	//从线程组移除掉该线程
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
    private native void start0();

public void run() {
        if (target != null) {
            target.run();
        }
    }

    关于sleep方法,它是一个Native方法,它主动让线程睡眠一段时间后自动唤醒,也就是调用sleep方法的线程处于TIME_WAITING状态:

public static native void sleep(long millis) throws InterruptedException;

    注意的是,sleep方法并不会主动的释放对象锁,如下面的例子:

public class MyThread extends Thread {
    Object object;
    public MyThread1(Object o){
        this.object=o;
    }

    @Override
    public void run(){
        synchronized (object){
            try {
                System.out.println("你好啊,蕾姆");
                sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//测试:
Object o=new Object();
MyThread myThread1=new MyThread1(o);
MyThread myThread2=new MyThread1(o);
myThread1.start();
myThread2.start();
//打印:
//你好啊,蕾姆
//你好啊,蕾姆

    两个"你好啊,蕾姆",是隔了5秒才打印出来了,而打印第二个后隔了5秒才结束,所以可以得出结论sleep方法并不会主动的释放对象锁。它与Object类的wait方法恰好相反,wait方法是释放对象锁的,sleep方法就是让线程进入阻塞状态的。关于yield方法,跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。同时,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,这一点是和sleep方法不一样的。

public static native void yield();

    关于join方法,它的底层调用的是Object类的join方法,它的源码如下:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

    当传入方法的millis大于等于0时,由于内部的while循环,让该线程处在一直等待被唤醒的状态,唤醒后会让该线程执行完到DEAD状态,再去执行另外的线程,但是这里调用了wait方法,并没有被唤醒,博主猜测可能是另外的线程唤醒了join方法里的wait。示例代码:

public class MyThread1 extends Thread {
    @Override
    public void run(){
            System.out.println("你好啊,蕾姆");
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("蕾姆走了");
    }
}

//测试
System.out.println("进入主线程");
MyThread1 myThread1=new MyThread1();
myThread1.start();
myThread1.join();
System.out.println("主线程退出了");
//打印:
//进入主线程
//你好啊,蕾姆
//蕾姆走了
//主线程退出了

    关于interrupt方法,它是用来中断处于阻塞状态下的线程的,但是并不能中断正在运行的线程,同时它也不能中断由于执行同步的I/O或者等待获取内置所而阻塞的线程。它调用的是Native方法,虽然不能通过interrupt方法来中断正在进行中的线程,但是可以巧妙的用isInterrupted 这个Native方法利用flag值来中断正在运行中的线程:

public void interrupt() {
    //.....
        interrupt0();
    }

    关于stop、destroy废弃以及其余的方法再次不再讲述。谈到这里,先提出一个问题,怎么实现线程间的数据共享?

猜你喜欢

转载自blog.csdn.net/hackersuye/article/details/84567750