Java并发编程实战 笔记(二) 线程安全性

来源:互联网 发布:编程培训班 编辑:程序博客网 时间:2024/05/19 12:16

第二章:线程安全性

上一篇文章感觉是照抄了书,这样不好,以后要用自己的语言来组织。


1.概况

要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。

什么是对象的状态

非正式意义上,指储存在状态变量(例如实例或静态域)中的数据。对象的状态可能包括其他依赖对象的域。例如,某个HashMap的状态不仅储存在HashMap对象本身,还储存在许多Map.Entry对象中。在对象的状态中包含了任何可能影响其外部可见行为的数据。(无状态的对象一定是线程安全的)

什么是线程的正确性

所见即所知(we know it when we see it)。

什么是线程的安全性

当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。

什么是无状态的

一个类是无状态的,就是说它既不包含任何域,也不包含任何对其他类中域的引用。执行方法过程中产生的临时状态(变量)仅存在于线程栈(线程私有的)上的局部变量中。

什么是竞态条件

当某个计算的正确性取决于多个线程的的交替执行时序时,那么就会发生竞态条件。也就是说,计算的正确性取决于『运气』。

竞态条件的本质

基于一种可能失效的观察结果来做出判断或者执行某个计算。

两种竞态条件

①先检查后执行:当你根据一个你已经确定了的观察结果做出判断后,你所确定的结果又被其他线程改变了,这使得你的观察结果无效了,从而导致各种问题。延迟初始化就是一种先检查后执行的常见情况:

public class LazyInitRace{    private ExpensiveObject instance = null;    public ExpensiveObject getInstance(){        if(instance == null){            instance = new ExpensiveObject();        }        return instance;    }}/*当你把instance==null当做已知条件时,所以你会继续往下运行,new了一个对象,但你却不知道这时你的已知条件可能被其他线程改变了,于是你重复的new了一个对象出来*/

2.Java的主要同步机制

①synchronized,它提供了一种独占的加锁方式
②volatile,它是一种稍弱的同步机制,只确保被修饰变量的可见性(其他线程可以看到该变量的变化),而不能确保该变量的原子性。
③显式锁
④原子变量

synchronized:

Java提供了一种内置的锁机制来支撑原子性:同步代码块(Synchronized Block)。同步代码块包括两部分:①作为锁的对象引用②作为这个锁保护的代码块。

synchronized (lock) {    //访问或修改由锁保护的共享状态    //其中该同步代码块的锁是方法调用所在的对象,静态的synchronized方法以Class对象作为锁。}

注意:
①每个Java对象只有一个内置锁,第一个访问带synchronized的同步块的线程首先获得锁。此时,其他线程,如果执行非synchronized同步块的代码,依然可以执行,如果执行synchronized同步块的代码,则需要竞争锁,被堵塞。

②synchronized不是锁,是同步块。代码里写了两个synchronized不是有两个锁,而是一个对象里有两个同步块。

③并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要。

④当类的某一个不变性条件涉及多个状态变量时,每个变量都必须由同一个锁来保护。

⑤如果一个类的每个方法都是同步方法,例如Vector,也不能确保这些方法的复合操作是原子的:

if(!vector.contains(element))    vector.add(element);//虽然contains和add都是原子方法,但这种『如果不存在则添加』的操作依然存在竞态条件。假设,开始有一个空的vector链表,我们执行两次『如果不存在则添加』操作://一开始线程A执行contains方法,返回false,所以继续执行add方法。但就在线程A刚判断完结果,还没来得及add的这段时间里,线程B也执行了contains方法,也返回false。所以,执行两次『如果不存在则添加』操作,却添加了两个element。

volatile:

其实volatile更能体现的应该是线程的可见性,而不是安全性

Java提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他进程。当把变量声明为volatile类型后,编译器与运行时都会『注意到』这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。

个人理解:
①volatile应该解释为”直接存储原始内存地址”,用volatile修饰的变量,会实时更新寄存器里储存的值,而不是直接复制一个副本储存起来。

②volatile指出该变量是实时可变的,这样就可以保证,多线程下,当一个变量被其他线程修改了,它能更新其值,也就是变量的修改对线程是可见的。

volatile不仅可以提供可见性,在某些情况下还能提供原子性:
比如修饰long、double类型时。因为,Java读取这两种类型,是分两步,先读取前4个字节,所以可能导致其他线程仅读到了前4个字节的值,但是有了volatile就可以让long的值对其他线程实时可见了。

volatile 与 const的区别:
volatiel是防止变量被意想不到地改变。
const是程序不应该试图去修改它。

volatile的使用场合:
先给出一个volatile变量的典型用法:检查某个状态标记以判断是否退出循环。在这个事例中,线程试图通过类似于数绵羊的传统方法进入休眠状态。这里必须使用volatile变量。否则,当asleep被另一个线程修改时,执行判断的线程却发现不了(asleep已经被改变了)。当然也可以用锁来确保asleep更新操作的可见性,但这将使代码更复杂。

volatile boolean asleep;...while(!asleep)    countSomeSheep

volatile变量通常用做某个操作完成、发生中断或者状态的标志。通俗的来说一般用在boolean类型这样的只有一种值的变量上,而不能用在++count这样复合操作的变量上。

总结:
当且仅当满足下列所有条件时,才应该使用volatile变量:
①对变量的写入操作不依赖变量的当前值(boolean类型符合这个要求),或者你能确保只有单个线程更新变量的值(单线程的++count操作符合这个要求)。

②该变量不会与其他状态变量一起纳入不变性条件中(不变性是指一个对象是不可变的,Immutable的。也就是说该变量无论如何变化都不会影响对象的不可变性)

③在访问变量时不需要加锁(访问该对象的get、set方法没有加锁)

0 0
原创粉丝点击