美文网首页
Java线程简介及线程的创建(一)

Java线程简介及线程的创建(一)

作者: 骑着乌龟去看海 | 来源:发表于2018-07-08 08:18 被阅读23次

    一、前言

      虽然工作中也偶尔用到线程相关代码,但由于以前学习的时候并不系统,所以从本篇文章开始,重新梳理并系统的学习下线程相关。本篇文章主要简单介绍下线程,然后是线程的几种创建方式。

    本文及后续的文章,代码所使用的JDK版本都是:JDK 8。

    二、线程和进程

    学习线程之前,我们还是要先了解下进程。

    1. 进程

      大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮询的抢占式调度方式,也就是说一个任务执行一小段时间后会暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行暂停后去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来,这样每个任务都能得到执行,而这每个任务,就对应一个进程。

    1. 每个进程对应一定的内存地址空间,并且只能使用自己的内存空间,各个进程间互不干扰,并且进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能。当进程暂时时,它会保存当前进程的状态(比如进程标识、进程的使用的资源等),在下一次重新切换回来时,便根据之前保存的状态进行恢复,然后继续执行。
    2. 由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发,而这种宏观意义上的并发,就是通过进程来实现的。不过,对于单核CPU而言,任何具体的某一时刻,都只会有一个任务在占用CPU资源。
    2. 线程

      后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。也就是说进程的执行不可能是一条逻辑执行的,必定有多个子任务,因为一个进程在一个时间段内只能做一件事情,如果一个进程有多个子任务,那只能逐个地去执行这些子任务。在这种情况下,人们发明了线程的概念。

    1. 通过线程去执行进程的每个子任务,这样一个进程就包括了多个线程,每个线程负责一个独立的子任务。如果说,进程让操作系统的并发成为可能,而线程则是让进程的内部并发成为可能;
    2. 一个进程内的所有线程共享进程所占有的资源和内存空间,所以,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位;
    3. 并行和并发

      有了进程和线程后,就会涉及到并发(concurrency),而有了多核CPU后,自然也会涉及到并行(parallellism)

    1. 并行是指两个或多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生;或者说,并发是在一台处理器上“同时”处理多个任务,而并行是在多台处理器上同时处理多个任务;
    2. 因为并行是基于多处理器的,所以说在单核CPU系统上,只可能存在并发而不可能存在并行;
    3. 也就是说,并发是时间段内有很多的进程在执行,但任何时间点上都只有一个在执行,多个进程轮询CPU的时间片执行,或者说CPU在不同的进程之间切换,每个进程只运行一小段时间。只不过这个时间特别短,短到我们察觉不到它们断断续续的执行,所以我们就感觉好像是在同时运行多个进程,而进程切换其实主要就是提升交互体验。
    concurrency and parallel(图片来自github).png

    三、线程的几个基本概念

    1. 线程安全性

      同一进程下的线程是共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),但进程内的线程在其他进程则是不可访问的。对线程安全而言,其核心在于对状态访问的管理,特别是共享和可变状态的访问。共享意味着变量可以由多个线程同时访问,而可变则意味着变量的值在其生命周期内是会发生变化的。说了这么多,那到底什么是线程安全性呢?(这里直接引用《Java并发编程实战》中的内容)

    当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的性为,那么就称这个类是线程安全的。

    2. 竞态条件

      在同一程序中运行多个线程本身不会导致问题,但如果多个线程访问了同一资源,并且如果由于执行顺序而出现不正确的结果,这种情况就被称为竞态条件(Race Condition)。最常见的竞态条件就是“先检查后执行”(Check-Then-Act),也就是基于一个可能失效的结果来决定下一步的操作,比如常见的代码:

    public Instance getInstance() {
        if (instance == null) {
            instance = new Instance();
        }
        return instance;
    }
    

      和大多数并发错误一样,竞态条件并不总是会产生问题,还需要某种不恰当的执行时序。而要避免出现这个问题,就需要保证在某个线程修改该变量时,通过某种方式阻止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取状态,也就是该线程执行的操作相对其他线程来说是原子的。后续我们将会来学习解决这类问题的方式,比如加锁操作等。

    3. 临界区

    临界区其实和竞态条件有关联的,竞态条件发生的代码区就被称作临界区。来看一个简单的例子:

    public class Counter {
        protected long count = 0;
        public void add(long value){
            this.count = this.count + value;  
        }
    }
    

    在上述方法中,多线程环境中,add方法的结果将不准确,所以add方法就是一个临界区,在临界区中可以使用synchronized或其他加锁的方式可以避免竞态条件的出现。这里参考自:http://ifeve.com/race-conditions-and-critical-sections/

    4. 线程的状态

      说到线程,就不得不说一下线程的状态,线程从创建到结束,一般会有6个状态,这6个状态对应于Thread类中的State枚举类型,分别是NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED

    1. NEW:这个状态是说线程创建了但还没有调用start方法进行启动,这时候的状态就是NEW
    1. RUNNABLE:可运行状态,这个状态其实包含了操作系统状态中的就绪(
      READY)和运行中(RUNNING)两种状态。也就是说处于此状态的线程可能正在等待被线程调度选中,获取CPU的使用权,此时就处于一种就绪状态;就绪状态的线程在获取CPU的时间片之后就变为了运行中状态;
    1. BLOCKED:阻塞状态,线程被挂起,处于阻塞状态,通常是阻塞于锁,比如线程准备进入一个synchronized语句块/方法时,锁已经被其它线程占有,该线程就会被阻塞,直到另一个线程释放锁。
    1. WAITING:处于该状态的线程不会自动醒来,需要显示的被其他线程唤醒,这种状态通常是指一个线程拥有对象锁后进入到相应的代码区域后,调用相应的“锁对象”的wait()方法操作后产生的一种结果,它们在等待另一个事件的发生,该线程就是等待的意思,根据API,如下方法会让线程进入WAITING状态:
      (1)没有设置timeout的Object.wait()
      (2)没有设置timeout的Thread.join()
      (3)LockSupport.park()
    1. TIMED_WAITING:超时等待状态,有时也称为休眠状态,该状态和WAITING状态不同的地方是,该状态不需要等待其他线程来唤醒,在一定时间之后它们会由系统自动唤醒,就是说有一定的等待时间。根据API,如下方法会让线程进入TIMED_WAITING状态:
      (1)Thread.sleep()方法
      (2)设置了timeout的Object.wait()方法
      (3)设置了timeout的Thread.join()方法
      (4)LockSupport.parkNanos()方法
      (5)LockSupport.parkUntil()方法
    1. TERMINATED:线程的终止状态,此时线程已经执行结束;线程一旦终止,就不能再复活,在一个终止的线程上调用start方法,将会直接抛出异常。
    5. 线程优先级

      线程是有优先级的,Java中的线程优先级的范围是 1~10,默认的优先级是5,对应于Thread类中的priority:

    /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;
    
    /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;
    
    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;
    

    但需要注意的是,优先级高的线程并不是一定先执行的,只是优先执行的概率比较大,毕竟是一种抢占式的调度方式,抢占的时候,优先级高的线程机会大一点而已。

    6. 守护线程

      Java中的线程一般分为用户线程(User Thread)和守护线程(Daemon Thread),我们上面所说的其实都是用户线程,守护线程和用户线程其实没什么大的区别,要说区别的话,可能在用途方面有些简单的区别:

      守护线程的作用是为了其他线程进行提供服务,比如说垃圾回收线程等;所以说守护线程依赖于创建它的线程,而用户线程则不依赖。比如说:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也就没有要服务的线程了,自然也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕;

    Thread 类中有对应的设置线程为守护线程及判断线程是否是守护线程的方法:

    private boolean     daemon = false;
    public final void setDaemon(boolean on)
    public final boolean isDaemon()
    

    四、线程的创建

      Java中,线程最基本的类是Thread,而基本的接口是Runnable接口,Callable接口。目前,在Java中创建线程,一般会有如下几种方式:

    1. 继承Thread类

      我们通过定义Thread类的子类,并重写该类的run方法,该方法被称为线程执行体,也就是线程需要完成的任务,然后调用线程的start方法来启动线程:

    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("test Thread");
        }
    }
    public class ThreadTest {
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            myThread.start();
        }
    }
    

    当然,我们也可以通过匿名内部类的形式来实现:

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("test Thread");
            }
        };
        thread.start();
    }
    

    当然,JDK 8之后的可以通过Lambda表达式来实现:

    Thread thread = new Thread(() -> System.out.println("test Thread"));
    thread.start();
    
    2. 实现Runnable接口

      实现Runnable接口的话,同样需要重写run方法,然后将Runnable实现类作为Thread的构造参数,最后通过Thread的start方法来启动线程:

    public class ThreadTest {
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread thread = new Thread(myRunnable);
            thread.start();
        }
    }
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("test Runnable");
        }
    }
    

    同样,可以借助匿名内部类来实现:

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test Runnable");
            }
        });
        thread.start();
    }
    

    Runnable翻译为“任务”,顾名思义,通过实现Runnable接口,我们定义了一个子任务,然后将子任务交由Thread去执行。

    3. 使用Callable和Future创建线程

      JDK1.5之后,创建线程的方式还可以通过Callable接口来实现,Callable接口和Runnable接口类似,提供了call方法来作为线程执行体,并且该方法可以有返回值,支持泛型,可以抛出异常。而Future接口则表示的是异步计算的结果,通过该对象我们可以了解任务执行情况,是否执行完成,以及任务执行的结果。

    public class ThreadTest {
        public static void main(String[] args) {
           try {
               ExecutorService executor = Executors.newCachedThreadPool();
               Future<Integer> future = executor.submit(new MyCallable());
               System.out.println("future result:" + future.get());
               System.out.println("callable is done:" + future.isDone());
           } catch (Exception e) {
               System.out.println("exception:" + e);
           }
        }
    }
    
    class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("test callable");
            return 100;
        }
    }
    

    本文参考,并强烈推荐:海子 - Java并发编程:进程和线程之由来
    另一参考:《Java并发编程实战》

    相关文章

      网友评论

          本文标题:Java线程简介及线程的创建(一)

          本文链接:https://www.haomeiwen.com/subject/begnuftx.html