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

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

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. 卷积神经网络的变种: PCANet

    前言:昨天和大家聊了聊卷积神经网络,今天给大家带来一篇论文:pca+cnn=pcanet.现在就让我带领大家来了解这篇文章吧. 论文:PCANet:A Simple Deep Learning Bas ...

  2. android studio IDE 下,设置ACTIVITY全屏

    因为ANDROID STUDIO的JAVA类是继承AppCompatActivity的 ,所以常规的全屏设置并不管用.如果要设置全屏,请参照如下代码/ 1/首先,打开AndroidManifest.x ...

  3. 转 使用HAProxy,PHPRedis,和MySQL支撑10亿请求每周架构细节

    [编者按]在公司的发展中,保证服务器的可扩展性对于扩大企业的市场需要具有重要作用,因此,这对架构师提出了一定的要求.Octivi联合创始人兼软件架构师Antoni Orfin将向你介绍一个非常简单的架 ...

  4. .net 4.0 中的特性总结(三):垃圾回收

    1.内存基础知识 每个进程都有其自己单独的虚拟地址空间. 同一台计算机上的所有进程共享相同的物理内存,如果有页文件,则也共享页文件. 默认情况下,32 位计算机上的每个进程都具有 2 GB 的用户模式 ...

  5. v9手机版文章内容不显示

    方法一: 打开PHPCMS v9的/phpcms/templates/default/wap/show.html页面, 将网页中的{$content}替换为:{$rs['content']} 这样wa ...

  6. Markdown: 编译pdf

    在网上发布博文的时候希望能顺便在本地保存一份记录,这样总结的东西很多的时候就可以写成一本给自己看的小书了.在linux下面有两个选择latex和markdown,虽然latex非常强大,但是很少有博客 ...

  7. docker - 启动container时出现 [warning] : ipv4 forwarding is disabled. networking will not work

    起因 今天在一台新的centos宿主机上安装docker,由于关闭了iptables,在此之后启动container的时候会出现警告: WARNING: IPv4 forwarding is disa ...

  8. mac如何进入应用程序的内部文件夹?

    在程序点击右键,选择显示包内容,就可以看到了

  9. Angular4 后台管理系统搭建(2) - flexgrid 单元格模板 wjFlexGridCellTemplate 的坑

    这几天中了很多坑,尤其是两个大坑.先是运行环境的坑,在是flexgrid单元格内部模板的坑.这里记录下. 一开始我遇见一些很奇怪的问题,按网上的说法,别人这么写代码都正常,就在我机器上不正常.按以前的 ...

  10. [leetcode-532-K-diff Pairs in an Array]

    Given an array of integers and an integer k, you need to find the number of unique k-diff pairs in t ...