JavaSE中线程与并行API框架学习笔记——线程为什么会不安全?
前言:休整一个多月之后,终于开始投简历了。这段时间休息了一阵子,又病了几天,真正用来复习准备的时间其实并不多。说实话,心里不是非常有底气。
这可能是学生时代遗留的思维惯性——总想着做好万全准备才去做事。当然,在学校里考试之前当然要把所有内容学一遍和复习一遍。但是,到了社会里做事,很多时候都是边做边学。应聘如此,工作如此,很多的挑战都是如此。没办法,硬着头皮上吧。
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中线程与并行API框架学习笔记1——线程是什么?
如果你喜欢我的文章,可以扫描关注我的个人公众号“李文业的思考笔记”。
不定期地会推送我的原创思考文章。

JavaSE中线程与并行API框架学习笔记——线程为什么会不安全?的更多相关文章
- JavaSE中线程与并行API框架学习笔记1——线程是什么?
前言:虽然工作了三年,但是几乎没有使用到多线程之类的内容.这其实是工作与学习的矛盾.我们在公司上班,很多时候都只是在处理业务代码,很少接触底层技术. 可是你不可能一辈子都写业务代码,而且跳槽之后新单位 ...
- java JDK8 学习笔记——第11章 线程和并行API
第11章 线程与并行API 11.1 线程 11.1.1 线程 在java中,如果想在main()以外独立设计流程,可以撰写类操作java.lang.Runnable接口,流程的进入点是操作在run( ...
- JavaSE中Collection集合框架学习笔记(2)——拒绝重复内容的Set和支持队列操作的Queue
前言:俗话说“金三银四铜五”,不知道我要在这段时间找工作会不会很艰难.不管了,工作三年之后就当给自己放个暑假. 面试当中Collection(集合)是基础重点.我在网上看了几篇讲Collection的 ...
- JavaSE中Collection集合框架学习笔记(3)——遍历对象的Iterator和收集对象后的排序
前言:暑期应该开始了,因为小区对面的小学这两天早上都没有像以往那样一到七八点钟就人声喧闹.车水马龙. 前两篇文章介绍了Collection框架的主要接口和常用类,例如List.Set.Queue,和A ...
- JavaSE中Map框架学习笔记
前言:最近几天都在生病,退烧之后身体虚弱.头疼.在床上躺了几天,什么事情都干不了.接下来这段时间,要好好加快进度才好. 前面用了三篇文章的篇幅学习了Collection框架的相关内容,而Map框架相对 ...
- Yii框架学习笔记(二)将html前端模板整合到框架中
选择Yii 2.0版本框架的7个理由 http://blog.chedushi.com/archives/8988 刚接触Yii谈一下对Yii框架的看法和感受 http://bbs.csdn.net/ ...
- phalcon(费尔康)框架学习笔记
phalcon(费尔康)框架学习笔记 http://www.qixing318.com/article/phalcon-framework-to-study-notes.html 目录结构 pha ...
- MEAN框架学习笔记
MEAN框架学习笔记 MEAN开发框架的资料非常少.基本的资料还是来自于learn.mean.io站点上的介绍. 于是抱着一种零基础学习的心态,在了解的过程中,通过翻译加上理解将MEAN框架一点点消化 ...
- Java基础及JavaWEB以及SSM框架学习笔记Xmind版
Java基础及JavaWEB以及SSM框架学习笔记Xmind版 转行做程序员也1年多了,最近开始整理以前学习过程中记录的笔记,以及一些容易犯错的内容.现在分享给网友们.笔记共三部分. JavaSE 目 ...
随机推荐
- 自定义分布式RESTful API鉴权机制
微软利用OAuth2为RESTful API提供了完整的鉴权机制,但是可能微软保姆做的太完整了,在这个机制中指定了数据持久化的方法是用EF,而且对于用户.权限等已经进行了封装,对于系统中已经有了自己的 ...
- openfire极限优化
日志优化 默认是 用info 级别,最好不用openfire原生的打日志方式. 离线消息用存储不打回方式,不要用打回方式 xmpp.offline.type=store_and_drop ...
- 使用 Socket.IO 开发聊天室
前言 Socket.IO 是一个用来实现实时双向通信的框架,其本质是基于 WebSocket 技术. 我们首先来聊聊 WebSocket 技术,先设想这么一个场景: · 用户小A,打开了某个网站的充值 ...
- Linux网络服务10——远程访问及控制
Linux网络服务10--远程访问及控制 一.SSH概述 1.SSH简介 SSH(Secure Shell)是一种安全通道协议,主要用来实现字符界面的远程登录.远程复制等功能.SSH协议对通信双方的数 ...
- php使用openssl进行数字签名验证
<?php /** * Created by PhpStorm. * User: hanks * Date: 6/2/2017 * Time: 6:03 PM */ /* [数字签名] 使用完全 ...
- 详解Mysql自动备份与恢复
通过 mysqldump命令,直接生成一个完整的 .sql 文件 Step 1: 创建一个批处理备份SQL c: cd C:Program Filesmysql5.6.24bin mysqldump ...
- NPOI 生成 excel基本设置
//设置页眉页脚 tempSheet.Header.Center = "2017-04-27"; tempSheet.Footer.Center = "√" + ...
- input标签在只允许输入数字的时候添加的代码
oninput="this.value=this.value.replace(/\D/g, '')"
- vijos1051题解
题目: 圣诞老人回到了北极圣诞区,已经快到12点了.也就是说极光表演要开始了.这里的极光不是极地特有的自然极光景象.而是圣诞老人主持的人造极光. 轰隆隆--烟花响起(来自中国的浏阳花炮之乡).接下来就 ...
- 解决CentOS7中文乱码(包括Tomcat日志乱码)问题
Linux系统中文语言乱码,是很多小伙伴在开始接触Linux时经常遇到的问题,而且当我们将已在Wndows部署好的项目搬到Linux上运行,Tomcat的输出日志中文全为乱码(在Windows上正常) ...