从我开始写博客到如今,已经写了很多关于并发编程的了,差很少还有一半内容整个并发编程系列就结束了,而今天这篇博客是比较简单的,只是介绍下并发编程的基础知识( = =!其实,对于大神来讲,前面全部博客都是基础)。原本我不太想写这篇博客,由于这篇博客的不少内容都是以记忆为主,并且网上也有大把大把的博客,都写的至关不错,可是我最终决定仍是要写一写,由于没有这篇博客,并发编程系列就不能算是一个完整的系列。java
说到线程,不得不说到进程,由于线程是没法单独存在的,它只是进程中的一部分。
进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。线程则是进程的一个执行路径,一个进程中至少有一个线程。操做系统在分配系统资源的时候,会把CPU资源分配给线程,由于真正执行工做,须要占用CPU运行的是线程,因此也能够说线程是CPU分配的基本单位。
在Java中,咱们启动一个main函数,就启动了一个JVM的进程,而main函数所在的线程被称为“主线程”。
每一个线程都有一个叫“程序计数器”的私有的内存区域,用来记录当前线程下一个要执行的指令地址,为何要把程序计数器设计成私有的呢?由于线程是占用CPU的基本单位,而CPU通常是使用时间片轮转的方式来让线程占有的,因此当某个线程的时间片用完后,要让出CPU,等下一次得到时间片了,再继续执行。那么线程怎么知道以前的程序执行到哪里了呢?就是靠程序计数器。另外须要注意的是,若是执行的是native方法,那么程序计数器记录的是undefined地址。面试
线程有三种建立方式,分别是编程
class MyThread extends Thread { @Override public void run() { System.out.println("run"); } } public class MyTest { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); } }
class MyRannable implements Runnable { @Override public void run() { System.out.println("run"); } } public class MyTest { public static void main(String[] args) { MyRannable myRannable = new MyRannable(); new Thread(myRannable).start(); } }
class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "MyCallable"; } } public class MyTest { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<String>futureTask=new FutureTask<>(new MyCallable()); new Thread(futureTask).start(); System.out.println(futureTask.get()); } }
相信前面两种方式不用多说,你们都懂。咱们如今来看看第三种方式,首先定义了MyCallable类,并实现了Callable接口的call方法。在main方法中建立了FutureTask对象,传入了MyCallable对象,而后用Thread包装了FutureTask对象,随后启动,最后用futureTask提供的get方法获取结果,获取结果这一步是阻塞的。并发
在面试中,常常会问以下的问题:ide
关于死亡和阻塞状态,其实说的不太完整,由于除了线程运行结束后这种“天然死亡”,还有一个状况,就是被stop了,可是Java已经不推荐使用stop等操做了,因此就忘记吧,阻塞也是一样的道理,也不推荐使用suspend方法了,也忘记它把。函数
在Java中,每一个对象都继承了Object类,而在Object类中提供了通知和等待的操做,因此每一个对象都有这样的操做,既然是线程的通知与等待,为何要把它定义在Object类中?由于Java提供的锁,锁的是对象,而不是方法或是线程,因此天然要定义在Object类中。网站
当一个线程调用共享变量的wait方法后,该线程会被阻塞挂起,直到发生如下的两个事情才返回:this
class MyRunnable implements Runnable { Object object=new Object(); @Override public void run() { try { synchronized (object){ object.wait(); System.out.println("run"); } } catch (InterruptedException e) { System.out.println("被中断了"); e.printStackTrace(); } } } public class MyTest { public static void main(String[] args) throws ExecutionException, InterruptedException { Thread thread = new Thread(new MyRunnable()); thread.start(); thread.interrupt(); } }
运行结果:操作系统
被中断了 java.lang.InterruptedException at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:502) at com.codebear.MyRunnable.run(MyTest.java:13) at java.lang.Thread.run(Thread.java:748)
首先新建了一个子线程,子线程内部获取了object的监视器锁,随后调用object的wait方法阻塞当前线程,主线程调用interrupt方法中断子线程,子线程被返回,而且产生了异常。线程
这也就是为何咱们在调用共享变量的wait方法的时候,Java“死皮赖脸”的要咱们对异常进行处理的缘由:
调用wait方法后,还会释放对共享变量的监视器锁,让其余线程能够进入临界区:
class MyRunnable implements Runnable { @Override public void run() { try { synchronized (MyRunnable.class) { System.out.println("我是" + Thread.currentThread().getName() + ",我进入了临界区"); MyRunnable.class.wait(); Thread.sleep(Integer.MAX_VALUE); } } catch (Exception e) { e.printStackTrace(); } System.out.println("run"); } } public class MyTest { public static void main(String[] args) throws ExecutionException, InterruptedException { MyRunnable myRunnable = new MyRunnable(); Thread thread1 = new Thread(myRunnable); thread1.start(); Thread thread2 = new Thread(myRunnable); thread2.start(); } }
运行结果:
我是Thread-1,我进入了临界区 我是Thread-0,我进入了临界区
能够很清楚的看到,两个线程都进入了临界区。
线程A获取了共享对象的监视器锁后,进入了临界区,线程B只能等待,线程A调用了共享对象的wait方法后,释放了共享对象的监视器锁,让线程B也能够得到共享变量的监视器锁,而且进入临界区。
在调用共享变量的wait方法前,必须先对该共享变量进行synchronized操做,不然会抛出IllegalMonitorStateException异常:
class MyRunnable implements Runnable { Object object = new Object(); @Override public void run() { try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("run"); } } public class MyTest { public static void main(String[] args) throws ExecutionException, InterruptedException { Thread thread = new Thread(new MyRunnable()); thread.start(); thread.interrupt(); } }
运行结果:
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:502) at com.codebear.MyRunnable.run(MyTest.java:12) at java.lang.Thread.run(Thread.java:748)
另外须要注意的是,一个线程虽然从阻塞挂起的状态到就绪的状态,可是可能其余线程并无唤醒它,这就是虚假唤醒,虽然虚假唤醒在实践中不多发生,可是防患于未然,比较严谨的作法就是在wait方法外面,包裹一个while循环,while循环的条件就是检测是否知足了被唤醒的条件,这样即便虚假唤醒发生了,该线程被返回了,因为被while包裹了,发现并无知足被唤醒的条件,又会被再次wait。
以下所示:
while(是否知足了被唤醒的条件) { object.wait(); }
wait方法是将当前线程阻塞挂起,那么一定有一个方法是唤醒此线程的,就像沉睡的白雪公主也在等待王子的到来,将她唤醒同样。
被唤醒的线程不能立刻从wait方法处返回,而且继续执行,由于还须要再次获取共享变量的监视器锁(由于调用wait方法后,已经释放了监视器,因此这里须要再次获取)。
若是有多个线程都调用了共享变量的wait方法而被阻塞挂起,那么调用notify方法后,只会随机唤醒其中一个线程。
还有一点尤为须要注意:当调用共享变量的notify方法后,并无释放共享变量的监视器锁,只有退出临界区或者调用wait方法后,才会释放共享变量的监视器锁,咱们能够作一个实验:
class CodeBearRunnable implements Runnable { private Object object = new Object(); @Override public void run() { synchronized (object) { object.notify(); System.out.println("我是" + Thread.currentThread().getName() + LocalDateTime.now()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class NotifyTest { public static void main(String[] args) { CodeBearRunnable codeBearRunnable = new CodeBearRunnable(); new Thread(codeBearRunnable).start(); new Thread(codeBearRunnable).start(); } }
运行结果:
我是Thread-02019-04-28T18:12:19.195 我是Thread-12019-04-28T18:12:22.196
咱们来分析下代码:当线程A获取了共享变量的监视器锁,进入了临界区,调用共享变量的notify方法,打印出当前的时间,随后sleep当前线程3秒。若是notify方法会释放锁,那么线程B打印出来时间和线程A打印出来的时间应该相差不大,可是能够很清楚的看到,打印出来的时间相差了3秒,说明了线程A调用共享变量的notify方法后,并无释放共享变量的锁,只有退出了临界区,才释放了共享变量的锁。
若是有多个线程都调用了共享变量的wait方法而被阻塞挂起,那么调用notifyAll方法后,全部线程都会被唤醒。
最后,咱们用一个常见的面试题来熟悉下wait/notify的应用:两个线程交替打印奇偶数:
class MyRunnable implements Runnable { static private int i = 0; @Override public void run() { try { while (i < 100) { synchronized (MyRunnable.class) { MyRunnable.class.notify(); MyRunnable.class.wait(); System.out.println("我是" + Thread.currentThread() + ":" + i++); } } } catch (InterruptedException e) { e.printStackTrace(); } } } public class MyTest { public static void main(String[] args) throws ExecutionException, InterruptedException { Thread thread1 = new Thread(new MyRunnable()); thread1.start(); Thread thread2 = new Thread(new MyRunnable()); thread2.start(); } }
运行结果:
在开发中,咱们常常会遇到这样的需求:等待某些事情都完成后,才能够继续执行。好比旅游网站查询某个产品的航班,航班能够分为去程和返程,咱们能够开两个线程同时查询去程和返程的航班,等他们的结果都返回后,再执行其余操做。
class GoRunnable implements Runnable { @Override public void run() { System.out.println("查询去程航班"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } } class ReturnRunnable implements Runnable { @Override public void run() { System.out.println("查询返程航班"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } public class MyTest { public static void main(String[] args) throws ExecutionException, InterruptedException { Thread thread1 = new Thread(new GoRunnable()); thread1.start(); Thread thread2 = new Thread(new ReturnRunnable()); thread2.start(); System.out.println("开始查询航班,如今的时间是"+ LocalDateTime.now()); thread1.join(); thread2.join(); System.out.println("航班查询完毕,如今的时间是"+ LocalDateTime.now()); } }
运行结果:
查询去程航班 查询返程航班 开始查询航班,如今的时间是2019-04-28T21:18:05.719 航班查询完毕,如今的时间是2019-04-28T21:18:10.654
若是是同步查询,那么查询航班的耗时应该在(5+3)秒左右,如今利用线程+join方法,两个线程同时执行,耗时5秒左右(取决于慢的那个),在实际项目中,能够提高用户的体验,大幅提升查询的效率。
这里仅仅是演示join的功能,若是在实际项目中遇到这样的场景应该不会用join这么“粗糙”的方法。
让咱们再来看看当join遇到interrupt方法会擦出怎样的火花:
class GoRunnable implements Runnable { @Override public void run() { System.out.println("查询去程航班"); for (; ; ) { } } } public class MyTest { public static void main(String[] args) throws ExecutionException, InterruptedException { Thread thread1 = new Thread(new GoRunnable()); thread1.start(); Thread.currentThread().interrupt(); try { thread1.join(); } catch (InterruptedException e) { System.out.println("主线程" + e.toString()); } } }
运行结果:
主线程java.lang.InterruptedException 查询去程航班
子线程内部是一个死循环,执行子线程后,中断主线程,在主线程中的thread1.join处抛出了异常。可是须要注意的是,由于中断的是主线程,因此是在主线程中抛出异常,这里我用try包住thread1.join()只是为了更好的展示错误,其实这里并不强制要求对异常进行捕获。
原本想用一篇博客就结束并发编程基础的,可是写起来才发现想多了,一是想把每一个知识点都说的清楚一点,并给出各类例子来帮助你们更好的理解,二是并发编程基础的知识点确实挺多的,因此仍是分两篇博客来吧。