java并发编程读书笔记(1)-- 对象的共享
1. 一些原则
- RIM(Remote Method Invocation):远程方法调用
- Race Condition:竞态条件
- Servlet要满足多个线程的调用,必须是线程安全的
- 远程对象,即通过远程方法调用将对象放入字节流中传给其他jvm的对象,要特别注意对象中的共享状态
- Shared:共享的
- Mutable:可变的
- 当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明晰的不变性规范都能起到一定的帮助作用;
- 无状态对象是线程安全的:没有任何域也不包含任何对其他类中域的引用(比如StatelessFactory implements Servlet),多个线程访问并没有共享状态,不会影响其正确性。
- 最常见的一个静态类型是:先检查后执行(Check-Then-Act)操作,即通过一个可能失效的观测结果来决定下一步的动作。
- 要保持状态的一致性,就需要自单个原子操作中更新所有相关的状态变量
- 内置锁(Intrinsic Lock),监视锁(Monitor Lock):每个对象都可以当做。
- 重入:当某个线程请求一个由其他线程持有的锁时会被阻塞,但请求他自己持有的锁就会成功。内置锁是可重入锁。
- 子类继承了父类,重写了父类的synchronized方法,访问子类的这个方法时要先获得父类的锁,然后获取自身的锁。如果在这个方法里又调用了父类的这个方法(super.xx),可以继续说明,获取的父类锁可以重入。
- 对于每个包含多个变量的不变条件,其中涉及的多有变量都需要由同一个锁来保护
- java内存模型要求变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的64位数值变量(double,long),jvm允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非使用volatile来声明他们或者用锁保护起来。
- 加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有的线程都能看到共享变量的最新值,所有的执行读操作或写操作的线程都必须在同一个锁上同步。
- 把变量声明为volatile类型后,编译与运行时都会注意到这个变量是共享的,因此不会讲该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存到寄存器或者对其他处理器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。
- volatile的一个用法:while的条件变量,为保证可见性。
- 加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
- 发布(Publish)一个对象:是对象能够在当前作用域之外的代码中使用。
- 逸出(Escape):当某个不应发布的对象被发布时。
- 不要再构造过程中使this逸出。
2. 对象的共享
2.1 volatile确保可见性
java内存模型要求变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的64位数值变量(double,long),jvm允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非使用volatile来声明他们或者用锁保护起来。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
把变量声明为volatile类型后,编译与运行时都会注意到这个变量是共享的,因此不会讲该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存到寄存器或者对其他处理器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。
2.2 volatile可见性的应用
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
比如,while循环的条件变量,如果监视是否改变,需要设置为volatile,否则访问while的线程会将asleep放到工作区,一直循环直到某刻去内存读取。
volatile boolean asleep;
...
while(!asleep)
countSomeSheep();
2.3发布与逸出
发布(Publish)一个对象是指:使对象能够在当前作用域之外的代码中使用。
在许多情况,我们要确保对象及其内部状态不被发布。而在某些情况,我又需要发布该对象,但如果在发布时要确保线程安全性,则可能需要同步。发布内部状态可能会破坏封装性,使线程难以维持不变的状态。例如,如果在对象构造完成之前就发布该对象,就会破坏线程安全性。
发布对象的最简单的方法是将对象的引用保存到一个共有的静态变量中。
逸出(Escape):当某个不应该发布的对象呗发布时。
class UnsafeStates{
private String[] states = new String[]{"as","dsf",...};
public String[] getStates(){return states;}
}
如果按上述的方式发布states,则任何调用者都可以修改数组,这是不安全的。
2.4 构造函数中this逸出
上述构造函数好像没啥问题,至少我看本书之前看不出。构造函数中的对象是this,其他譬如引用类属性的拥有者为this,如果将这哥匿名类传递给source,source如果对其进行了引用,而这时候构造函数还没结束即没有创建ThisEscape的对象,这个匿名类也还没构造,即空。当且仅当对象的构造函数返回时,对象才处于一个可预测和一致的状态。
参考http://bruce008.iteye.com/blog/1461345
文章指出,匿名内部类编译的结果表明ThisEscape这个类会作为一个final成员变量放到匿名内部类中,即这个ThisEscape被逃逸出去了。因此不要再构造函数中发布匿名类和起线程。
2.5线程封闭
当某个对象封闭在一个线程中时,这种方法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。
public int loadTheArk(Collection<Animal> candidates){
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null; //animals被封闭在方法中,不要使他们逸出
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for (Animal a:animals) {
if(candidate==null || !candidate.isPotentialMate(a))
candidate = a;
else{
ark.load(new AnimalPair(candidate,a));
++numPairs;
candidate = null;
}
}
return numPairs;
}
如果在线程内部(Within-Thread)上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。但要注意后期维护的时候对象逸出。
2.6.ThreadLocal类
维持线程封闭性的一种更规范方法是使用ThreadLocal。ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。比如,在单线程应用中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个connection对象。由于jdbc的连接对象不一定是线程安全的,因此当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将jdbc的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
public Connection initialValue(){
try {
return DriverManager.getConnection("url","username","password");
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}; public static Connection getConnection(){
return connectionHolder.get();
}
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。
当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。
ThreadLocal变量类似于全局比阿娘,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
2.7. 不变性
满足同步需求的另一种方法是使用不可变对象(Immutable Object)。如果某个对象在被创建后其状态就不能修改,那么这个对象就成为不可变对象。
- 对象创建后其状态就不能修改。final
- 对象的所有域都是final
- 对象是正确创建的(在创建对象期间,this没有逸出)
正如除非需要更高的可见性,否则应将所有的域都声明为私有域是一个良好的习惯,除非需要某个域是可变的,否则应将其声明为final域也是一个良好的编程习惯。
2.8 安全的发布
在某些情况下,我们希望在多个线程间共享对象,此时必须确保安全地进行共享。以下是错误的:
//不安全的发布
public Holder holder;
public void initialize(){
holder = new Holder(22);
}
看起来应该没什么问题才对,实际上多线程访问的时候可能得到一个尚未创建的对象。
再详细些:
public class Holder{
private int n;
public Holder(int n){this.n = n}
public void assertSanity(){
if(n!=n){
throw new AssertionError("This statement is false.");
}
}
}
除了创建hoder的线程,其他线程获取holder的状态是不一定的。也许是创建前获得为null,也许是创建了还没赋值,等等。这是不安全不正确的发布。
要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:
- 在今天初始化函数中初始化一个对象引用。
- 将对象的引用保存到volatile类型的域或者atomicReferance对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中
对象的发布需求取决于它的可变性:
- 不可变对象可以通过任意机制来发布。
- 事实不可变对象(对象从技术上来看是可变的,但其状态在发布后不会再改变)必须通过安全方式来发布
- 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
2.9安全地共享对象
当获得对象的一个引用时,你需要知道在这个引用闪个可以执行哪些操作。在使用它之前手否需要获得一个锁?是否可以修改它的状态,或者只能读取它?许多并发错误都是由于没有理解共享对象的这些“既定规则”而导致的。当发布一个对象时,必须明确地说明对象的访问方式。
在并发程序中使用和共享对象时,可以使用一些使用的策略,包括:
- 线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
- 只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但很任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
- 线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步欧。
- 保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。
java并发编程读书笔记(1)-- 对象的共享的更多相关文章
- Java并发编程读书笔记(一)
----------------------------------------------<Java并发编程实战>读书笔记-------------------------------- ...
- CSAPP 并发编程读书笔记
CSAPP 并发编程笔记 并发和并行 并发:Concurrency,只要时间上重叠就算并发,可以是单处理器交替处理 并行:Parallel,属于并发的一种特殊情况(真子集),多核/多 CPU 同时处理 ...
- 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理
1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...
- Java并发编程学习笔记
Java编程思想,并发编程学习笔记. 一.基本的线程机制 1.定义任务:Runnable接口 线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现R ...
- 【Java并发编程一】线程安全和共享对象
一.什么是线程安全 当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用代码代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的 ...
- Java并发编程实战.笔记十一(非阻塞同步机制)
关于非阻塞算法CAS. 比较并交换CAS:CAS包含了3个操作数---需要读写的内存位置V,进行比较的值A和拟写入的新值B.当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不 ...
- Java 并发编程(二)对象的不变性和安全的公布对象
一.不变性 满足同步需求的还有一种方法是使用不可变对象(Immutable Object). 到眼下为止,我们介绍了很多与原子性和可见性相关的问题,比如得到失效数据.丢失更新操作或光查到某个对象处于不 ...
- java并发编程实践笔记
文章转自:http://kenwublog.com/java-concurrency-in-practise-note 1, 保证线程安全的三种方法 :a, 不要跨线程访问共享变量b, 使共享变量是 ...
- 多线程-java并发编程实战笔记
线程安全性 编写线程安全的代码实质上就是管理对状态的访问,而且通常都是共享的,可变的状态. 一个对象的状态就是他的数据,存储在状态变量中,比如实例域或静态域.所谓共享是指一个对象可以被多个线程访问:所 ...
随机推荐
- Apache Lucene 4.5 发布,Java 搜索引擎
Apache Lucene 4.5 发布了,该版本提供基于磁盘的文档值以及改进了过滤器的缓存.Lucene 4.5 的文档请看这里. Lucene 是apache软件基金会一个开放源代码的全文检索引擎 ...
- 高性能网站架构设计之缓存篇(5)- Redis 集群(上)
集群技术是构建高性能网站架构的重要手段,试想在网站承受高并发访问压力的同时,还需要从海量数据中查询出满足条件的数据,并快速响应,我们必然想到的是将数据进行切片,把数据根据某种规则放入多个不同的服务器节 ...
- javascript 设计模式-----享元模式
四个轮子,一个方向盘,有刹车,油门,车窗,这些词首先让人联想到的就是一辆汽车.的确,这些都是是一辆车的最基本特征,或者是属性,我们把词语抽象出来,而听到这些词语的人把他们想象陈一辆汽车.在代码里面也是 ...
- 【转】yahoo前端优化军规
雅虎给出了前端优化的34条法则(包括Yslow规则22条) 详细说明,下载转发 ponytail 的译文(来自帕兰映像). Minimize HTTP Requests 减少http请求 图片.css ...
- UWP开发随笔——UWP新控件!AutoSuggestBox!
摘要 要开发一款优秀的application,控件肯定是必不可少的,uwp就为开发者提供了各种各样的系统控件,AutoSuggestBox就是uwp极具特色的控件之一,也是相对于之前win8.1的ua ...
- Nim教程【十一】
引用类型和指针类型 不同的引用可以只想和修改相同的内存单元 在nim中有两种引用方式,一种是追踪引用,另一种是非追踪引用 非追踪引用也就是指针,指向手动在内存中分配的对象: 追踪引用指向一个垃圾收集的 ...
- 用DirectX实现魔方(二)
这篇说一下如何构造魔方,主要包括魔方几何体的构造及纹理贴图.以下论述皆以三阶魔方为例,三阶魔方共有3 x 3 x 3 = 27个小立方体. 构造魔方 在第一篇里面说过,最初模型用的是微软的.x文件格式 ...
- Windows Azure Virtual Machine (25) 使用SSH登录Azure Linux虚拟机
<Windows Azure Platform 系列文章目录> 本文介绍内容适合于Azure Global和Azure China 为什么使用SSH登录Azure Linux虚拟机? 我们 ...
- ios 使用UINavagationController时,push,pop方法执行的一些方法
(一)当创建某一个UIViewController B,并push到ViewController B时,B执行方方法的顺序如下: 1>viewDidLoad2>initWithNibNam ...
- js笔记——js里var与变量提升
var是否可以省略 一般情况下,是可以省略var的,但有两点值得注意: 1.var a=1 与 a=1 ,这两条语句一般情况下作用是一样的.但是前者不能用delete删除.不过,绝大多数情况下,这种差 ...