宏升温岭网站建设/百度上海总部
线程安全性
要编写出安全性代码,其核心在于对状态访问(实例或静态域)操作进行管理,特别是共享的和可变的状态访问。
如果当多个线程访问同一个可变的状态变量时候没有合适的同步,那么会出错:
解决:
不在线程之间共享该状态变量;
该状态量修改不可变的变量
在访问状态量时使用同步。
1.理解线程安全性
单线程可以近似定义为:‘所见即所知’
可以这么定义线程安全性:当多个线程访问某个类时,这个类可以始终保持的正确的行为
无状态对象一定是线程安全的。
2.原子性
还记得count++这个案例吗?
priva long count = 0;
public long getCount(){
return count;
}
publuc void service (ServletRequest req,ServletResponse resp){BigInteger i= extractFromRequest(req);BigInterger[] factors = factor(i);++count;encodeIntoResponse(resp,factors);
}
很明显这是非线程安全的,这个类会丢失更新操作。
不要认为++count只是一个操作,但这个操作并非原子的,因而它并不会作为一个不可分割的操作来执行。实际上,它包含了三个独立的操作;
读取count的值,将值加1,然后将计算结果写入count。这是一个“读取-修改-写入”的操作序列。并且结果依赖于之前的状态。
怎么个不安全法:
两个线程在没有同步的情况下同时对一个计数器执行递增操作时发生的情况下。如果计数器的初始值取9,那么在某些情况下,每个线程读到的值都为9,接着执行递增操作,并且都将计数器的值设为10。显然,这不是我们希望看到的情况,如果有一次递增操作丢失了,计数器的值就将偏差1。
这种不恰当的执行时序出现不正确的结果是一种非常重要的情况:竞态条件
最常见的竞态条件类型就是**“先检查后执行”(延迟初始化)操作**,即通过一个可能失效的观测结果来决定下一步的动作
理解:观察结果的失效就是大多数竞争条件的本质—基于一种可能失效的观察结果来做出判断或者执行某个计算。
首先观察到某个条件为真(例如文件x不存在)然后根据这个观察结果采用相应的动作(创建文件x),事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件x),从而导致各种问题(未预期的异常。数据被覆盖、文件被破坏等)
在计数器操作中存在另一种竞态条件:在“读取-修改-写入”这种操作中,基于对象之前的状态来定义对象状态的转换。要递增一个计数器,必须知道它之前的值,并确定在执行更新的过程中没有其他线程会修改过使用这个值(狸猫换太子的意思)
复合操作
如果上面的递增情况是原子操作,竞态条件就不会发生
"先检查后执行"和“读取-修改-写入(例如递增操作)”等操作统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性
解决方案:
参考juc
juc的atomic包中包含了一些原子变量类,用于是现在数值和对象上的原子状态转换。这是java中用于确保原子性的内置机制
后面还有加锁机制
内置锁
相当于互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程A尝试 获取一个由线程B持有的锁,线程A必须等待或者阻塞,知道线程B释放这个锁。如果永远不释放锁,那么A也将永远地等待下去,用synchronized来修饰方法。虽然是安全的。然而,这种方法过于极端,性能不高
内置锁是可重入的,下面这段代码不会发生死锁:
public class Widget{public synchronized void doSomething(){......}
}
public class LoggingWidget extends Widget{public synchronized void doSomething(){super.doSomething();
}
}
用锁来保护状态
只有被多个线程同时访问的可变数据才需要通过锁来保护,并不是所有数据都需要锁的保护
缓存的数值和因数分解结果都由对象的内置锁来保护
安全和性能不可兼得
在使用锁的时候,应该清楚代码块中实现的功能,以及在执行该代码块时是否需要很长的时间。无论是执行计算密集的操作,还是在只执行某个可能阻塞的操作,如果持有锁的时间过长,那么带来安全或性能问题。
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁
后续。。。。。。