今天来讲一下Java和Spring开发中需要重点关注的并发(concurrency)。本文选自BitTiger软件工程师项目实战精品课课程内容。
到并发,可以分为线程(thread)和进程(process)两种模式,而当我们在谈论Java的并发的时候,一般都是在讲线程的并发。
初见线程
Java中主要通过主线程来创建其他线程,每一个线程都是和Thread类的实例相关联的。有两种基本的管理Thread对象的方式:
1.“亲力亲为”:
在应用程序需要发起异步任务的后,直接控制线程的创建并对其进行管理。
2.“甩手掌柜”:
把应用程序的任务交给执行器(executor),这样可以将对线程的管理从程序中抽离出来。
创建线程
从无到有,我们当然需要创建一个线程,Java中提供了两种方法:
1.实现一个Runnable接口(Implement Runnable Interface)
2.继承一个线程类(Extend Thread Class)
一般情况下,推荐使用第一种方式,因为java不允许多继承,因此实现了Runnable接口的类可以再继承其他类。
线程创建完了以后,就需要进行管理。正如前文所说,有两种管理方式,第一种“亲力亲为”的方式需要在自己写代码对线程从创建到销毁的全部处理,当然也是可行的,但是太麻烦了。所以一般倾向于采用“甩手掌柜”的方式,让执行器去管理线程的生命周期。
要想启动线程需要两步走。首先,重写 (override)Runnable接口的run方法,实现需要实现的功能,其次调用start方法来真正启动线程。如下图所示:
1. public classHelloRunnable implements Runnable {  
2. publicvoid run() {  
3.         System.out.println("Hello from a thread!");  
4.     }  
5. publicstaticvoid main(String args[]) {  
6.         (new Thread(newHelloRunnable())).start();  
7.     }  
8. }  
暂停线程
有时候模拟位置变更的线程,不需要每时每刻都在变更位置,而只是需要有一定时间间隔后再来更新位置,那就要用到sleep方法来使线程挂起一个指定的时间段。
我们来看一个例子:
1. publicclass SleepMessages {  
2. publicstaticvoid main(String args[])  
3. throws InterruptedException {  
4.         String importantInfo[] = {  
5. "Mares eat oats",  
6. "Does eat oats",  
7. "Little lambs eat ivy",  
8. "A kid will eat ivy too"
9.         };  
10. for (int i = 0;  
11.              i < importantInfo.length;  
12.              i++) {  
13. //Pause for 4 seconds
14.             Thread.sleep(4000);  
15. //Print a message
16.             System.out.println(importantInfo[i]);  
17.         }  
18.     }  
19. }  
中断处理
细心的同学可能已经发现了,上面的代码里面throws后面跟着一个异常类型InterruptedException。试想一下,一个线程本来很舒服地睡在那里(处于挂起状态),突然被暴力丢了出去(中断),心中一定会千万只草泥马奔腾而过。这个抛出的InterruptedException异常这个线程最后的呐喊,作为开发者的我们就需要处理。这个例子里面就简单的抛出异常,没有特殊处理。
深入Java源码,我们会发现在这个sleep方法的实现的时候我们看到用native来修饰,这也就意味着执行sleep方法不仅仅会挂起当前的Java线程,也会挂起对应的操作系统线程。
同步的问题
多线程不可避免的会遇到同步的问题,这也是许多语言选择单线程的原因。虽然单线程相对来说同步的问题,但对于很多CPU密集型操作性能不佳。
1. class Counter {
2.   private int c = 0;
3.   publicvoid increment() {  
4.         c++;  
5.     }  
6. publicvoid decrement() {  
7.         c--;  
8.     }  
9.    publicvoid value() {  
10. return c;  
11.     }  
12. }  
上面是一个经典的例子,基本上只要讲到Java多线程都会举这个例子。在多线程编程过程中必须考虑到同步的问题。由于多线程中,线程访问的顺序是不定的,而自增和自减都不是线程安全的,如果不做同步,就可能导致不一致的情况。
为了使操作结果同步,可以给方法加上synchronized修饰,这样就可以让一个线程在操作数据的时候,加上一把锁,直到处理完才释放。可以将代码修改如下:
1. class Counter {
2.   private int c = 0;
3.   public synchronizedvoid increment() {  
4.         c++;  
5.     }  
6. public synchronizedvoid decrement() {  
7.         c--;  
8.     }  
9.    public synchronizedvoid value() {  
10. return c;  
11.     }  
12. }  
线程安全其他方法
通常情况下,在Java里面,自增和自减都不是线程安全的,这里面有三个独立的操作:获得变量当前值,为该值+1/-1,然后写回新的值。在没有额外资源可以利用的情况下,只能使用加锁才能保证读-改-写这三个操作保证是原子操作(Atomic Action)的。如果不想手动处理同步问题,可以使用自带原子属性的原子变量(Atomic Variable),比如AtomicInteger和AtomicBoolean。当你需要自增的时候,就调用AtomicInteger的incrementAndGet()方法就可以实现线程安全的自增了。
也可以通过定义不可变对象(Immutable Object)来实现线程安全。首先,不提供“setter”方法,其次所有的Field都定义为final和private,然后,不让子类重写方法,把类定义为final。总之,让你的对象状态就是一旦被初始化,就不能被改变。
Execute方法
如果一个线程很早就创建了,但一直处于闲置状态,就需要使用Java提供的execute()方法来真正让线程执行起来。
上图定义了一个ExecutorService,执行submit后,如果线程没有挂起,资源也可用,就会执行起来了,否则就暂停。
Spring中的使用
首先是Spring提供的Task Scheduler任务调度器,来运行未来的定时任务,允许按照时间间隔,日期时间安排任务来执行任务。
还有一个是Spring中的AsyncTaskExecutor。假设需要启动5个线程去执行,并且想要这5个线程异步执行,就可以在Spring中实现AsyncTaskExecutor这个接口,调用submit方法,把线程对象传进来,返回Future。Future意味着这个任务当前还没有执行,是一个未来的状态。如果现在线程池的资源满足task需求的话,就会立即被执行,可以使用Future的get方法去获取返回值,如果没有资源来执行或者还处于执行状态,Future会返回null。
刚刚提到的Future,可以调用get方法获取返回值,如果你的task是一个长时间运行的作业,就要小心使用了,因为调用get方法后他会等到你返回结果了以后再结束。
那么接下来,我们应该如何让多个线程之间高效协作呢?又应该如何灵活使用volatile?
这些知识点我们都会明晚正式开课的BitTiger软件工程师项目实战精品课中为大家深入讲解,我们还将免费开放第一周正式课程(共四节课八小时),详细报名方式参见下图,你也可以点击阅读原文了解课程,抓紧最后机会吧!
继续阅读
阅读原文