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) }}//不需要使用线程安全类了
在保证线程安全的情况下,锁仅保护最小需要保护的代码。
本文内容由小樊整理编辑!