什么是线程安全性?

当多个线程访问某个类时,这个类都能表现出正确的行为,那么这个类就是线程安全的。
确保线程安全的三种方式:

  • 不在线程之间共享同一变量
  • 将变量改为不可变
  • 访问变量时加锁

    原子性

  1. public class Demo {
  2. private static int count = 0;
  3. private static void setCount(){
  4. count++;
  5. }
  6. public static void main(String[] args) {
  7. for (int i = 0; i < 10000; i++) {
  8. new Thread(() -> {
  9. setCount();
  10. }).start();
  11. }
  12. try {
  13. Thread.sleep(1000);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(count);
  18. }
  19. }

这段代码输出结果不一定是10000,原因是count++不是原子性操作,实际上,他包含三个独立的操作:

  • 读取count的值
  • count加1
  • 将计算结果赋给count

这是一个”读取-修改-赋值”的操作,每个结果状态都依赖于之前的状态。假如有两个线程在没有同步的情况下同时读取到count的值为100,接着同时执行递增操作,并且都将值设为101,那么其中就有一个线程做了无用功,结果就跟预想值偏差1。这种由于不恰当的执行时序而出现不正确的结果叫:竞态条件。要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或者之后读取和修改状态,而不是在修改状态的过程中。

加锁机制

1.内置锁

java提供了一种内置的锁机制来支持原子性:同步代码块。它包括两部分:1.锁的对象引用,2.由锁保护的代码块。
静态代码块是以Class对象作为锁。

  1. synchronized(lock){
  2. }

同步代码块中的lock可以是任意对象,但是要保证在多线程访问的情况下,使用的lock是同一把锁,这种锁称为内置锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而获取内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

2.重入锁

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。

  1. public class SynFather {
  2. public synchronized void doSomething(){
  3. System.out.println("father doSomething...");
  4. }
  5. public static void main(String[] args) {
  6. SynFather synSon = new SynSon();
  7. synSon.doSomething();
  8. }
  9. }
  10. class SynSon extends SynFather{
  11. @Override
  12. public synchronized void doSomething() {
  13. System.out.println("son doSomething...");
  14. super.doSomething();
  15. }
  16. }
  17. 输出结果:
  18. son doSomething...
  19. father doSomething...

在这段代码中,子类重写了父类的synchronized方法,然后调用父类中的方法,如果synchronized不是可重入锁,那么在调用super.doSomething()时将无法获取SynFather上的锁,因为这个锁已经被SynSon所持有,最终产生死锁。重入锁则避免了死锁的产生。

用锁来保护状态

锁可以使保护的代码以串行的形式来访问,因此可以通过锁来实现对共享状态的独占访问。访问共享状态的复合操作,例如递增操作,如果复合操作的执行过程中持有同一把锁,那么复合操作就成为了原子操作。如果使用同步对某个变量的访问,那么访问这个变量的所有位置都需要使用同步,并且使用的是同一把锁。
不要错误的认为只有写操作时才需要使用同步。
也不要认为所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
如果同步可以避免竞态条件,为何不在每个方法声明时都使用synchronized呢?

  1. if(!vector.contains(element)){
  2. vector.add(element);
  3. }

虽然上述vector是线程安全的类,并且contains方法和add方法都是原子操作,但是整个操作”如果不包含则添加”仍然是复合操作,存在线程安全问题,需要额外的加锁机制,另外将每个方法都加锁还会导致性能问题。

总结

  • 一个对象是否需要是线程安全的,取决于是否被多个线程访问
  • 为确保安全性,对某个变量的操作必须是原子的。
  • 先保证程序的正确执行,再考虑性能,不要为了性能牺牲安全性。