Java并发编程有多难?这几个核心技术你掌握了吗?
本文主要内容索引
1、Java线程
2、线程模型
3、Java线程池
4、Future(各种Future)
5、Fork/Join框架
6、volatile
7、CAS(原子操作)
8、AQS(并发同步框架)
9、synchronized(同步锁)
10、并发队列(阻塞队列)
本文仅分析java并发编程中的若干核心问题,对于上面没有提到但是又和java并发编程有密切关系的技术将会不断添加进来完善文章,本文将长期更新,不断迭代。本文试图从一个更高的视觉来总结Java语言中的并发编程内容,希望阅读完本文之后,可以收获一些内容,至少应该知道在java中做并发编程实践的时候应该注意什么,应该关注什么,如何保证线程安全,以及如何选择合适的工具来满足需求。当然,更深层次的内容就会涉及到jvm层面的知识,包括底层对java内存的管理,对线程的管理等较为核心的问题,当然,本文的定位在于抽象与总结,更为具体而深入的内容就需要自己去实践,考虑到可能篇幅过长、重复描述某些内容,以及自身技术深度等原因,本文将在深度和广度上做一些权衡,某些内容会做一些深入的分析,而有些内容会一带而过,点到为止,总之,本文就当是对学习java并发编程内容的一个总结,以及给哪些希望快速了解java并发编程内容的读者抛砖引玉,不足之处还望指正。
一、Java线程
一般来说,在java中实现高并发是基于多线程编程的,所谓并发,也就是多个线程同时工作,来处理我们的业务,在机器普遍多核心的今天,并发编程的意义极为重大,因为我们有多个cpu供线程使用,如果我们的应用依然只使用单线程模式来工作的话,对极度浪费机器资源的。所以,学习java并发知识的首要问题是:如何创建一个线程,并且让这个线程做一些事情?这是java并发编程内容的起点,下面将分别介绍多个创建线程,并且让线程做一些事情的方法。
继承Thread类
继承Thread类,然后重写run方法,这是第一种创建线程的方法。run方法里面就是我们要做的事情,可以在run方法里面写我们想要在新的线程里面运行的任务,下面是一个小例子,我们继承了Thread类,并且在run方法里面打印出了当然线程的名字,然后sleep1秒中之后就退出了:
/*** Created by hujian06 on 2017/10/31.** the demo of thread*/public class ThreadDemo { public static void main(String ... args) { AThread aThread = new AThread(); //start the thread aThread.start(); }}class AThread extends Thread { @Override public void run() { System.out.println("Current Thread Name:" + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }}
如果我们想要启动这个线程,只需要像上面代码中那样,调用Thread类的start方法就可以了。
实现Runnable接口
启动一个线程的第二种方法是实现Runnable接口,然后实现其run方法,将你想要在新线程里面执行的业务代码写在run方法里面,下面的例子展示了这种方法启动线程的示例,实现的功能和上面的第一种示例是一样的:
/*** Created by hujian06 on 2017/10/31.** the demo of Runnable*/public class ARunnableaDemo { public static void main(String ... args) { ARunnanle aRunnanle = new ARunnanle(); Thread thread = new Thread(aRunnanle); thread.start(); }}class ARunnanle implements Runnable { @Override public void run() { System.out.println("Current Thread Name:" + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }}
在启动线程的时候,依然还是使用了Thread这个类,只是我们在构造函数中将我们实现的Runnable对象传递进去了,所以在我们执行Thread类的start方法的时候,实际执行的内容是我们的Runnable的run方法。
使用FutureTask
启动一个新的线程的第三种方法是使用FutureTask,下面来看一下FutureTask的类图,就可以明白为什么可以使用FutureTask来启动一个新的线程了:
FutureTask的类图
从FutureTask的类图中可以看出,FutureTask实现了Runnable接口和Future接口,所以它兼备Runnable和Future两种特性,下面先来看看如何使用FutureTask来启动一个新的线程:
import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;/*** Created by hujian06 on 2017/10/31.** the demo of FutureTask*/public class FutureTaskDemo { public static void main(String ... args) { ACallAble callAble = new ACallAble(); FutureTask<String> futureTask = new FutureTask<>(callAble); Thread thread = new Thread(futureTask); thread.start(); do { }while (!futureTask.isDone()); try { String result = futureTask.get(); System.out.println("Result:" + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }}class ACallAble implements Callable<String> { @Override public String call() throws Exception { Thread.sleep(1000); return "Thread-Name:" + Thread.currentThread().getName(); }}
可以看到,使用FutureTask来启动一个线程之后,我们可以监控这个线程是否完成,上面的示例中主线程会一直等待这个新创建的线程直到它返回,其实只要是Future提供的接口,我们在FutureTask中都可以使用,这极大的方便了我们,Future在并发编程中的意义极为重要,Future代表一个未来会发生的东西,它是一种暗示,一种占位符,它示意我们它可能不会立即得到结果,因为它的任务还在运行,但是我们可以得到一个对这个线程的监控对象,我们可以对线程的执行做一些判断,甚至是控制,比如,如果我们觉得我们等了太久,并且我们觉得没有必要再等待下去的时候,就可以将这个Task取消,还有一点需要提到的是,Future代表它可能正在运行,也可能已经返回,当然Future更多的暗示你可以在等待这个结果的同时可以使用其他的线程做一些其他的事情,当你真的需要这个结果的时候再来获取就可以了,这就是并发,理解这一点非常重要。
本小节通过介绍三种创建并启动一个新线程的方法,为进行并发编程开了一个头,目前,我们还只是在能创建多个线程,然后让多个线程做不同个的事情的阶段,当然,这是学习并发编程最为基础的,无论如何,现在,我们可以让我们的应用运行多个线程了,下面的文章将会基于这个假设(一个应用开启了多个线程)讨论一些并发编程中值得关注的内容。关于本小节更为详细的内容,可以参考文章Java CompletableFuture中的部分内容。
二、线程模型
我们现在可以启动多个线程,但是好像并没有形成一种类似于模型的东西,非常混乱,并且到目前为止我们的多个线程依然只是各自做各自的事情,互不相干,多个线程之间并没有交互(通信),这是最简单的模型,也是最基础的模型,本小节试图介绍线程模型,一种指导我们的代码组织的思想,线程模型确定了我们需要处理那些多线程的问题,在一个系统中,多个线程之间没有通信是不太可能的,更为一般的情况是,多个线程共享一些资源,然后相互竞争来获取资源权限,多个线程相互配合,来提高系统的处理能力。正因为多个线程之间会有通信交互,所以本文接下来的讨论才有了意义,如果我们的系统里面有几百个线程在工作,但是这些线程互不相干,那么这样的系统要么实现的功能非常单一,要么毫无意义(当然不是绝对的,比如Netty的线程模型)。
继续来讨论线程模型,上面说到线程模型是一种指导代码组织的思想,这是我自己的理解,不同的线程模型需要我们使用不同的代码组织,好的线程模型可以提高系统的并发度,并且可以使得系统的复杂度降低,这里需要提一下Netty 4的线程模型,Netty 4的线程模型使得我们可以很容易的理解Netty的事件处理机制,这种优秀的设计基于Reactor线程模型,Reactor线程模型分为单线程模型、多线程模型以及主从多线程模型,Netty的线程模型类似于Reactor主从多线程模型。
当然线程模型是一种更高级别的并发编程内容,它是一种编程指导思想,尤其在我们进行底层框架设计的时候特别需要注意线程模型,因为一旦线程模型设计不合理,可能会导致后面框架代码过于复杂,并且可能因为线程同步等问题造成问题不可控,最终导致系统运行失控。类似于Netty的线程模型是一种好的线程模型,下面展示了这种模型:
Netty线程模型
简单来说,Netty为每个新建立的Channel分配一个NioEventLoop,而每个NioEventLoop内部仅使用一个线程,这就避免了多线程并发的同步问题,因为为每个Channel处理的线程仅有一个,所以不需要使用锁等线程同步手段来做线程同步,在我们的系统设计的时候应该借鉴这种线程模型的设计思路,可以避免我们走很多弯路。关于线程池以及Netty线程池这部分的内容,可以参考文章Netty线程模型及EventLoop详解。Java线程池池化技术是一种非常有用的技术,对于线程来说,创建一个线程的代价是很高的,如果我们在创建了一个线程,并且让这个线程做一个任务之后就回收的话,那么下次要使用线程来执行我们的任务的时候又需要创建一个新的线程,是否可以在创建完成一个线程之后一直缓冲,直到系统关闭的时候再进行回收呢?java线程池就是这样的组件,使用线程池,就没必要频繁创建线程,线程池会为我们管理线程,当我们需要一个新的线程来执行我们的任务的时候,就向线程池申请,而线程池会从池子里面找到一个空闲的线程返回给请求者,如果池子里面没有可用的线程,那么线程池会根据一些参数指标来创建一个新的线程,或者将我们的任务提交到任务队列中去,等待一个空闲的线程来执行这个任务。细节内容在下文中进行分析,目前我们只需要明白,线程池里面有很多线程,这些线程会一直到系统关系才会被回收,否则一直会处于处理任务或者等待处理任务的状态。首先,如何使用线程池呢?下面的代码展示了如何使用java线程池的例子:
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.ThreadFactory;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;/*** Created by hujian06 on 2017/10/31.** the demo of Executors*/public class ExecutorsDemo { public static void main(String ... args) { int cpuCoreCount = Runtime.getRuntime().availableProcessors(); AThreadFactory threadFactory = new AThreadFactory(); ARunnanle runnanle = new ARunnanle(); ExecutorService fixedThreadPool= Executors.newFixedThreadPool(cpuCoreCount, threadFactory); ExecutorService cachedThreadPool = Executors.newCachedThreadPool(threadFactory); ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(cpuCoreCount, threadFactory); ScheduledExecutorService singleThreadExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory); fixedThreadPool.submit(runnanle); cachedThreadPool.submit(runnanle); newScheduledThreadPool.scheduleAtFixedRate(runnanle, 0, 1, TimeUnit.SECONDS); singleThreadExecutor.scheduleWithFixedDelay(runnanle, 0, 100, TimeUnit.MILLISECONDS); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } fixedThreadPool.shutdownNow(); cachedThreadPool.shutdownNow(); newScheduledThreadPool.shutdownNow(); singleThreadExecutor.shutdownNow(); }}class ARunnable implements Runnable { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Current Thread Name:" + Thread.currentThread().getName()); }}/*** the thread factory*/class AThreadFactory implements ThreadFactory { private final AtomicInteger threadNumber = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { return new Thread("aThread-" + threadNumber.incrementAndGet()); }}
更为丰富的应用应该自己去探索,结合自身的需求来借助线程池来实现,下面来分析一下Java线程池实现中几个较为重要的内容。
ThreadPoolExecutor和ScheduledThreadPoolExecutor
ThreadPoolExecutor和ScheduledThreadPoolExecutor是java实现线程池的核心类,不同类型的线程池其实就是在使用不同的构造函数,以及不同的参数来构造出ThreadPoolExecutor或者ScheduledThreadPoolExecutor,所以,学习java线程池的重点也在于学习这两个核心类。前者适用于构造一般的线程池,而后者继承了前者,并且很多内容是通用的,但是ScheduledThreadPoolExecutor增加了schedule功能,也就是说,ScheduledThreadPoolExecutor使用于构造具有调度功能的线程池,在需要周期性调度执行的场景下就可以使用ScheduledThreadPoolExecutor。关于ThreadPoolExecutor与ScheduledThreadPoolExecutor较为详细深入的分析可以参考下面的文章:
Java线程池详解(二)
Java线程池详解(一)
Java调度线程池ScheduleExecutorService
Java调度线程池ScheduleExecutorService(续)
Java并发编程有多难?这几个核心技术你掌握了吗?的更多相关文章
- 【Java并发编程实战】----- AQS(四):CLH同步队列
在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...
- 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport
在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...
- 【Java并发编程实战】----- AQS(二):获取锁、释放锁
上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...
- 【Java并发编程实战】-----“J.U.C”:CLH队列锁
在前面介绍的几篇博客中总是提到CLH队列,在AQS中CLH队列是维护一组线程的严格按照FIFO的队列.他能够确保无饥饿,严格的先来先服务的公平性.下图是CLH队列节点的示意图: 在CLH队列的节点QN ...
- 【Java并发编程实战】-----“J.U.C”:CountDownlatch
上篇博文([Java并发编程实战]-----"J.U.C":CyclicBarrier)LZ介绍了CyclicBarrier.CyclicBarrier所描述的是"允许一 ...
- 【Java并发编程实战】-----“J.U.C”:CyclicBarrier
在上篇博客([Java并发编程实战]-----"J.U.C":Semaphore)中,LZ介绍了Semaphore,下面LZ介绍CyclicBarrier.在JDK API中是这么 ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock
ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...
- Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...
- JAVA并发编程J.U.C学习总结
前言 学习了一段时间J.U.C,打算做个小结,个人感觉总结还是非常重要,要不然总感觉知识点零零散散的. 有错误也欢迎指正,大家共同进步: 另外,转载请注明链接,写篇文章不容易啊,http://www. ...
随机推荐
- c#控件攻略宝典之ListBox控件
ListBox控件的使用: 1)控件属性 Items SelectedItems SelectioModes 2)数据绑定 DataSoure DisplayMember ValueMenber 3) ...
- redis特性 存储 API 集群 等
公司组内技术分享,刚好最近工作用redis构建缓存,所以想同事们分享关于redis的一些知识, 这些知识不仅仅是包括一些API层,而是一些关于redis功能功能特性的 目前为什么使用redis构建缓存 ...
- shell中条件判断语法与判断条件小结
1. IF条件判断语法: if Athen dosthelif B dosthelse dosthfi 2. 判断条件: 2.1 字符串判断 str1 = str2 当两个串有相同 ...
- 3)C语言数组(C自考学习)
一维数组 一维数组中的元素是排列成一行的,当需要存放一行或一列数据时常用一维数组.一维数组的元素常用"数组名[下标]"来表示,其中,下表是从0开始的.例如:a[0]="h ...
- Makefile Android.mk 引发的思索
在我们编写 Android 平台 cocos2d-x 游戏的时候,我们除了编写 Classes 之内的源代码文件之外,我们还需要维护其编译文件 Android.mk,如我们在 Classes 添加新的 ...
- Javascript-基础概念总结
[作用域]javascript中的作用域可以理解为一个语句执行的环境大小,有全局的作用域,函数作用域和eval作用域.在JS中没有块级作用域.讲到作用域,不得不讲执行环境,执行环境在JS中是最为重要的 ...
- devdependencies与dependencies的区别
一直在纠结devdependencies与dependencies的区别是什么,下面就对此作出详细介绍,希望对你有所帮助! 我们在使用npm install 安装模块或插件的时候,有两种命令把他们写入 ...
- java 之 适配器模式(大话设计模式)
适配器模式,笔者不是很推荐在项目初期阶段使用,在笔者看来这个设计模式就是套接了一层,从而达到能够迎合现有的外部接口规范. 先来简单的看下类图: 大话设计模式-类图 这个模式理解起来非常简单,A→B因为 ...
- ACM HDU 1081 To The Max
To The Max Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) To ...
- Java分布式锁实现详解
在进行大型网站技术架构设计以及业务实现的过程中,多少都会遇到需要使用分布式锁的情况.那么问题也就接踵而至,哪种分布式锁更适合我们的项目? 下面就这个问题,我做了一些分析: 分布式锁现状: 目前几乎很多 ...