多线程不只是Java后端开发面试中很是热门的一个问题,也是各类高级工具、框架与分布式的核心基石。可是这个领域相关的知识点涉及到了线程调度、线程同步,甚至在一些关键点上还涉及到了硬件原语、操做系统等更底层的知识。想要背背面试题很容易,可是若是面试官一追问就很容易露馅,更不用说真正想搞明白这个问题并应用在实际的代码实践中了。面试
不用担忧!在接下来的一系列文章中将会由浅入深地贯穿这个问题的方方面面,虽然不如一些面试大全来得直接和速成。可是真正搞明白多线程编程不只可以一劳永逸地解决面试中的尴尬,并且还能打开通往底层知识的大门,不止是搞明白一个孤立的知识点,更是一个将之前曾经了解过的理论知识融会贯通连点成面的好机会。编程
虽然阅读本文不须要事先了解并发相关的概念,可是若是已经掌握了一些大概的概念将会大大下降理解的难度。有兴趣的读者能够参考本系列的第一篇文章来了解一下并发相关的基本概念——当咱们在说“并发、多线程”,说的是什么?。后端
这一系列文章将会包含10篇文章,本文是其中的第二篇,相信只要有耐心看完全部内容必定能轻松地玩转多线程编程,不止是游刃有余地经过面试,更是能熟练掌握多线程编程的实践技巧与并发实践这一Java高级工具与框架的共同核心。bash
前五篇包含如下内容,将会在近期发布:服务器
多线程程序和通常的单线程程序相比引入了同步、线程调度、内存可见性等一大堆复杂的问题,大大提升了开发者开发程序的难度,那么为何如今多线程在各个邻域中还被如此趋之若鹜呢?微信
在我大学的时候宿舍边上有一家盖浇饭,也提供炒菜。老板很是地耿直,非要按点菜的顺序一桌一桌地烧,若是前一桌的菜没上完后一桌一个菜都别想吃到。结果就是天天这家店里都是怨声载道,顾客们经常等了半个小时也等不来一个菜填填肚子。你问我为何还会有人去吃,受这罪,那确定是由于好吃啊😂。网络
不过仔细想一想,好像通常的店里好像并无这种状况,由于大部分饭店都是混合着上的,就算前一桌没上无缺歹会给几个菜垫垫肚子。这在程序中也是同样,不一样的程序之间能够交替运行,不至于在咱们的电脑上打开了开发工具就不能接收微信消息。数据结构
这就是多线程的一个应用场景:经过任务的交替执行使一台计算机上能够同时运行多个程序。多线程
仍是在小饭馆里,一个服务员在给一桌点完菜以后确定不会等到这桌菜上完了才去给另一桌点菜。通常都是点完菜就把订单给了厨房,以后就继续给下一桌点菜了。在这里,咱们能够把服务员想象成咱们的计算机,把厨房想象成远程的服务器。那么在咱们的电脑下载音乐的时候同时继续播放音乐,这就能更高效地利用咱们的电脑了。并发
这种场景能够描述为:在等待网络请求、磁盘I/O等耗时操做完成时,能够用多线程来让CPU继续运转,以达到有效利用CPU资源的目的。
而后咱们来到了厨房,居然看到了一个大神,能一我的烧2个灶台。若是这个厨师大神是一个多核处理器,那么两个灶台就是两个线程,若是只给一个灶台,那就浪费他的才能了,这绝对是一种损失。
这就是多线程应用的最后一种场景:将计算量比较大的任务拆分到两个CPU上执行能够减小执行完成的时间,而多线程就是拆分和执行任务的载体,没有多线程就没办法把任务放到多个CPU上执行了。
多线程就是不少线程的意思,嗯,是否是很简单?
线程是操做系统中的一个执行单元,一样的执行单元还有进程,全部的代码都要在进程/线程中执行。线程是从属于进程的,一个进程能够包含多个线程。进程和线程之间还有一个区别就是,每一个进程有本身独立的内存空间,互相直接不能直接访问;可是同一个进程中的多个线程都共享进程的内存空间,因此能够直接访问同一块内存,其中最典型的就是Java中的堆。
了解了这么多理论概念,终于到了实际上手写写代码的时候了。
Java中的线程使用Thread
类表示,Thread类的构造器能够传入一个实现了Runnable
接口的对象,这个Runnable对象中的void run()
方法就表明了线程中会执行的任务。例如若是要建立一个对整型变量进行自增的Runnable
任务就能够写为:
// 静态变量,用于自增
private static int count = 0;
// 建立Runnable对象(匿名内部类对象)
Runnable task = new Runnable() {
public void run() {
for (int i = 0; i < 1e6; ++i) {
count += 1;
}
}
复制代码
有了Runnable
对象表明的待执行任务以后,咱们就能够建立两个线程来运行它了。
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
复制代码
可是这时候只是建立了线程对象,实际上线程尚未被执行,想要执行线程还须要调用线程对象的start()
方法。
t1.start();
t2.start();
复制代码
这时候线程就能开始执行了,完整的代码以下所示:
public class SimpleThread {
private static int count = 0;
public static void main(String[] args) throws Exception {
Runnable task = new Runnable() {
public void run() {
for (int i = 0; i < 1000000; ++i) {
count = count + 1;
}
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
// 等待t1和t2执行完成
// t1.join();
// t2.join();
System.out.println("count = " + count);
}
}
复制代码
最后输出的结果是8251,你执行的时候应该会与这个值不一样,可是同样会远远小于一百万。这好像离咱们指望的结果有点远,毕竟每一个任务都累加了至少一百万次。
这是由于咱们在main方法中建立线程并运行以后并无等待线程完成,使用t1.join()
可使当前线程等待t1线程执行完成后再继续执行。让咱们去掉两个join方法调用前面的双斜杠试一试效果。
在个人电脑上执行的结果是1753490,你执行的结果会有不一样,可是一样达不到咱们所指望的两百万。具体的缘由能够从下面的执行顺序图中找到答案。
t1 | t2 |
---|---|
获取count值为0 | |
获取count值为0 | |
计算0+1的结果为2 | |
将2保存到count | |
计算0+1的结果为2 | |
将2保存到count |
能够看到,t1和t2两个线程之间的并发运行会致使互相本身的结果覆盖,最后的结果就会在一百万与两百万之间,可是离两百万会有比较大的距离。这样的多线程共同读取并修改同一个共享数据的代码区块就被称为临界区,临界区同一时刻只容许一个线程进入,若是同时有多个线程进入就会致使数据竞争问题。若是有读者对这里提到的临界区和数据竞争概念还不清楚的,能够参考本系列的第一篇介绍并发基本概念的文章——当咱们在说“并发、多线程”,说的是什么?。
在Java 5以前,咱们最经常使用的线程同步方式就是关键字synchronized
,这个关键字既能够标在方法上,也能够做为独立的块结构使用。方法声明形式的synchronized关键字能够在方法定义时如此使用:public synchronized static void methodName()
。由于咱们的累加操做在继承自Runnable接口的run()
方法中,因此没办法改变方法的声明,那么就可使用以下的块结构形式使用synchronized
关键字:
Runnable task = new Runnable() {
public void run() {
for (int i = 0; i < 1000000; ++i) {
synchronized (SimpleThread.class) {
count += 1;
}
}
}
};
复制代码
synchronized是一种对象锁,采用的锁和具体的对象有关,若是是同一个对象就是同一个锁;若是是不一样的对象则是不一样的锁。同一时刻只能有一个线程持有锁,也就意味着其余想要获取同一个锁的线程会被阻塞,直到持有锁的线程释放这个锁为止。这里能够把对象锁对应的对象看作是锁的名称,实现同步的并非对象自己,而是与对象对应的对象锁。
在块结构的synchronized关键字后的括号中的就是对象锁所对应的对象,在上面的代码中,咱们使用了SimpleThread类的类对象对应的锁做为同步工具。而若是synchronized关键字被用在方法声明中,那么若是是实例方法(非static方法)对应的对象就是this指针所指向的对象,若是是static方法,那么对应的对象就是所处类的类对象。
此次咱们能够看到输出的结果每次都是稳定的两百万了,咱们成功完成了咱们的第一个完整的多线程程序🎉🎉🎉
可是通常在实际编写多线程代码时,咱们通常不会直接建立Thread对象,而是使用线程池管理任务的执行。相信读者们也在不少地方看见过“线程池”这个词,若是但愿了解线程池相关的使用与具体实现,能够关注一下将会在近期发布的下一篇文章。
到目前为止,咱们都只是涉及了并发与多线程相关的概念和简单的多线程程序实现。接下来咱们就会进入更深刻与复杂的多线程实现当中了,包括但不限于volatile关键字、CAS、AQS、内存可见性、经常使用线程池、阻塞队列、死锁、非死锁并发问题、事件驱动模型等等知识点的应用和串联,最后你们均可以逐步实如今各类工具中经常使用的一系列并发数据结构与程序,例如AtomicInteger、阻塞队列、事件驱动Web服务器。相信你们经过这一系列多线程编程的冒险历程以后必定能够作到对多线程这个话题举重若轻、有条不紊了。