Java并发编程的艺术(十二)——线程安全
1. 什么是『线程安全』?
如果一个对象构造完成后,调用者无需额外的操作,就可以在多线程环境下随意地使用,并且不发生错误,那么这个对象就是线程安全的。
2. 线程安全的几种程度
线程安全性的前提:对『线程安全性』的讨论必须建立在对象内部存在共享变量这一前提,若对象在多条线程间没有共享数据,那这个对象一定是线程安全的!
2.1. 绝对的线程安全
上述线程安全性的定义即为绝对线程安全的情况,即:一个对象在构造完之后,调用者无需任何额外的操作,就可以在多线程环境下随意使用。
绝对的线程安全是一种理想的状态,若要达到这一状态,往往需要付出巨大的代价。
通常并不需要达到绝对的线程安全。
2.2. 相对的线程安全
我们通常所说的『线程安全』即为『相对的线程安全』,JDK中标注为线程安全的类通常就是『相对的线程安全』,如:Vector、HashTable、Collections.synchronizedXXX。
对于相对线程安全的类,使用它们时一般不需要使用额外的保障措施,但对于一些特定的使用场景,仍然需要额外的操作来保证线程安全,如:
// 读线程
Thread t1 = new Thread( new Runnable(){
public void run(){
for(int i=0; i<vector.size(); i++){
System.out.println( vector.get(i) );
}
}
}).start();
// 写线程
Thread t2 = new Thread( new Runnable(){
public void run(){
for(int i=0; i<vector.size(); i++){
vector.remove(i);
}
}
}).start();
vector是一个线程安全的容器,它所提供的方法均为同步方法,但上述代码仍然会出现线程安全性问题:
若线程1读了一半的元素后暂停,线程2开始执行,并删除了所有的元素,然后线程1继续执行,此时发生角标越界异常!
修改方案:加上额外的同步
// 读线程
Thread t1 = new Thread( new Runnable(){
public void run(){
synchronized( vector ){
for(int i=0; i<vector.size(); i++){
System.out.println( vector.get(i) );
}
}
}
}).start();
// 写线程
Thread t2 = new Thread( new Runnable(){
public void run(){
synchronized( vector ){
for(int i=0; i<vector.size(); i++){
vector.remove(i);
}
}
}
}).start();
2.3. 线程对立
线程对立指的是:不论调用者采用何种同步措施,都无法达到线程安全的目的。
如Thread类的suspend、resume方法就是线程对立的方法。
suspend方法会暂停线程,但它不会释放资源,若resume需要请求到该资源才会被运行的话,系统就会进入死锁状态。
3. 实现线程安全的方法
3.1. 互斥同步
同步指的是同一时刻,只有一条线程操作『共享变量』。
实现同步的方式有很多:互斥访问、CAS操作。
互斥会引起阻塞,当一条线程请求一个已经被另一线程使用的锁时,就会进入阻塞态;而进入阻塞态会涉及上下文切换。因此,使用互斥来实现同步的开销是很大的。
互斥同步(阻塞式同步)是一种『悲观锁』,即它认为总是存在多条线程竞争资源的情况,因此它不管当前是不是真的有多条线程在竞争共享资源,它总是先上锁,然后再处理。
Java中有两种实现互斥同步的方式:synchronized和ReentrantLock。
- synchronized
- 编译器会在synchronized同步块的开始和结束位置加上monitorenter和monitorexit指令;
- 这两个指令需要一个reference类型的参数来指名要锁定和解锁的对象;
- 若同步块没有明确指定锁对象,那么就使用当前对象或当前类的Class对象;
- 它是一把可重入的锁,即:当前线程在已经获得锁的情况下,可以再次获取该锁,因此不会出现当前线程把自己锁死的情况;
- ReentrantLock
它也是一把可重入的锁,但比synchronized多如下功能:- 等待可中断:若一条线程长时间占用锁不释放,那被阻塞的线程可以选择放弃等待,而去做别的事;这对于要处理长时间的同步块时是很有帮助的。
- 可实现公平锁:synchronized是一种非公平锁,即:被阻塞的线程竞争锁是随机的;而公平锁是根据被阻塞线程先来后到的顺序给予锁。ReentrantLock默认是非公平锁,可以通过构造函数构造公平锁。
- 可以绑定多个条件:synchronized可使用wait/notify来实现等待/通知机制,但一个synchronized同步块只能使用一次,若要使用多次,就需要嵌套同步块;但ReentrantLock可以通过newCondition创建多个条件。
synchronized和ReentrantLock如何选择?
优先选择synchronized!
JDK1.6已经对synchronized做了很多优化,性能与ReentrantLock相差不大。在条件允许的请况下应优先选择synchronized。
3.2. 非阻塞同步
它是一种『乐观锁』,即它总是认为当前没有线程使用共享资源,因此它不管当前的状态,直接操作共享资源,若发现产生了冲突,那么再采取补偿措施(如:CAS的补偿措施就是不断尝试,直到不发生冲突为止),这种方式线程无需进入阻塞态(挂起态),因此称为『非阻塞同步』。
JUC中各种整形原子类的自增、自减等操作就使用了CAS。
CAS操作过程:CAS操作存在3个值:共享变量V、预期的旧值A、新值B,若V与A相同,则将V更新成B,否则就不更新,继续循环比较,直到更新完成为止。
CAS操作可能引发的问题:ABA问题。
若V一开始的值为A,但在准备赋新值的过程中A变成了B,又变成了A,而CAS操作误认为V没有被改过。
无同步方案
『阻塞式同步』和『非阻塞式同步』都是同一时刻只让一条线程处理共享数据,而下面的方案使得多条线程之间不存在共享数据,从而无需同步。
可重入代码
如果一块代码段只要输入的值一样其结果就一样的话,这段代码就叫『可重入代码』。
这一类代码天生具有线程安全性,线程随意切换结果都一样。线程封闭
线程封闭:把所有涉及共享变量操作的任务都放在一个线程中运行。
这样就不存在多条线程同时处理共享变量了,从而达到了线程安全目的。
WEB服务器采用的就是这种方式,它把每个请求封装在一条线程中处理,从而不存在线程安全性问题。
- 不可变对象
如果是共享的基本数据类型变量,只要被final修饰,它就是不可变的;
如果是共享的对象,那就要确保它内部的共享成员变量不会被它的行为所改变。
PS:保证对象内部共享变量不会被改变的方法有很多,最简单粗暴的方式就是将所有共享变量用final修饰。
不可变对象一定是线程安全的。
Java并发编程的艺术(十二)——线程安全的更多相关文章
- Java并发编程的艺术(十二)——并发容器和框架
ConcurrentHashMap 为什么需要ConcurrentHashMap HashMap线程不安全,因为HashMap的Entry是以链表的形式存储的,如果多线程操作可能会形成环,那样就会死循 ...
- Java并发编程的艺术(二)——volatile、原子性
什么是volatile Java语言允许线程访问共享变量,为了确保共享变量能够被准确一致地更新,如果一个字段被声明为volatile,那么Java内存模型将会确保所有线程看到这个变量时值是一致的.保证 ...
- 转:【Java并发编程】之十二:线程间通信中notifyAll造成的早期通知问题(含代码)
转载请注明出处:http://blog.csdn.net/ns_code/article/details/17229601 如果线程在等待时接到通知,但线程等待的条件还不满足,此时,线程接到的就是早期 ...
- 【Java并发编程】之十二:线程间通信中notifyAll造成的早期通知问题
如果线程在等待时接到通知,但线程等待的条件还不满足,此时,线程接到的就是早期通知,如果条件满足的时间很短,但很快又改变了,而变得不再满足,这时也将发生早期通知.这种现象听起来很奇怪,下面通过一个示例程 ...
- Java并发编程的艺术(十)——线程池(1)
线程池的作用 减少资源的开销 减少了每次创建线程.销毁线程的开销. 提高响应速度 每次请求到来时,由于线程的创建已经完成,故可以直接执行任务,因此提高了响应速度. 提高线程的可管理性 线程是一种稀缺资 ...
- Java并发编程的艺术(十)——线程池
线程池的作用 降低资源消耗.重复利用已有线程,减少线程的创建和销毁造成的消耗. 提高响应速度.当有任务需要处理的时候,就不用再花费重新创建线程的时间了. 提高线程的可管理性.不合理利用线程,会浪费资源 ...
- Java并发编程系列之三十二:丢失的信号
这里的丢失的信号是指线程必须等待一个已经为真的条件,在開始等待之前没有检查等待条件.这样的场景事实上挺好理解,假设一边烧水,一边看电视,那么在水烧开的时候.由于太投入而没有注意到水被烧开. 丢失的信号 ...
- java并发编程JUC第十二篇:AtomicInteger原子整型
AtomicInteger 类底层存储一个int值,并提供方法对该int值进行原子操作.AtomicInteger 作为java.util.concurrent.atomic包的一部分,从Java 1 ...
- java并发编程的艺术(二)---重排序与volatile、final关键字
本文来源于翁舒航的博客,点击即可跳转原文观看!!!(被转载或者拷贝走的内容可能缺失图片.视频等原文的内容) 若网站将链接屏蔽,可直接拷贝原文链接到地址栏跳转观看,原文链接:https://www.cn ...
随机推荐
- Java编程的逻辑 (26) - 剖析包装类 (上)
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- hdu 5003 模拟水题 (2014鞍山网赛G题)
你的一系列得分 先降序排列 再按0.95^(i-1)*ai 这个公式计算你的每一个得分 最后求和 Sample Input12530 478Sample Output984.1000000000 # ...
- K8S Dashborad登陆认证文档
K8S Dashboard是官方的一个基于WEB的用户界面,专门用来管理K8S集群,并可展示集群的状态.因为我们使用kubeadm搭建的集群会默认开启RABC(角色访问控制机制),所以我们必须要进行额 ...
- ps -aux与ps -ef
ps -aux与ps -ef这两个命令显示的结果是差不多的. 不同之处就是显示风格不同,前者是BSD风格,后者SYSTEM V(其实我不太明白这尼玛风格是什么跟什么,我看起来都差不多啊) 然后重要的不 ...
- ref:Spring JDBC框架
ref:https://blog.csdn.net/u011054333/article/details/54772491 Spring JDBC简介 先来看看一个JDBC的例子.我们可以看到为了执行 ...
- 简单实现一个EventEmiter
在前端开发中,“发布-订阅”也是“观察者模式”是一种常用的设计模式:之前对设计模式没有过深的认识,直到前段时间在封装一个运用AngularJS封装table组件时,遇到一个难题,那就是AngularJ ...
- CentOS7下安装MySQL5.7安装与配置(YUM)
http://blog.csdn.net/xyang81/article/details/51759200 安装环境:CentOS7 64位 MINI版,安装MySQL5.7 1.配置YUM源 在My ...
- java 使用grpc步骤
1.配置grpc maven依赖 <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-ne ...
- 【BZOJ 1061】 1061: [Noi2008]志愿者招募 (线性规划与网络流)**
1061: [Noi2008]志愿者招募 Description 申奥成功后,布布经过不懈努力,终于成为奥组委下属公司人力资源部门的主管.布布刚上任就遇到了一个难 题:为即将启动的奥运新项目招募一批短 ...
- android 进程间通信 messenger 是什么 binder 跟 aidl 区别 intent 进程间 通讯? android 消息机制 进程间 android 进程间 可以用 handler么 messenger 与 handler 机制 messenger 机制 是不是 就是 handler 机制 或 , 是不是就是 消息机制 android messenge
韩梦飞沙 韩亚飞 313134555@qq.com yue31313 han_meng_fei_sha messenger 是什么 binder 跟 aidl 区别 intent 进程间 通讯 ...