搜索
写经验 领红包

java中的线程安全(java中线程安全问题)

导语:java之关于线程的安全性(二)

java中的线程安全(java中线程安全问题)

上一篇讲到对于单个共享状态可以使用线程安全类来管理,但是如果是多个共享状态,那还是线程安全的吗?

public class UnThredSafeClass implments Servlet {    privite AtomicLong count = new AtomicLong(0);    privite AtomicLong id = new AtomicLong(0);    public  int getCount() {      return count.get();    }    public  int getId() {        return id.get();      }    public void service(ServletRequest req, ServletResponse res) {       int param = (int)req.getParam(&34;)       count.incrementAndGet();  //操作1       id.incrementAndGet();        //操作2      res.write(param)    }}

在这个例子中,count和id两个共享状态变量,均使用线程安全类AutomicLong来管理,在service方法中,两个状态变量都作+1处理。

线程A开始执行,count加1(结果1);线程B获取执行count加1(结果2),继续id加1(结果1);继续线程A,id加1(结果2)。

线程A:count:1 id:2

线程B:count:2 id:1

但是我们需要的是每个线程执行完后两个变量count和id值均相等,由于线程的不确认性,两个加1处理可能是在不同时刻处理的,因此这两步加1操作是不属于原子操作的,所谓原子操作就是,要么同时执行,要么同时不执行,将它看成一步操作。

尽管上例中每个加1处理的方法都是原子的,但是他们无法同时被调用,在两次调用之间,其他线程可能会操作共享状态变量。

现在我们需要将两条代码变为原子操作,就需要加锁。

内置锁

java提供了一种内置锁机制来支持原子性:同步代码块(Synchronized Block),其包含两部分内容:锁的对象引用和锁保护的代码块。

由关键字synchronized修饰方法或代码块。

同步方法:此为内置锁,隐式this锁
public synchronized void doFun(){   //这其中的所有方法操作均属于原子性的}
同步代码块:此为显示锁,锁必须显式申明
public void doFun(){    synchronized(this){       //这其中的所有方法操作均属于原子性的    }}

线程在进入同步方法或者同步代码块的时候会自动获得锁,在退出(执行完毕相关代码或异常退出)同步方法或者同步代码块的时候释放这个锁。

内置锁其实也是一种互斥锁,就是指在同一时刻,最多只有一个线程能够获得这个锁,只有当这个线程释放锁后,其他线程才能获取这个锁,否则其他线程只有等待或者阻塞。

由于锁中的代码每次仅能由一个线程来执行,因此锁内的代码是以原子方式执行的(一次性完全执行完毕,不可分割)。

重入

当一个线程请求其他线程持有的锁时,就会发生线程阻塞。

但是如果一个线程请求它自己持有的锁时,这个请求就会成功,这个就叫锁的“重入”性。

“重入”性可以看出,获取锁的操作是线程,而不是调用。

重入的原理:锁隐性持有一个计数器,其值默认为0,代表的是其不被任何线程持有,当有线程持有这个锁时,其计数器加1,如果是自身再次请求这个锁时,其计数器继续加1,在释放锁的时候也依次递减,直至为0,代表锁被释放。

假设锁不是可重入的,那么,一个子类中的同步方法获取锁后需要调用父类中的同步方法,就会发生无限阻塞下去。

public class Father{  public synchronized doFun(){    ....  }}public class Sun extends Father {  public synchronized doFun(){     super.doFun();  }}

以上代码锁的引用隐式为this,Sun和Father继承关系,在调用子类同步方法块的时候,持有锁this,在调用父类同步方法的时候,需要继续获取this这个锁,如果是不可重入的话,那么父类同步方法的锁this就永远不可获取,产生死锁。

共享状态使用锁来保护(同一个锁)

锁能使其保护的代码以原子方式执行(串行且一次性执行完全),因此将共享状态的操作置于锁中,就能保证代码的线程安全性。

特别要注意一些复合操作:a++这种,延迟初始化(懒汉式单例模式)。

在对共享状态变量进行操作的时候,要注意其锁必须是同一个。

对于共享变量不仅在其修改的时候需要同步,读取的时候也需要同步(同一个锁)。

每个共享和可变的变量都应该只由一个锁来保护,以便于维护。

加锁的约定:将所有可变状态都封装在对象内部,通过对象的内置锁对所有操作可变状态的代码进行同步。

活跃性和性能

将synchronized使用在方法上时,使用的是对象的内置锁,这种方法简单粗暴且粗粒度,但是性能代价比较高。

比如Servlet接口的service方法,如果使用同步方法的话,在多个请求时,service方法就会完全阻塞,也许service方法内部有几百条代码,但是我们需要同步的仅有2条,使用方法锁时,则除开2条需要同步的代码之外的其他代码也同时阻塞,其他代码耗费的时间远大于需要同步的2条代码执行时间,性能上在并发请求数量级越高的时候会越差。

因此我们需要考虑锁的性能,一个锁只保护其所需要保护的对于共享状态和可变变量的操作即可,对于其他无状态的操作代码是不需要使用锁机制的。

因此需要使用显示锁。

改写篇头的例子:

public class UnThredSafeClass implments Servlet {    privite int count = 0;    privite int id =  0;    public void service(ServletRequest req, ServletResponse res) {       int param = (int)req.getParam(&34;)       synchronized(this){  //仅同步最小需要的代码         count++;  //操作1         id++;        //操作2       }      res.write(param)    }}//不需要使用线程安全类了

在保证线程安全的情况下,锁仅保护最小需要保护的代码。

本文内容由小樊整理编辑!