前言:休整一个多月之后,终于开始投简历了。这段时间休息了一阵子,又病了几天,真正用来复习准备的时间其实并不多。说实话,心里不是非常有底气。

这可能是学生时代遗留的思维惯性——总想着做好万全准备才去做事。当然,在学校里考试之前当然要把所有内容学一遍和复习一遍。但是,到了社会里做事,很多时候都是边做边学。应聘如此,工作如此,很多的挑战都是如此。没办法,硬着头皮上吧。

3.5 线程的分组管理

在实际的开发过程当中,可能会有多个线程同时存在,这对批量处理有了需求。这就有点像用迅雷下载电视剧,假设你在同时下载《越狱》和《纸牌屋》,这时候女朋友说想先看《越狱》,那么为了尽快满足她就要先暂停其他电视剧的下载。一个一个点暂停效率很低,最好的方法是批量选择所有的目标任务再点暂停。

每个线程都属于某个线程群组,即ThreadGroup。如果在main()主流程中产生了一个线程,该线程就属于main线程群组。我们可以使用这样的语句取得目前线程所属线程组名:

         Thread.currentThread().getThreadGroup().getName();

每个线程产生时,都会归入某个线程群组。如果没有指定,则会归入产生该子线程的线程群组。当然,也可以自行指定线程群组。需要特别注意的是,线程一旦归到某个群组,就无法更换。

java.lang.ThreadGroup类如其名,可以管理群组中的线程。可以使用以下方法产生群组,并在产生线程的时候指定所属群组:

 ThreadGroup threadGroup1 = new ThreadGroup("group1");
ThreadGroup threadGroup2 = new ThreadGroup("group2");
Thread thread1 = new Thread(threadGroup1, "group1's member");
Thread thread2 = new Thread(threadGroup2, "group2's member");

ThreadGroup的某些方法,可以对群组中所有线程产生作用。例如,interrupt()方法可以中断群组里面所有的线程,setMaxPriority()方法可以设定群组中所有线程最大优先权(本来就拥有更高优先权的线程不受影响)。

如果想要一次性地取得群组中所有线程,可以使用enumerate()方法:

 Thread[] threads = new Thread[threadGroup1.activeCount()];
threadGroup1.enumerate(threads);

在这个代码片段里面,activeCount()方法取得群组的线程数量,enumerate()方法要传入Thread数组,这会将线程对象设定到每个数组索引。

3.5.1 线程群组的异常处理

把若干线程归入到某个特定的线程群组之后,如果群组中某个线程发生了异常,有可能我们会采用统一的处理方式。

ThreadGroup中有个uncaughtException()方法,群组中某个线程发生异常而未捕捉时,JVM会调用此方法进行处理。如果ThreadGroup有父ThreadGroup,就会调用父ThreadGroup的uncaughtException()方法,否则看看异常是否为ThreadDeath实例。如果是那就什么都不做;如果不是就要调用异常的printStrackTrace()。如果必须定义ThreadGroup中线程的异常处理行为,可以重新定义此方法。例如:

 /**
* Created by Levenyes on 2017/7/29.
*/
public class ThreadGroupDemo {
public static void main(String[] args) {
ThreadGroup tg1 = new ThreadGroup("tg1") {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("%s: %s%n", t.getName(), e.getMessage());
}
}; Thread t1 = new Thread(tg1, new Runnable() {
public void run() {
throw new RuntimeException("测试异常");
}
}
); t1.start();
}
}

uncaughtException()方法第一个参数可取得发生异常的线程实例,第二个参数可取得异常对象,实验用例中显示了线程的名称以及异常信息:

3.6 为什么线程会不安全?

以前刚毕业那会儿背面试题就背到过,“String是定长的,StringBuffer和StringBuilder是不定长的;StringBuffer是线程安全的,StringBuilder是线程不安全的”。那么问题就来了,为什么线程会不安全呢?

我们在之前的文章里讲到过ArrayList类。之前在单线程的情况下使用是没有问题的,但如果在多线程的环境下使用会不会出现意外呢?

 import java.util.*;

 /**
* 线程不安全实验用例
*/
public class ArrayListDemo {
public static void main(String[] args) {
final ArrayList list = new ArrayList ();
Thread t1 = new Thread() {
public void run() {
while(true) {
list.add(1);
}
}
};
Thread t2 = new Thread() {
public void run() {
while(true) {
list.add(2);
}
}
};
t1.start();
t2.start();
}
}

如果你跟我一样让上面这段代码跑起来,就“有可能”出现下面这些异常:

为什么要强调说是“有可能”呢?这是几率问题,有可能发生,也有可能没发生,就因数组长度过长,JVM分配到的内存不够,而发生java.lang.OutOfMemoryError,我们不讨论OutOfMemoryError问题,而将焦点放在为何会出现ArrayIndexOutOfBoundsException异常。

首先来看ArrayList在JavaSE源代码中的add()方法:

     /**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

这个方法先会检查数组的大小是否已经到了最大值,如果是的话就会先做增加最大值的动作,再把新元素加入到数组当中来。按理来说,不可能会出现溢出的情况。

然而,如果有t1、t2两个线程同时调用add()方法,假设t1执行add()已经到了elementData[size++] = e这行,这个时候CPU调度器将t1置为Runnable状态,将t2置为Running状态,而t2执行add()已经完成elementData[size++] = e这行的执行,此时刚好数组满了。如果这个时候CPU调度器将t2置为Runnable状态,将t1置为Running状态,t1就会继续跑elementData[size++] = e这一行,因为数组已经满了,就会出现ArrayIndexOutOfBoundsException异常。

用术语来说,这就是线程存取同一对象相同资源时所引发的竞速,即Race condition。类似这样因为多线程而出错的情况,我们就可以理解成线程有了出错的危险,即不安全。

像ArrayList这样的类,我们习惯称为不具备线程安全(Thread-safe)或线程不安全的类。

3.7 保证同步的syncronized

如何解决线程不安全的问题呢?我们可以使用关键字synchronized,顾名思义,就是同步的意思。

 public synchronized boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

想办法在add()方法前面加上synchronized关键字之后,再次运行前面那个demo,ArrayIndexOutOfBoundsException就不会再出现了。这是为什么呢?

这是因为每个对象都会有个内部锁定,即IntrinsicLock,或称为监控锁定,即Monitor lock。被标识为synchronized的区块将会被监控,任何线程要执行synchronized区块将会被监控,任何线程要执行该区块都必须先取得指定的对象锁定。

如果A线程已取得对象锁定开始执行synchronized区块,B线程也想执行synchronized区块,会因无法取得对象锁定而进入等待锁定状态,直到A线程释放锁定(例如执行完了区块内的任务),B线程才有可能取得锁定而执行synchronized区块。

举个不太恰当的例子,这就好像你跟一个好哥们到西藏自驾游,任意时刻都只能有一个人在驾驶位上开车。如果你在开车,你的哥们就只能在旁边看着。只有等你停车从驾驶位上走开,他才可以坐上去开车。如果你们是一个人负责踩刹车和油门,另一个人负责握方向盘,就很有可能发生交通意外。

值得一提的是,线程在等待对象锁定时,也会进入Blocked状态。所以我们可以进一步扩展在上一篇文章提到过的线程生命周期示意图:

线程如果因为尝试执行synchronized区块而进入Blocked状态,在取得锁定之后,会先回到Runnable状态,等待CPU调度器排入Running状态。

synchronized不是只可以声明在方法上,也可以描述句方式使用。例如下面这样的写法:

     public void add(Object o) {
synchronized (this) {
if(next == list.length) {
list = Arrays.copyOf(list, list.length * 2);
}
list[next++] = o;
}
}

这个程序片段的意思就是,在线程要执行synchronized区块时,必须取得括号中指定的对象锁定。事实上此语法目的之一,可应用于不想锁定整个方法,而只想锁定会发生竞速状况的区块,在执行完区块后线程即释放锁定,其他线程就有机会再竞争对象锁定,相较于将整个方法声明为synchronized来说,会比较有效率。

我们在之前的文章介绍过的Collection和Map,它们的实现类大多没有考虑线程安全,其实可以使用自带的synchronizedCollection()、synchronizedList()、synchronizedSet()、synchronizedMap()等方法获取新增线程安全特性的对象。

3.8 线程安全小结

值得注意的是,synchronized声明固然可以让线程变得“安全”,不容易发生竞速状况,但这样的线程安全特性需要付出代价。首先是很大概率会使运行效率有程度不一的下降,因为会一旦发生因synchronized而起的阻塞状况就会令运行时间变长。其次,如果程序设计不当,还有可能会发生死锁这样严重的问题,即Dead Lock。

一个线程要完成一个事务可能需要多个资源,就好像你要做饭,需要用到菜刀和砧板。如果这时候你跟你的舍友都要切菜,你拿着菜刀不肯放,他拿着砧板不肯放,那就谁都吃不上饭。对应到多线程当中,一个事务需要同时利用a和b资源才可以完成,有可能出现A线程锁定a资源,B线程锁定b资源,两个线程同时在等待对方放弃锁定。

当然了,我们人是活的,可以互相商量着让谁先切菜。但是程序是“死”的,如果没有一个良好的设计机制避免死锁发生,很有可能就会出现多个线程同时等待且不可能等待结束的状况。

因此,如果你的程序没有出现竞速状况的可能性,就尽量不要用synchronized声明。一旦使用,就要考虑到性能下降和可能发生死锁这两个关键点,尽可能在获取线程安全这一特性的同时尽可能避免发生死锁和尽可能少地牺牲效率。

相关文章推荐:

JavaSE中Collection集合框架学习笔记(1)——具有索引的List

JavaSE中Collection集合框架学习笔记(2)——拒绝重复内容的Set和支持队列操作的Queue

JavaSE中Collection集合框架学习笔记(3)——遍历对象的Iterator和收集对象后的排序

JavaSE中Map框架学习笔记

JavaSE中线程与并行API框架学习笔记1——线程是什么?

如果你喜欢我的文章,可以扫描关注我的个人公众号“李文业的思考笔记”。

不定期地会推送我的原创思考文章。

JavaSE中线程与并行API框架学习笔记——线程为什么会不安全?的更多相关文章

  1. JavaSE中线程与并行API框架学习笔记1——线程是什么?

    前言:虽然工作了三年,但是几乎没有使用到多线程之类的内容.这其实是工作与学习的矛盾.我们在公司上班,很多时候都只是在处理业务代码,很少接触底层技术. 可是你不可能一辈子都写业务代码,而且跳槽之后新单位 ...

  2. java JDK8 学习笔记——第11章 线程和并行API

    第11章 线程与并行API 11.1 线程 11.1.1 线程 在java中,如果想在main()以外独立设计流程,可以撰写类操作java.lang.Runnable接口,流程的进入点是操作在run( ...

  3. JavaSE中Collection集合框架学习笔记(2)——拒绝重复内容的Set和支持队列操作的Queue

    前言:俗话说“金三银四铜五”,不知道我要在这段时间找工作会不会很艰难.不管了,工作三年之后就当给自己放个暑假. 面试当中Collection(集合)是基础重点.我在网上看了几篇讲Collection的 ...

  4. JavaSE中Collection集合框架学习笔记(3)——遍历对象的Iterator和收集对象后的排序

    前言:暑期应该开始了,因为小区对面的小学这两天早上都没有像以往那样一到七八点钟就人声喧闹.车水马龙. 前两篇文章介绍了Collection框架的主要接口和常用类,例如List.Set.Queue,和A ...

  5. JavaSE中Map框架学习笔记

    前言:最近几天都在生病,退烧之后身体虚弱.头疼.在床上躺了几天,什么事情都干不了.接下来这段时间,要好好加快进度才好. 前面用了三篇文章的篇幅学习了Collection框架的相关内容,而Map框架相对 ...

  6. Yii框架学习笔记(二)将html前端模板整合到框架中

    选择Yii 2.0版本框架的7个理由 http://blog.chedushi.com/archives/8988 刚接触Yii谈一下对Yii框架的看法和感受 http://bbs.csdn.net/ ...

  7. phalcon(费尔康)框架学习笔记

    phalcon(费尔康)框架学习笔记 http://www.qixing318.com/article/phalcon-framework-to-study-notes.html 目录结构   pha ...

  8. MEAN框架学习笔记

    MEAN框架学习笔记 MEAN开发框架的资料非常少.基本的资料还是来自于learn.mean.io站点上的介绍. 于是抱着一种零基础学习的心态,在了解的过程中,通过翻译加上理解将MEAN框架一点点消化 ...

  9. Java基础及JavaWEB以及SSM框架学习笔记Xmind版

    Java基础及JavaWEB以及SSM框架学习笔记Xmind版 转行做程序员也1年多了,最近开始整理以前学习过程中记录的笔记,以及一些容易犯错的内容.现在分享给网友们.笔记共三部分. JavaSE 目 ...

随机推荐

  1. javascript走马灯的效果(文档标题文字滚动)

    做一些网站的时候,文档标题会滚动,这个效果是走马灯的效果. <!DOCTYPE html> <html> <head> <meta charset=" ...

  2. [SDOI2010]所驼门王的宝藏

    题目描述 在宽广的非洲荒漠中,生活着一群勤劳勇敢的羊驼家族.被族人恭称为"先知"的Alpaca L. Sotomon是这个家族的领袖,外人也称其为"所驼门王". ...

  3. PHP中递归最详解释.

    说到递归函数想必会有很多同学感到晕晕的,很难绕,容易绕错,那下面就让我来为大家详解一下. 首先,什么是递归函数呢? 1.所谓递归:指的是在函数内部,调用函数自身的操作.2.递归分两布:递(从最外层函数 ...

  4. 详细介绍Java虚拟机(JVM)

    1. JVM生命周期 启动.启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例 ...

  5. RabbitMQ安装|使用|概念|Golang开发

    搬砖的陈大师版权所有,转载请注明:http://www.lenggirl.com/tool/RabbitMQ.html 手册:http://www.rabbitmq.com/getstarted.ht ...

  6. vijos1056题解

    题目: 桌面上放了N个平行于坐标轴的矩形,这N个矩形可能有互相覆盖的部分,求它们组成的图形的面积. 在翻题目时,偶然发现了这道标号为WA的题目. 原来,以前我把一中培训的代码发了上去,却WA了4个点, ...

  7. JavaScript 的注释和快捷键

    添加必要的注释,对一个有责任心.有道德模范的前端必须具备的好习惯, 可以大大提高代码的可维护性.可读性. java代码注释快捷键:ctrl+shift+/首先熟悉一下html.css.js的注释的写法 ...

  8. 使用three.js加载3dmax资源,以及实现场景中的阴影效果

    使用three.js可以方便的让我们在网页中做出各种不同的3D效果.如果希望2D绘图内容,建议使用canvas来进行.但很多小伙伴不清楚到底如何为我们绘制和导入的图形添加阴影效果,更是不清楚到底如何导 ...

  9. 取得system32文件夹下面文件的写入权限

    取得system32文件夹下面文件的写入权限 TAKEOWN /F %SystemRoot%\system32\riched32.dll ICACLS %SystemRoot%\system32\ri ...

  10. 面向对象15.3String类-常见功能-获取-2

    public class String_APImethod {/* * 1.4获取字符串中的一部分字符串,也叫字符串 * String substring(int beginIndex, int en ...