伟大的理想只有通过忘个人斗争和牺牲才能胜利实现。java
本篇为【Dali王的技术博客】Java并发编程系列第二篇,讲讲有关线程的那些事儿。主要内容是以下这些:面试
进程表明了运行中的程序,一个运行的Java程序就是一个进程。在Java中,当咱们启动main函数时就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,称为主线程。
进程和线程的关系以下图所示:
编程
由上图能够看出来,一个进程中有多个线程,多个线程共享进程的堆的方法区资源,可是每一个线程有本身的程序计数器和栈区域。json
Java中有三种线程建立方式,分别为:继承Thread类并重写run方法,实现Runnable接口的run方法,使用FutureTask方式。
先看继承Thread方式的实现,代码示例以下:并发
public class ThreadDemo { public static class DemoThread extends Thread { @Override public void run() { System.out.println("this is a child thread."); } } public static void main(String[] args) { System.out.println("this is main thread.") DemoThread thread = new DemoThread(); thread.start(); } }
上面代码中DemoThread类继承了Thread类,并重写了run方法。在main函数里建立了一个DemoThread的实例,而后调用其start方法启动了线程。ide
tips:调用start方法后线程并无立刻执行,而是处于就绪状态,也就是这个线程已经获取了除CPU资源外的其余资源,等待获取CPU资源后才会真正处于运行状态。
使用继承方式,好处在于经过this就能够获取当前线程,缺点在于Java不支持多继承,若是继承了Thread类,那么就不能再继承其余类。并且任务与代码耦合严重,一个线程类只能执行一个任务,使用Runnable则没有这个限制。
来看实现Runnable接口的run方法的方式,代码示例以下:函数
public class RunnableDemo { public static class DemoRunnable implements Runnable { @Override public void run() { System.out.println("this is a child thread."); } } public static void main(String[] args) { System.out.println("this is main thread."); DemoRunnable runnable = new DemoRunnable(); new Thread(runnable).start(); new Thread(runnable).start(); } }
上面代码两个线程共用一个Runnable逻辑,若是须要,能够给RunnableTask添加参数进行任务区分。在Java8中,可使用Lambda表达式对上述代码进行简化:学习
public static void main(String[] args) { System.out.println("this is main thread."); Thread t = new Thread(() -> System.out.println("this is child thread")); t.start(); }
上面两种方式都有一个缺点,就是任务没有返回值,下面看第三种,使用FutureTask的方式。代码示例以下:this
public class CallableDemo implements Callable<JsonObject> { @Override public JsonObject call() throws Exception { return new JsonObject(); } public static void main(String[] args) { System.out.println("this is main thread."); FutureTask<JsonObject> futureTask = new FutureTask<>(new CallableDemo()); // 1. 可复用的FutureTask new Thread(futureTask).start(); try { JsonObject result = futureTask.get(); System.out.println(result.toString()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } // 2. 一次性的FutureTask FutureTask<JsonObject> innerFutureTask = new FutureTask<>(() -> { JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("name", "Dali"); return jsonObject; }); new Thread(innerFutureTask).start(); try { JsonObject innerResult = innerFutureTask.get(); System.out.println(innerResult.toString()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } }
如上代码,CallableDemo实现了Callable接口的call方法,在main函数中使用CallableDemo的实例建立了一个FutureTask,而后使用建立的FutureTask对象做为任务建立了一个线程并启动它,最后经过FutureTask等待任务执行完毕并返回结果。
一样的,上面的操做过程适合于须要复用的任务,若是对于一次性的任务,大能够经过Lambda来简化代码,如注释2处。线程
在项目中常常会遇到一个场景,就是须要等待某几件事情完成后才能继续往下执行。Thread类中有一个join方法就能够用来处理这种场景。直接上代码示例:
public static void main(String[] args) throws InterruptedException { System.out.println("main thread starts"); Thread t1 = new Thread(() -> System.out.println("this is thread 1")); Thread t2 = new Thread(() -> System.out.println("this is thread 2")); t1.start(); t2.start(); System.out.println("main thread waits child threads to be over"); t1.join(); t2.join(); System.out.println("child threads are over"); }
上面代码在主线程里启动了两个线程,而后分别调用了它们的join方法,主线程会在调用t1.join()后被阻塞,等待其执行完毕后返回;而后主线程调用t2.join()后再次被阻塞,等待t2执行完毕后返回。上面代码的执行结果以下:
main thread starts main thread waits child threads to be over this is thread 1 this is thread 2 child threads are over
须要注意的是,线程1调用线程2的join方法后会被阻塞,当其余线程调用了线程1的interrupt方法中断了线程1时,线程1会抛出一个InterruptedException异常而返回。
Thread类中有一个static的sleep方法,当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,可是该线程所拥有的监视器资源,好比锁仍是不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,而后等待CPU的调度执行。
tips:面试当中wait和sleep常常会被用来比较,须要多加体会两者的区别。
调用某个对象的wait()方法,至关于让当前线程交出此对象的monitor,而后进入等待状态,等待后续再次得到此对象的锁;notify()方法可以唤醒一个正在等待该对象的monitor的线程,当有多个线程都在等待该对象的monitor的话,则只能唤醒其中一个线程,具体唤醒哪一个线程则不得而知。
调用某个对象的wait()方法和notify()方法,当前线程必须拥有这个对象的monitor,所以调用wait()方法和notify()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)。
看一个线程睡眠的代码示例:
private static final Lock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { // 获取独占锁 lock.lock(); System.out.println("thread1 get to sleep"); try { Thread.sleep(1000); System.out.println("thread1 is awake"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); Thread t2 = new Thread(() -> { // 获取独占锁 lock.lock(); System.out.println("thread2 get to sleep"); try { Thread.sleep(1000); System.out.println("thread2 is awake"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); t1.start(); t2.start(); }
上面的代码建立了一个独占锁,而后建立了两个线程,每一个线程在内部先获取锁,而后睡眠,睡眠结束后会释放锁。执行结果以下:
thread1 get to sleep thread1 is awake thread2 get to sleep thread2 is awake
从执行结果来看,线程1先获取锁,而后睡眠,再被唤醒,以后才轮到线程2获取到锁,也即在线程1sleep期间,线程1并无释放锁。
须要注意的是,若是子线程在睡眠期间,主线程中断了它,子线程就会在调用sleep方法处抛出了InterruptedException异常。
Thread类中有一个static的yield方法,当一个线程调用yield方法时,实际就是暗示线程调度器当前线程请求让出本身的CPU使用,若是该线程还有没用完的时间片也会放弃,这意味着线程调度器能够进行下一轮的线程调度了。
当一个线程调用yield方法时,当前线程会让出CPU使用权,而后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,固然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。
请看代码示例:
public static void main(String[] args) { Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) { if (i == 8) { System.out.println("current thread: " + Thread.currentThread() + " yield cpu"); } Thread.yield(); // 2 } System.out.println("current thread: " + Thread.currentThread() + " is over"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10; i++) { if (i == 8) { System.out.println("current thread: " + Thread.currentThread() + " yield cpu"); } Thread.yield(); // 1 } System.out.println("current thread: " + Thread.currentThread() + " is over"); }); t1.start(); t2.start(); }
在如上的代码中,两个线程的功能同样,运行屡次,同一线程的两行输出是顺序的,可是总体顺序是不肯定的,取决于线程调度器的调度状况。
当把上面代码中1和2处代码注释掉,会发现结果只有一个,以下:
current thread: Thread[Thread-1,5,main] yield cpu current thread: Thread[Thread-0,5,main] yield cpu current thread: Thread[Thread-1,5,main] is over current thread: Thread[Thread-0,5,main] is over
从结果可知,Thread.yiled方法生效使得两个线程分别在执行过程当中放弃CPU,而后在调度另外一个线程,这里的两个线程有点互相谦让的感受,最终是因为只有两个线程,最终仍是执行完了两个任务。
tips:sleep和yield的区别:
当线程调用sleep方法时,调用线程会阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出本身剩余的时间片,并无被阻塞挂起,而是出于就绪状态,线程调度器下一次调度时就可能调度到当前线程执行。
Java中的线程中断是一种线程间的协做模式。每一个线程对象里都有一个boolean类型的标识(经过isInterrupted()方法返回),表明着是否有中断请求(interrupt()方法)。例如,当线程t1想中断线程t2,只须要在线程t1中将线程t2对象的中断标识置为true,而后线程2能够选择在合适的时候处理该中断请求,甚至能够不理会该请求,就像这个线程没有被中断同样。
在上面章节中也讲到了线程中断的一些内容,此处就再也不用代码来展开了。
继续附上Java编程的系统学习大纲以供参考:
Java并发编程.png
【参考资料】
本文由微型公众号【Dali王的技术博客】原创,扫码关注获取更多原创技术文章。