您的位置:首页 > 编程语言 > Java开发

JAVA并发编程之线程安全性

2016-10-25 14:24 183 查看

线程安全性

在编写并发编程时,必须正确的使用线程和锁,但这只是一种机制,要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是共享和可变的状态的访问。

当JAVA线程访问某个状态变量并且其中一个线程执行写操作的时候,必须采用协同机制来协同这些线程对变量的访问,JAVA中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但同步的这个术语还包含valatale类型的变量,显示锁( Explicit Lock)以及原子变量。

1、什么是线程安全性

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

无状态对象一定是线程安全的,即所有变量都为局部变量



例如在上图中,因为存在状态变量,count,当多个线程访问时可能会出项重复读取count,然后count++后,两个线程只将count加了一次1,出现记录次数少的情况。在并发编程中,这种由于不恰当的执行时序而出现不正确的执行结果,我们把它称作竟态条件(Race Condition)

2、静态条件

在非线程安全的类中,如果出现多个静态条件,从而使结果变得不够可靠,计算的正确性取决于多个线程执行的时序,当多个线程串行时那么得到正确结果,但是当过个线程并行时,就会出现上图的重复读取。这种最常见的静态条件就是“先检查,在执行。”即一个可能是失效的观察结果,当做条件来决定下一步动作。

延迟初始化:

使用“先检查,后执行。”的一种常见的情况就是延迟初始化,延迟初始化的目的是将对象的初始化操作,延迟到实际使用时在初始化,同时要确保只初始化一次。



例如在上图,我们采用延迟初始化(参考懒汉模式),在上述代码中包含一个静态条件(instance),当线程A与线程B同时执行getInstancce时,返回的实例取决于A&B的执行效率,但是两个线程返回的不是同一个实例。

与大多数并发错误一样,静态条件并不总是发生错误,还需要某种不恰当的执行时序。

3、符合操作

在上述两种静态条件中,懒加载以及计数器两个程序中,都需要一组原子性的操作。要避免这种情况,就必须在某个线程修改该变量的同时,通过某种方式防止其他线程使用这个变量,从而确保其他线程在修改操作完成之前或者之后读取和修改状态,而不是在修改状态的过程中。

++i  为复合操作,先读取i,然后将i+1,然后再将i+1赋值给i。


我们通过加锁的方式来改变技术器



在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态的装换,通过AtomicLong类型来代替计数器的Long类型,能够确保所有对计数器的访问都是原子性的操作。因此上述代码中的servlet计数器为线程安全的。

4、加锁机制

内置锁

JAVA中提过一种内置的锁来支持原子性,同步代码块Synchronized Block)。同步代码块包含两个部分,一个为锁对象的引用,一个为这个锁保护的代码块。以synchronized来修饰方法就是一中横跨整个方法体的同步代码块,其中同步代码块的锁就是方法调用所在的对象,静态的 synchronize方法是以Class对象为锁。

synchronize (lock){
//访问或者修改锁保护的共享状态
}


每个JAVA对象都可以一个用作实现同步的锁,这些锁被称为内置锁( Intrinsic Lock)或者监视器锁(Monitor Lock)

线程在进入同步代码块之前会自动获取锁,并且在退去同步代码时自动释放锁(无论是通过代码块退出,还是抛出异常退出),获得内置锁的唯一途径就是进入这个由锁保护的同步代码块或者方法。

JAVA的内置所相当于互斥锁,这意味着只有一个线程可以进入到同步代码块,因此这个方法过于极端,程序性能非常低。

5、重入

当某个线程请求一个由其他线程持有锁时,那么请求线程就会进入阻塞状态。然而,内置锁是可重入的,因此如果某个线程试图获取一个已经由他自己持有的锁,那么这个请求就会成功。 重入意味着获取锁的操作的粒度是线程,而不是调用。

重入的实现方法

重入的一种实现方法是,为每个锁关联一个计数值和一个所有者线程。当计数器为0时,这个锁没有任何线程持有,如果同一个线程获取这个锁,那么计数器+1,当线程退出同步代码块时,计数器-1,计数器为0时,这个锁释放。



6、用锁来保护状态

锁使其保护的同步代码块,线程以串行的方式访问,因此可以通过锁来实现一些协议,来实现对共享状态的独占访问。

常见加锁约定

一种常见的加锁约定就是将所有可变状态都封装在对象内部,并通过对象的内置锁对所有可变状态的访问进行同步,是的在该对象不会发生并发访问,例如Vector和其他同步集合类。

7、活跃性与性能

当我们对Servlet中的service方法添加锁时,虽然能够保证计数器的正确性,但是对整个方法进行加锁,导致性能下降,service编程串行执行,背离了servlet 的设计初衷,然而幸运的是我们可以通过缩小同步代码块来实现高并发的设计,同时又保证了线程的安全性。

当使用锁时,我们应该清楚同步代码块中实现的功能,以及执行该代码块是否需要很长的时间,如果过长时间持有锁,那么就会带来活跃性以及性能的问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: