Java:并发不易,先学会用
我从事Java编程已经11年了,绝对是个老兵;但对于Java并发编程,我只能算是个新兵蛋子。我说这话估计要遭到某些高手的冷嘲热讽,但我并不感到害怕。
因为我知道,每年都会有很多很多的新人要加入Java编程的大军,他们对“并发”编程中遇到的问题也会有感到无助的时候。而我,非常乐意与他们一道,对使用Java线程进行并发程序开发的基础知识进行新一轮的学习。
01、我们为什么要学习并发?
我的脑袋没有被如来佛祖开过光,所以喜欢一件事接着一件事的想,做不到“一脑两用”。但有些大佬就不一样,比如说诸葛亮,就能够一边想着琴谱一边谈着弹着琴,还能夹带着盘算出司马懿退兵后的打算。
诸葛大佬就有着超强的“并发”能力啊。换做是我,面对司马懿的千万大军,不仅弹不了琴,弄不好还被吓得屁滚尿流。
每个人都只有一个脑子,就像电脑只有一个CPU一样。但一个脑子并不意味着不能“一脑两用”,关键就在于脑子有没有“并发”的能力。
脑子要是有了并发能力,那真的是厉害到飞起啊,想想司马懿被气定神闲的诸葛大佬吓跑的样子就知道了。
对于程序来说,如果具有并发的能力,效率就能够大幅度地提升。你一定注册过不少网站,收到过不少验证码,如果网站的服务器端在发送验证码的时候,没有专门起一个线程来处理(并发),假如网络不好发生阻塞的话,那服务器端岂不是要从天亮等到天黑才知道你有没有收到验证码?如果就你一个用户也就算了,但假如有一百个用户呢?这一百个用户难道也要在那傻傻地等着,那真要等到花都谢了。
可想而知,并发编程是多么的重要!况且,懂不懂Java虚拟机和会不会并发编程,几乎是判定一个Java开发人员是不是高手的不三法则。所以要想挣得多,还得会并发啊!
02、并发第一步,创建一个线程
通常,启动一个程序,就相当于起了一个进程。每个电脑都会运行很多程序,所以你会在进程管理器中看到很多进程。你会说,这不废话吗?
不不不,在我刚学习编程的很长一段时间内,我都想当然地以为这些进程就是线程;但后来我知道不是那么回事儿。一个进程里,可能会有很多线程在运行,也可能只有一个。
main函数其实就是一个主线程。我们可以在这个主线程当中创建很多其他的线程。来看下面这段代码。
public class Wanger {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我叫" + Thread.currentThread().getName() + ",我超喜欢沉默王二的写作风格");
}
});
t.start();
}
}
}
创建线程最常用的方式就是声明一个实现了Runnable
接口的匿名内部类;然后将它作为创建Thread
对象的参数;再然后调用Thread
对象的start()
方法进行启动。运行的结果如下。
我叫Thread-1,我超喜欢沉默王二的写作风格
我叫Thread-3,我超喜欢沉默王二的写作风格
我叫Thread-2,我超喜欢沉默王二的写作风格
我叫Thread-0,我超喜欢沉默王二的写作风格
我叫Thread-5,我超喜欢沉默王二的写作风格
我叫Thread-4,我超喜欢沉默王二的写作风格
我叫Thread-6,我超喜欢沉默王二的写作风格
我叫Thread-7,我超喜欢沉默王二的写作风格
我叫Thread-8,我超喜欢沉默王二的写作风格
我叫Thread-9,我超喜欢沉默王二的写作风格
从运行的结果中可以看得出来,线程的执行顺序不是从0到9的,而是有一定的随机性。这是因为Java的并发是抢占式的,线程0虽然创建得最早,但它的“争宠”能力却一般,上位得比较艰辛。
03、并发第二步,创建线程池
java.util.concurrent.Executors
类提供了一系列工厂方法用于创建线程池,可把多个线程放在一起进行更高效地管理。示例如下。
public class Wanger {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("我叫" + Thread.currentThread().getName() + ",我超喜欢沉默王二的写作风格");
}
};
executorService.execute(r);
}
executorService.shutdown();
}
}
运行的结果如下。
我叫pool-1-thread-2,我超喜欢沉默王二的写作风格
我叫pool-1-thread-4,我超喜欢沉默王二的写作风格
我叫pool-1-thread-5,我超喜欢沉默王二的写作风格
我叫pool-1-thread-3,我超喜欢沉默王二的写作风格
我叫pool-1-thread-4,我超喜欢沉默王二的写作风格
我叫pool-1-thread-1,我超喜欢沉默王二的写作风格
我叫pool-1-thread-7,我超喜欢沉默王二的写作风格
我叫pool-1-thread-6,我超喜欢沉默王二的写作风格
我叫pool-1-thread-5,我超喜欢沉默王二的写作风格
我叫pool-1-thread-6,我超喜欢沉默王二的写作风格
Executors
的newCachedThreadPool()
方法用于创建一个可缓存的线程池,调用该线程池的方法execute()
可以重用以前的线程,只要该线程可用;比如说,pool-1-thread-4
、pool-1-thread-5
和pool-1-thread-6
就得到了重用的机会。我能想到的最佳形象代言人就是女皇武则天。
如果没有可用的线程,就会创建一个新线程并添加到池中。当然了,那些60秒内还没有被使用的线程也会从缓存中移除。
另外,Executors
的newFiexedThreadPool(int num)
方法用于创建固定数目线程的线程池;newSingleThreadExecutor()
方法用于创建单线程化的线程池(你能想到它应该使用的场合吗?)。
但是,故事要转折了。阿里巴巴的Java开发手册(可在「沉默王二」公众号的后台回复关键字「Java」获取)中明确地指出,不允许使用Executors来创建线程池。
不能使用Executors
创建线程池,那么该怎么创建线程池呢?
直接调用ThreadPoolExecutor
的构造函数来创建线程池呗。其实Executors
就是这么做的,只不过没有对BlockQueue
指定容量。我们需要做的就是在创建的时候指定容量。代码示例如下。
ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
04、并发第三步,解决共享资源竞争的问题
有一次,我陪家人在商场里面逛街,出电梯的时候有一个傻叉非要抢着进电梯。女儿的小推车就压到了那傻叉的脚上,他竟然不依不饶地指着我的鼻子叫嚣。我直接一拳就打在他的鼻子上,随后我们就纠缠在了一起。
这件事情说明了什么问题呢?第一,遇到不讲文明不知道“先出后进”(LIFO)规则的傻叉真的很麻烦;第二,竞争共享资源的时候,弄不好要拳脚相向。
在Java中,解决共享资源竞争问题的首个解决方案就是使用关键字synchronized
。当线程执行被synchronized
保护的代码片段的时候,会对这段代码进行上锁,其他调用这段代码的线程会被阻塞,直到锁被释放。
下面这段代码使用ThreadPoolExecutor
创建了一个线程池,池里面的每个线程会对共享资源count进行+1操作。现在,闭上眼想一想,当1000个线程执行结束后,count的值会是多少呢?
public class Wanger {
public static int count = 0;
public static int getCount() {
return count;
}
public static void addCount() {
count++;
}
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(10, 1000,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10));
for (int i = 0; i < 1000; i++) {
Runnable r = new Runnable() {
@Override
public void run() {
Wanger.addCount();
}
};
executorService.execute(r);
}
executorService.shutdown();
System.out.println(Wanger.count);
}
}
事实上,共享资源count的值很有可能是996、998,但很少会是1000。为什么呢?
因为一个线程正在写这个变量的时候,另外一个线程可能正在读这个变量,或者正在写这个变量。这个变量就变成了一个“不确定状态”的数据。这个变量必须被保护起来。
通常的做法就是在改变这个变量的addCount()
方法上加上synchronized
关键字——保证线程在访问这个变量的时候有序地进行排队。
示例如下:
public synchronized static void addCount() {
count++;
}
还有另外的一种常用方法——读写锁。分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,由Java虚拟机控制。如果代码允许很多线程同时读,但不能同时写,就上读锁;如果代码不允许同时读,并且只能有一个线程在写,就上写锁。
读写锁的接口是ReadWriteLock
,具体实现类是 ReentrantReadWriteLock
。synchronized
属于互斥锁,任何时候只允许一个线程的读写操作,其他线程必须等待;而ReadWriteLock
允许多个线程获得读锁,但只允许一个线程获得写锁,效率相对较高一些。
我们先使用枚举创建一个读写锁的单例。代码如下:
public enum Locker {
INSTANCE;
private static final ReadWriteLock lock = new ReentrantReadWriteLock();
public Lock writeLock() {
return lock.writeLock();
}
}
再在addCount()
方法中对count++;
上锁。示例如下。
public static void addCount() {
// 上锁
Lock writeLock = Locker.INSTANCE.writeLock();
writeLock.lock();
count++;
// 释放锁
writeLock.unlock();
}
使用读写锁的时候,切记最后要释放锁。
05、最后
并发编程难学吗?说实话,真的不太容易。来看一下王宝令老师总结的思维导图就能知道。
但你也知道,“冰冻三尺非一日之寒”,学习是一件循序渐进的事情。只要你学会了怎么创建一个线程,学会了怎么创建线程池,学会了怎么解决共享资源竞争的问题,你已经在并发编程的领域里迈出去了一大步。
为自己加个油,好吗?
上一篇:Java I/O 入门篇
下一篇:Java 并发编程(一):简介
Java:并发不易,先学会用的更多相关文章
- Java 并发编程(一):摩拳擦掌
这篇文章的标题原本叫做——Java 并发编程(一):简介,作者名叫小二.但我在接到投稿时觉得这标题不够新颖,不够吸引读者的眼球,就在发文的时候强行修改了标题(也不咋滴). 小二是一名 Java 程序员 ...
- 如何才能够系统地学习Java并发技术?
微信公众号[Java技术江湖]一位阿里Java工程师的技术小站 Java并发编程一直是Java程序员必须懂但又是很难懂的技术内容. 这里不仅仅是指使用简单的多线程编程,或者使用juc的某个类.当然这些 ...
- Java并发简介
年轻的时候学会了“使用”Servlet后,感觉自己什么都会做了,之后就不停的写所谓的业务逻辑,框架(这里说的不是structs,spring等,就是说servlet)给人们屏蔽了很多复杂性(更别说构建 ...
- 【转】关于Java并发编程的总结和思考
一.前言 就是想学习Java并发编程了,所以转载一下这篇认为还不错的博客~ 二.正文 编写优质的并发代码是一件难度极高的事情.Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的 ...
- java并发笔记之证明 synchronized锁 是否真实存在
警告⚠️:本文耗时很长,先做好心理准备 证明:偏向锁.轻量级锁.重量级锁真实存在 由[java并发笔记之java线程模型]链接: https://www.cnblogs.com/yuhangwang/ ...
- java并发笔记之四synchronized 锁的膨胀过程(锁的升级过程)深入剖析
警告⚠️:本文耗时很长,先做好心理准备,建议PC端浏览器浏览效果更佳. 本篇我们讲通过大量实例代码及hotspot源码分析偏向锁(批量重偏向.批量撤销).轻量级锁.重量级锁及锁的膨胀过程(也就是锁的升 ...
- Java 并发编程(二):如何保证共享变量的原子性?
线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心 ...
- 2019年Java并发精选面试题,哪些你还不会?(含答案和思维导图)
Java 并发编程 1.并发编程三要素? 2.实现可见性的方法有哪些? 3.多线程的价值? 4.创建线程的有哪些方式? 5.创建线程的三种方式的对比? 6.线程的状态流转图 7.Java 线程具有五中 ...
- Java并发指南开篇:Java并发编程学习大纲
Java并发编程一直是Java程序员必须懂但又是很难懂的技术内容. 这里不仅仅是指使用简单的多线程编程,或者使用juc的某个类.当然这些都是并发编程的基本知识,除了使用这些工具以外,Java并发编程中 ...
随机推荐
- Ocelot中文文档-配置
这里有一个配置的列子.其中有两个配置块.一个ReRoutes数组和一个GlobalConfiguration.ReRoutes配置块是一些告诉Ocelot如何处理上游请求的对象.Globalconfi ...
- Hashtable源码解析
Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. Hashtable也是JDK1.0引入的 ...
- MySQL快速生成本地测试数据
利用数据的存储过程生成测试数据: 我们可以通过数据库的的 INSERT 语句直接在存储过程中向普通数据表中添加数据,但是 当我们添加到百万数据后,往普通表插入测试数据的性能就会明显降低.所以在这里建议 ...
- python 中的csv读写
1.首先 import csv 2.读一个csv文件 data = open(filename) lines = csv.reader(data) #reader 函数和 dirtreader函数的 ...
- UE4学习心得:Scene Component蓝图的一个简单应用
Scene Component是蓝图类中一个不怎么常用的分类(特别是对于新手而言),主要是其实现的功能可以在Actor类中用相同的方法实现,使其作用显得有点多余. 笔者在使用过这个类之后发现其作用更相 ...
- java数据库基本操作(sqlserver 2000为例)
一.环境搭建 1.下载对应数据库连接驱动包并引入. 2.如果在web中调用必须在tomcat中也放入对应的驱动包. 3.在jre的lib\ext中也加入对应的驱动包. 二.连接数据库 public s ...
- asp.net图片上传及删除
方法一:UploadImage cetFU = new UploadImage(); if (cetFU.UpFile(FileUpload1, "teacher_picture" ...
- StringBuffer与StringBuilder
有些时候,需要由较短的字符串构建字符串.比如,按键或来自文件中的单词.采用字符串连接的方式达到此目的效率比较低.每次连接字符串的时候,都会构建一个新的String对象,既耗时,又浪费空间.使用Stri ...
- Oracle12c中SQL优化(SQL TUNING)新特性之SQL计划指令
SQL计划指令是Oracle12c中自适应查询优化的功能之一.SQL计划指令就像“额外的提醒” ,用以提醒优化器你先前选择了的计划并不是最优的,典型的是因为错误的势评估.错误的势评估往往是由统计信息缺 ...
- 网络营销行业十大看了就想吐的“滥词”
网络营销行业在国内的互联网界已"猖獗"数年之久,它是一个让企业爱让用户恨的行业.有互联网的地方,就有网络营销的机会,有了机会就有了相关产业的存在,只不过是业大业小的问题.但是随着互 ...