关于java:线程的状态转换以及基本操作2

31次阅读

共计 7490 个字符,预计需要花费 19 分钟才能阅读完成。

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

1. 新建线程

一个 java 程序从 main()办法开始执行,而后依照既定的代码逻辑执行,看似没有其余线程参加,但实际上 java 程序天生就是一个多线程程序,蕴含了:
(1)散发解决发送给给 JVM 信号的线程;
(2)调用对象的 finalize 办法的线程;
(3)革除 Reference 的线程;
(4)main 线程,用户程序的入口;

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

  1. 通过继承 Thread 类,重写 run 办法;
  2. 通过实现 runable 接口;
  3. 通过实现 callable 接口这三种形式,上面看具体 demo。

     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 并发编程的艺术》一书中,线程是会在不同的状态间进行转换的,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 状态,状态转换能够说是线程的生命周期。另外须要留神的是:

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

用一个表格将下面六种状态进行一个总结演绎。

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()

两者次要的区别:

  1. sleep()办法是 Thread 的静态方法,而 wait 是 Object 实例办法
  2. wait()办法必须要在同步办法或者同步块中调用,也就是必须曾经取得对象锁。而 sleep()办法没有这个限度能够在任何中央种应用。另外,wait()办法会开释占有的对象锁,使得该线程进入期待池中,期待下一次获取资源。而 sleep()办法只是会让出 CPU 并不会开释掉对象锁;
  3. sleep()办法在休眠工夫达到后如果再次取得 CPU 工夫片就会继续执行,而 wait()办法必须期待 Object.notift/Object.notifyAll 告诉后,才会来到期待池,并且再次取得 CPU 工夫片才会继续执行。

3.4 yield

public static native void yield(); 这是一个静态方法,一旦执行,它会是以后线程让出 CPU,然而,须要留神的是,让出的 CPU 并不是代表以后线程不再运行了,如果在下一次竞争中,又取得了 CPU 工夫片以后线程仍然会持续运行。另外,让出的工夫片只会调配 给以后线程雷同优先级 的线程。什么是线程优先级了?上面就来具体聊一聊。

古代操作系统根本采纳时候的模式调度运行的线程,操作系统会分出一个个工夫片,线程会调配到若干工夫片,以后工夫片用完后就会产生线程调度,并期待这下次调配。线程调配到的工夫多少也就决定了线程应用处理器资源的多少,而线程优先级就是决定线程须要或多或少调配一些处理器资源的线程属性。

在 Java 程序中,通过一个 整型成员变量 Priority来管制优先级,优先级的范畴从 1~10. 在构建线程的时候能够通过 **setPriority(int)** 办法进行设置,默认优先级为 5,优先级高的线程相较于优先级低的线程优先取得处理器工夫片。须要留神的是在不同 JVM 以及操作系统上,线程布局存在差别,有些操作系统甚至会疏忽线程优先级的设定。

sleep() VS yield()

另外须要留神的是,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)

这样的异样,然而该线程还是会执行,只不过会当做失常的用户线程执行。

正文完
 0