JavaSE中线程与并行API框架学习笔记——线程为什么会不安全?

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

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

3.5 线程的分组管理

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

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

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

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

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

1 ThreadGroup threadGroup1 = new ThreadGroup("group1");
2 ThreadGroup threadGroup2 = new ThreadGroup("group2");
3 Thread thread1 = new Thread(threadGroup1, "group1‘s member");
4 Thread thread2 = new Thread(threadGroup2, "group2‘s member");

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

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

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

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

3.5.1 线程群组的异常处理

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

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

 1 /**
 2  * Created by Levenyes on 2017/7/29.
 3  */
 4 public class ThreadGroupDemo {
 5     public static void main(String[] args) {
 6         ThreadGroup tg1 = new ThreadGroup("tg1") {
 7             @Override
 8             public void uncaughtException(Thread t, Throwable e) {
 9                 System.out.printf("%s: %s%n", t.getName(), e.getMessage());
10             }
11         };
12
13         Thread t1 = new Thread(tg1, new Runnable() {
14                 public void run() {
15                     throw new RuntimeException("测试异常");
16                 }
17             }
18         );
19
20         t1.start();
21     }
22 }

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

3.6 为什么线程会不安全?

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

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

 1 import java.util.*;
 2
 3 /**
 4  * 线程不安全实验用例
 5  */
 6 public class ArrayListDemo {
 7     public static void main(String[] args) {
 8         final ArrayList list = new ArrayList ();
 9         Thread t1 = new Thread() {
10             public void run() {
11                 while(true) {
12                     list.add(1);
13                 }
14             }
15         };
16         Thread t2 = new Thread() {
17             public void run() {
18                 while(true) {
19                     list.add(2);
20                 }
21             }
22         };
23         t1.start();
24         t2.start();
25     }
26 }

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

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

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

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

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

然而,如果有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,顾名思义,就是同步的意思。

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

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

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

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

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

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

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

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

1     public void add(Object o) {
2         synchronized (this) {
3             if(next == list.length) {
4                 list = Arrays.copyOf(list, list.length * 2);
5             }
6             list[next++] = o;
7         }
8     }

这个程序片段的意思就是,在线程要执行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——线程是什么?

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

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

时间: 2024-10-21 13:18:35

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

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

前言:虽然工作了三年,但是几乎没有使用到多线程之类的内容.这其实是工作与学习的矛盾.我们在公司上班,很多时候都只是在处理业务代码,很少接触底层技术. 可是你不可能一辈子都写业务代码,而且跳槽之后新单位很可能有更高的技术要求.除了干巴巴地翻书,我们可以通过两个方式来解决这个问题:一是做业余项目,例如在github上传自己的demo,可以实际使用:二是把自己的学习心得写成博客,跟同行们互相交流. 3.1 线程的初窥门径 我们在之前的文章里提到的程序其实都是单线程程序,也就说启动的程序从main()程

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

前言:俗话说“金三银四铜五”,不知道我要在这段时间找工作会不会很艰难.不管了,工作三年之后就当给自己放个暑假. 面试当中Collection(集合)是基础重点.我在网上看了几篇讲Collection的文章,大多都是以罗列记忆点的形式书写的,没有谈论实现细节和逻辑原理.作为个人笔记无可厚非,但是并不利于他人学习.希望能通过这种比较“费劲”的讲解,帮助我自己.也帮助读者们更好地学习Java.掌握Java. 无论你跟我一样需要应聘,还是说在校学生学习Java基础,都对入门和进一步启发学习有所帮助.(关

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

前言:暑期应该开始了,因为小区对面的小学这两天早上都没有像以往那样一到七八点钟就人声喧闹.车水马龙. 前两篇文章介绍了Collection框架的主要接口和常用类,例如List.Set.Queue,和ArrayList.HashSet.LinkedList等等.根据核心框架图,相信我们都已经对Collection这个JavaSE中最常用API之一有一个较为全面的认识. 这个学习过程,还可以推及到其他常用开源框架和公司项目的学习和熟悉上面.借助开发工具或说明文档,先是对项目整体有一个宏观的认识,再根

Java集合框架学习笔记之集合与Collection API

一.CollectionAPI 集合是一系列对象的聚集(Collection).集合在程序设计中是一种重要的数据接口.Java中提供了有关集合的类库称为CollectionAPI. 集合实际上是用一个对象代表一组对象,在集合中的每个对象称为一个元素.在集合中的各个元素的具体类型可以不同,但一般说来,它们都是由相同的类派生出来的(而这一点并不难做到,因为Java中的所有类都是Object的子类).在从集合中检索出各个元素是,常常要根据其具体类型不同而进行相应的强制类型转换. Collection

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

选择Yii 2.0版本框架的7个理由 http://blog.chedushi.com/archives/8988 刚接触Yii谈一下对Yii框架的看法和感受 http://bbs.csdn.net/topics/390807796 更多内容 百度:yii 前端 http://my.oschina.net/u/1472492/blog/221085 摘要 Yii框架学习笔记(二)将html前端模板整合到框架中 原文地址:http://www.ldsun.com/1309.html 上一节成功将Y

MEAN框架学习笔记

MEAN框架学习笔记 MEAN开发框架的资料非常少.基本的资料还是来自于learn.mean.io站点上的介绍. 于是抱着一种零基础学习的心态,在了解的过程中,通过翻译加上理解将MEAN框架一点点消化而且吸收,一步一步来.慢慢地记录我学习MEAN的点点滴滴. 1.MEAN是可以管理用户的 通过MEAN的mean-cli来管理用户.命令是: $ mean user <email> $ mean user <email> --addRole <role>; $ mean u

windows下scrapy框架学习笔记—&#39;scrapy&#39; 不是内部或外部命令

最近几天在深入的学习scrapy框架,但是装完各种需要的基础包之后却发现scrapy命令在别的路径下都用不了,我一开始是把python安装在F:\Python路径下的,安装了scrapy后它默认都会安装在这个路径下,scrapy在路径F:\Python\Scripts路径下,我的scrapy命令只能在此路径下用,因此创建什么工程也都只能在此文件下. 想了一下它的工作原理:它在F:\Python\Scripts路径下,就会在Scripts文件下存在一个scrapy批处理文件,那么在DOS下想要命令

SuperSocket框架学习笔记4-SuperWebSocket---使用SubCommandBase

首先下载所需要的 DLL http://download.csdn.net/detail/wai2dance123/7389285 或者参见第2章  到SuperSocket官网下载 http://www.cnblogs.com/xmcrew/p/3747354.html 1,新建一个 .NET4.0 控制台应用程序 命名为 DiyServers 添加以下引用 将默认的Program.cs改名为  DiyServers.cs 并添加以下命名空间 2,写自己的DSession和DServer继承自

j2ee开发之Spring2.5框架学习笔记

Spring 2.5框架学习笔记 1.是一个开源的控制反转IOC和面向切面AOP的容器框架 2.IOC控制反转 public class PersonServiceBean { private PersonDao personDao = new PersonDao(); publiv void save(Person person){ personDao.save(person); } } 控制反转:应用本身不负责依赖对象personDao的创建以及维护,依赖对象的创建以及维护是由外部容器负责的