我从事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() {@Overridepublic 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() {@Overridepublic 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,我超喜欢沉默王二的写作风格

ExecutorsnewCachedThreadPool()方法用于创建一个可缓存的线程池,调用该线程池的方法execute()可以重用以前的线程,只要该线程可用;比如说,pool-1-thread-4pool-1-thread-5pool-1-thread-6就得到了重用的机会。我能想到的最佳形象代言人就是女皇武则天。

如果没有可用的线程,就会创建一个新线程并添加到池中。当然了,那些60秒内还没有被使用的线程也会从缓存中移除。

另外,ExecutorsnewFiexedThreadPool(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() {@Overridepublic 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,具体实现类是 ReentrantReadWriteLocksynchronized属于互斥锁,任何时候只允许一个线程的读写操作,其他线程必须等待;而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:并发不易,先学会用相关推荐

  1. Java并发编程--学会他就能抵挡千军万马

    java并发主要依赖于JVM和cpu指令 java对象锁 3种 1.普通同步方法,锁是当前实例对象 2.对于静态同步方法,锁是当前类的class对象 3.对于同步代码块,锁是synchronized括 ...

  2. 学习笔记:Java 并发编程②_管程

    若文章内容或图片失效,请留言反馈. 部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 视频链接:https://www.bilibili.com/video/av81461839 配套资料: ...

  3. Java并发编程中的若干核心技术,向高手进阶

    来源:http://www.jianshu.com/p/5f499f8212e7 引言 本文试图从一个更高的视角来总结Java语言中的并发编程内容,希望阅读完本文之后,可以收获一些内容,至少应该知道在 ...

  4. Java 并发编程系列之带你了解多线程

    早期的计算机不包含操作系统,它们从头到尾执行一个程序,这个程序可以访问计算机中的所有资源.在这种情况下,每次都只能运行一个程序,对于昂贵的计算机资源来说是一种严重的浪费. 操作系统出现后,计算机可以运 ...

  5. Java并发/多线程教程——1

    本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获.由于个人水平有限,不对之处还望矫正!在早期,计算机只有一个CPU,同一时刻只能执行一个程序,后来有了多任务的说法,多任务是 ...

  6. Java并发编程实战_不愧是领军人物!这种等级的“Java并发编程宝典”谁能撰写?...

    前言 大家都知道并发编程技术就是在同一个处理器上同时的去处理多个任务,充分的利用到处理器的每个核心,最大化的发挥处理器的峰值性能,这样就可以避免我们因为性能而产生的一些问题. 大厂的核心负载肯定是非常 ...

  7. Java并发编程:从源码分析几道必问线程池的面试题?

    引言 上一篇文章我们有介绍过线程池的一个基本执行流程<[Java并发编程]面试必备之线程池>以及它的7个核心参数,以及每个参数的作用.以及如何去使用线程池 还留了几个小问题..建议看这篇文 ...

  8. Java并发工具辅助类代码实例

    java中的并发工具类 一:等待多线程完成的CountDownLatch CountDownLatch允许一个或多个线程等待其他线程完成操作. packag``.fuzhulei; import ja ...

  9. 如何才能够系统地学习Java并发技术?

    Java并发编程一直是Java程序员必须懂但又是很难懂的技术内容. 这里不仅仅是指使用简单的多线程编程,或者使用juc的某个类.当然这些都是并发编程的基本知识,除了使用这些工具以外,Java并发编程中 ...

最新文章

  1. Linux(CentOS 7)(64位)系统下安装Pymol(1.8.6)
  2. 零基础python必背代码-30个Python常用极简代码,拿走就用
  3. JAVA构造方法,继承关系和SUPER关键字
  4. loadrunner与事务有关的函数
  5. P6295-有标号 DAG 计数【多项式求逆,多项式ln】
  6. @Resource和@Autowired作用和区别
  7. JVM运行时常量池跟静态常量池区别
  8. 计算机学硕和专硕编码,2018考研报名:学会从专业代码判断学硕还是专硕
  9. 社保系列11《ATR》
  10. python画图turtle小人_python海龟绘图怎么增加每次画圆的半径|海龟python做图教程...
  11. 关于eclipse中maven项目的问题
  12. C#:获取访问端口号
  13. mysql返回李连杰的姓_李连杰:我爷爷不姓李,说出名字后害怕你们没胆量播出去...
  14. matlab新建数据字典及如何导入
  15. 2020.10-2021.01前端开发部分总结
  16. 我的OpenCV4学习之旅(1)-OpenCV4初步了解
  17. HCIP之路重点LSA
  18. 卡巴斯基互联网安全套装(kis 官方
  19. 嵌入式GUI LVGL资料下载
  20. java中什么是类型_什么是Java中的“SAM类型”?

热门文章

  1. ubuntu 16.04使用IntelRealSense D435i调用realsense ROS包时,报symbol lookup error和undefined symbol错误的解决办法
  2. 大富翁6 联网 联机 故障 解决
  3. 求图的割点,割边(啊哈算法)
  4. java 是脚本语言_java中使用脚本语言
  5. 程序员创业网——人脉网络
  6. lua 获取时分秒_果壳lua教程(第五集) :面向对象编程-制作一个钟表
  7. 67.深度解密网络项目五:线上和线下营销结合的“社交零售”和“社区团购”
  8. npm,node更新最新版本
  9. Apache Drill源码分析(2) 分析一次具体的查询过程以及RPC
  10. 济南网站优化公司告诉你快照回档的几个原因