线程状态转换以及基本操做

在上一篇文章中并发编程的优缺点谈到了为何花功夫去学习并发编程的技术,也就是说咱们必须了解到并发编程的优缺点,咱们在什么状况下能够去考虑开启多个线程去实现咱们的业务,固然使用多线程咱们应该着重注意一些什么,在上一篇文章中会有一些讨论。那么,说了这么多,不管是针对面试仍是实际工做中做为一名软件开发人员都应该具有这样的技能。万事开头难,接下来就应该了解如何新建一个线程?线程状态是怎样转换的?关于线程状态的操做是怎样的?这篇文章就主要围绕这三个方面来聊一聊。java

1. 新建线程

一个java程序从main()方法开始执行,而后按照既定的代码逻辑执行,看似没有其余线程参与,但实际上java程序天生就是一个多线程程序,包含了:面试

(1)分发处理发送给给JVM信号的线程;编程

(2)调用对象的finalize方法的线程;安全

(3)清除Reference的线程;多线程

(4)main线程,用户程序的入口。并发

那么,如何在用户程序中新建一个线程了,只要有三种方式:异步

  1. 经过继承Thread类,重写run方法;ide

  2. 经过实现runable接口;学习

  3. 经过实现callable接口这三种方式,下面看具体demo。this

 public class CreateThreadDemo {
 
     public static void main(String[] args) {
         //1.继承Thread
         Thread thread = new Thread() {
             @Override
             public void run() {
                 System.out.println("继承Thread");
                 super.run();
             }
         };
         thread.start();
         //2.实现runable接口
         Thread thread1 = new Thread(new Runnable() {
             @Override
             public void run() {
                 System.out.println("实现runable接口");
             }
         });
         thread1.start();
         //3.实现callable接口
         ExecutorService service = Executors.newSingleThreadExecutor();
         Future<String> future = service.submit(new Callable() {
             @Override
             public String call() throws Exception {
                 return "经过实现Callable接口";
             }
         });
         try {
             String result = future.get();
             System.out.println(result);
         } catch (InterruptedException e) {
             e.printStackTrace();
         } catch (ExecutionException e) {
             e.printStackTrace();
         }
     }
 
 }

 

三种新建线程的方式具体看以上注释,须要主要的是:

  • 因为java不能多继承能够实现多个接口,所以,在建立线程的时候尽可能多考虑采用实现接口的形式;

  • 实现callable接口,提交给ExecutorService返回的是异步执行的结果,另外,一般也能够利用FutureTask(Callable callable)将callable进行包装而后FeatureTask提交给ExecutorsService。如图

 

 

外因为FeatureTask也实现了Runable接口也能够利用上面第二种方式(实现Runable接口)来新建线程;

  • 能够经过Executors将Runable转换成Callable,具体方法是:Callable callable(Runnable task, T result), Callable callable(Runnable task)。

2. 线程状态转换

 

线程是会在不一样的状态间进行转换的,java线程线程转换图如上图所示。线程建立以后调用start()方法开始运行,当调用wait(),join(),LockSupport.lock()方法线程会进入到WAITING状态,而一样的wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增长了超时等待的功能,也就是调用这些方法后线程会进入TIMED_WAITING状态,当超时等待时间到达后,线程会切换到Runable的状态,另外当WAITING和TIMED _WAITING状态时能够经过Object.notify(),Object.notifyAll()方法使线程转换到Runable状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到BLOCKED阻塞状态,当线程获取锁时,线程进入到Runable状态。线程运行结束后,线程进入到TERMINATED状态,状态转换能够说是线程的生命周期。另外须要注意的是:

  • 用一个表格将上面六种状态进行一个总结概括。

3. 线程状态的基本操做

除了新建一个线程外,线程在生命周期内还有须要基本操做,而这些操做会成为线程间一种通讯方式,好比使用中断(interrupted)方式通知实现线程间的交互等等,下面就将具体说说这些操做。

3.1. interrupted

中断能够理解为线程的一个标志位,它表示了一个运行中的线程是否被其余线程进行了中断操做。中断比如其余线程对该线程打了一个招呼。其余线程能够调用该线程的interrupt()方法对其进行中断操做,同时该线程能够调用 isInterrupted()来感知其余线程对其自身的中断操做,从而作出响应。另外,一样能够调用Thread的静态方法 interrupted()对当前线程进行中断操做,该方法会清除中断标志位。须要注意的是,当抛出InterruptedException时候,会清除中断标志位,也就是说在调用isInterrupted会返回false。

 

  • 下面结合具体的实例来看一看

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        //sleepThread睡眠1000ms
        final Thread sleepThread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                super.run();
            }
        };
        //busyThread一直执行死循环
        Thread busyThread = new Thread() {
            @Override
            public void run() {
                while (true) ;
            }
        };
        sleepThread.start();
        busyThread.start();
        sleepThread.interrupt();
        busyThread.interrupt();
        while (sleepThread.isInterrupted()) ;
        System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
        System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
    }
}

 

  • 输出结果

    sleepThread isInterrupted: false busyThread isInterrupted: true

    开启了两个线程分别为sleepThread和BusyThread, sleepThread睡眠1s,BusyThread执行死循环。而后分别对着两个线程进行中断操做,能够看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。

    另外,一样能够经过中断的方式实现线程间的简单交互, while (sleepThread.isInterrupted()) 表示在Main中会持续监测sleepThread,一旦sleepThread的中断标志位清零,即sleepThread.isInterrupted()返回为false时才会继续Main线程才会继续往下执行。所以,中断操做能够看作线程间一种简便的交互方式。通常在结束线程时经过中断标志位或者标志位的方式能够有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。

 

3.2. join

join方法能够看作是线程间协做的一种方式,不少时候,一个线程的输入可能很是依赖于另外一个线程的输出,这就像两个好基友,一个基友先走在前面忽然看见另外一个基友落在后面了,这个时候他就会在原处等一等这个基友,等基友遇上来后,就两人携手并进。其实线程间的这种协做方式也符合现实生活。在软件开发的过程当中,从客户那里获取需求后,须要通过需求分析师进行需求分解后,这个时候产品,开发才会继续跟进。若是一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。关于join方法一共提供以下这些方法:

public final synchronized void join(long millis) public final synchronized void join(long millis, int nanos) public final void join() throws InterruptedException

Thread类除了提供join()方法外,另外还提供了超时等待的方法,若是线程threadB在等待的时间内尚未结束的话,threadA会在超时以后继续执行。join方法源码关键是:

 

 while (isAlive()) {
    wait(0);
 }
  • 能够看出来当前等待对象threadA会一直阻塞,直到被等待对象threadB结束后即isAlive()返回false的时候才会结束while循环,当threadB退出时会调用notifyAll()方法通知全部的等待线程。下面用一个具体的例子来讲说join方法的使用:

public class JoinDemo {
    public static void main(String[] args) {
        Thread previousThread = Thread.currentThread();
        for (int i = 1; i <= 10; i++) {
            Thread curThread = new JoinThread(previousThread);
            curThread.start();
            previousThread = curThread;
        }
    }

    static class JoinThread extends Thread {
        private Thread thread;

        public JoinThread(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
                thread.join();
                System.out.println(thread.getName() + " terminated.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果为:

main terminated. Thread-0 terminated. Thread-1 terminated. Thread-2 terminated. Thread-3 terminated. Thread-4 terminated. Thread-5 terminated. Thread-6 terminated. Thread-7 terminated. Thread-8 terminated.

在上面的例子中一个建立了10个线程,每一个线程都会等待前一个线程结束才会继续运行。能够通俗的理解成接力,前一个线程将接力棒传给下一个线程,而后又传给下一个线程......

 

3.3 sleep

public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。须要注意的是若是当前线程得到了锁,sleep方法并不会失去锁。sleep方法常常拿来与Object.wait()方法进行比价,这也是面试常常被问的地方。

sleep() VS wait()

二者主要的区别:

3.4 yield

public static native void yield();这是一个静态方法,一旦执行,它会是当前线程让出CPU,可是,须要注意的是,让出的CPU并非表明当前线程再也不运行了,若是在下一次竞争中,又得到了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。什么是线程优先级了?下面就来具体聊一聊。

现代操做系统基本采用时分的形式调度运行的线程,操做系统会分出一个个时间片,线程会分配到若干时间片,当前时间片用完后就会发生线程调度,并等待这下次分配。线程分配到的时间多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程须要或多或少分配一些处理器资源的线程属性。

在Java程序中,经过一个整型成员变量Priority来控制优先级,优先级的范围从1~10.在构建线程的时候能够经过**setPriority(int)**方法进行设置,默认优先级为5,优先级高的线程相较于优先级低的线程优先得到处理器时间片。须要注意的是在不一样JVM以及操做系统上,线程规划存在差别,有些操做系统甚至会忽略线程优先级的设定。

另外须要注意的是,sleep()和yield()方法,一样都是当前线程会交出处理器资源,而它们不一样的是,sleep()交出来的时间片其余线程均可以去竞争,也就是说都有机会得到当前线程让出的时间片。而yield()方法只容许与当前线程具备相同优先级的线程可以得到释放出来的CPU时间片。

4.守护线程Daemon

守护线程是一种特殊的线程,就和它的名字同样,它是系统的守护者,在后台默默地守护一些系统服务,好比垃圾回收线程,JIT线程就能够理解守护线程。与之对应的就是用户线程,用户线程就能够认为是系统的工做线程,它会完成整个系统的业务操做。用户线程彻底结束后就意味着整个系统的业务任务所有结束了,所以系统就没有对象须要守护的了,守护线程天然而然就会退。当一个Java应用,只有守护线程的时候,虚拟机就会天然退出。下面以一个简单的例子来表述Daemon线程的使用。

public class DaemonDemo {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println("i am alive");
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println("finally block");
                    }
                }
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
        //确保main线程结束前能给daemonThread可以分到时间片
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 输出结果为:

    i am alive finally block i am alive

    上面的例子中daemodThread run方法中是一个while死循环,会一直打印,可是当main线程结束后daemonThread就会退出因此不会出现死循环的状况。main线程先睡眠800ms保证daemonThread可以拥有一次时间片的机会,也就是说能够正常执行一次打印“i am alive”操做和一次finally块中"finally block"操做。紧接着main 线程结束后,daemonThread退出,这个时候只打印了"i am alive"并无打印finnal块中的。所以,这里须要注意的是守护线程在退出的时候并不会执行finnaly块中的代码,因此将释放资源等操做不要放在finnaly块中执行,这种操做是不安全的

    线程能够经过setDaemon(true)的方法将线程设置为守护线程。而且须要注意的是设置守护线程要先于start()方法,不然会报

    Exception in thread "main" java.lang.IllegalThreadStateException at java.lang.Thread.setDaemon(Thread.java:1365) at learn.DaemonDemo.main(DaemonDemo.java:19)

    这样的异常,可是该线程仍是会执行,只不过会当作正常的用户线程执行。

    • 当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态,而使用java.util.concurrent.locks下lock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,由于lock会调用LockSupport的方法。

    1. sleep()方法是Thread的静态方法,而wait是Object实例方法

    2. wait()方法必需要在同步方法或者同步块中调用,也就是必须已经得到对象锁。而sleep()方法没有这个限制能够在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;

    3. sleep()方法在休眠时间达到后若是再次得到CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,而且再次得到CPU时间片才会继续执行。

相关文章
相关标签/搜索