我的博客地址
要编写线程安全的代码,其核心是在于对共享的和可变的状态的访问。
- 共享:变量可以被多个线程同时访问
- 可变:变量的值在其生命周期内可以发生变化
在多个线程访问摸个状态变量,其中有一个线程执行写操作,那么这时就需要采取措施来协同这些线程对这个变量的访问。Java中主要的同步机制就是使用关键字synchronized,它提供了一种独占的加锁方式。其他的方式还包括:volatile类型的变量,显式锁,已经原子变量。
一开始设计个线程安全的类比以后再去修改这个类为线程安全要容易的多。
在一些过程式编程语言中编写线程安全的类是比较困难和复杂的,但使用面向对象这种编程思想(封装性)会容易写出线程安全的类。
ps:深刻理解面向对象的编程思想,写出面向对象的程序是很有必要的
但有些时候业务需求会使得你牺牲一些良好的设计原则,以换取性能和向后兼容,面向对象中的抽象和封装会降低程序的性能(这里不是很理解???)。在编写并发程序的时候的一种正确的做法就是:首先让代码正确运行,在提高代码速度。
ps:在必须要提高性能的时候在去做优化。
什么是线程安全的
线程安全(线程安全性)的定义:当多个线程访问某个类的时候,这个类中都能表现出正确的行为。
一个无状态的对象一定是线程安全的。
eg:Servlet里:
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger [] factors = factor(i);
encodeIntroResponse(req,factors);
}
}
原子性
就是不会被分割。
我们在要写线程安全的一个操作时候,要去判断这个操作是否是原子性的。例如一些count++的操作。
-
竞态条件
在并发编程中,由于不恰当的执行时序而出现不正确的结果。我们把这种情况叫做竞态条件。也就是说要获得正常的结果要取决事件执行的顺序。 -
延迟初始化中的竞态条件
这个的例子就是类似于单例设计模式中懒汉式容易出现的问题。
private Instancea a = null;
public Instance getInstance(){
if(a == null){
a = new Instance();
}
return a;
}
-
复合操作
像 count++这种读取-修改-写人的操作,和上面说的延迟初始化的操作其实都是多个原子操作组合在一起的,那么我们要保证线程安全的话,一定要将这写操作也以原子性来执行。这一样的操作叫做复合操作。(通过加锁机制可以很好的保证线程的安全性)
加锁机制
在Servlet中,我们可以通过线程安全的对象来表示Servlet的状态。但要是在一个Servlet中添加了很多种状态,那么是否只需要我们把这些状态变成线程安全的就足够了吗?
答案肯定是否定的。当这两个属性之间有某种关系,这些属性状态不是独立存在的。这时候就不能依靠使每个属性的操作是原子性来确保线程安全。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量
那如何让这种复杂的操作来保持原子性呢?java提供了一种内置的锁机制:
内置锁
synchronized关键字来实现内置锁。
关于synchronized的使用,可以点击这里
总结下:
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
- 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
- 修饰一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象
- 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象
但是偶用这种锁的时候要注意不要过于极端,因为使用这种锁只允许获得锁的线程来执行,其他获取不到的只能等待。那么这个效率是很低的。
值得一提的是用Synchronized是一个可重入锁。就是锁的对象可以被重复获取。
重入锁:就是线程可以获得一个由它自己持有的锁。多用于子类父类、迭代中
ps:重入的粒度是线程而不是调用
用锁来保护状态
锁可以使的代码得以串行执行。
访问共享状态的复合操作,一定要持有一个锁(切记必须是相同的对象),不能是不同的锁。并且对于这个共享状态的所有位置都要加这个锁,包括读和写的地方。不能只在写的地方加。
对于一些线程安全的类,并不是说使用了这个线程安全的类了就是线程安全的,它的方法是安全的,但是这些方法放到一起就不一定安全的了。
活跃性与性能
滥用Synchronized,会使得粒度很粗,为线程的安全性的代价就很高。我们已改湿度的缩小同步代码块的作用范围。在这两个之间要保持一个度。