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#编写的艺术字类方法
代码如下: using System;using System.Collections.Generic;using System.ComponentModel;using System.Drawing ...
- [转载] Redis系统性介绍
转载自http://blog.nosqlfan.com/html/3139.html?ref=rediszt 虽然Redis已经很火了,相信还是有很多同学对Redis只是有所听闻或者了解并不全面,下面 ...
- [转载] Java集合---HashMap源码剖析
转载自http://www.cnblogs.com/ITtangtang/p/3948406.html 一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射 ...
- 宏WINAPI和几种调用约定
在VC SDK的WinDef.h中,宏WINAPI被定义为__stdcall,这是C语言中一种调用约定,常用的还有__cdecl和__fastcall.这些调用约定会对我们的代码产生什么样的影响?让我 ...
- 以太坊客户端Geth命令用法-参数详解
Geth在以太坊智能合约开发中最常用的工具(必备开发工具),一个多用途的命令行工具. 熟悉Geth可以让我们有更好的效率,大家可收藏起来作为Geth命令用法手册. 本文主要是对geth help的翻译 ...
- SpringBoot错误求解决
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | ...
- Intellij IDEA热加载更新 IntelliJ IDEA热加载自动更新(Update classes and resources )
定义及分类 1.1 定义 在web开发环境下,所谓热部署,即在不重新部署webapp的情况下,实时将工程代码改动更新到web容器中(例如tomcat).其原理可以类比ajax的作用,即局部刷新工程资源 ...
- Python——Scrapy初学
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架.可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中.Scrapy最初是为了页面抓取(更确切来说, 网络抓取)所设计的,也 ...
- ssh中Hibernate懒加载,session问题的学习与理解
交代本项目中要求获取session的方式如下: public Session getCurrentSession() { // 增删改使用的session,事务必须是开启的(Required,即pro ...
- 洛谷银牛派对SPFA
题目描述 One cow from each of N farms (1 ≤ N ≤ 1000) conveniently numbered 1..N is going to attend the b ...