Java并发3:互斥锁(上)


Java并发3:互斥锁(上)

原子性

  • 如何理解原子性?

    原子性的本质其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。

  • 如何解决原子性?

    产生原子性源头问题是线程切换,线程切换依赖CPU中断,那么禁用CPU中断就能禁止线程切换。

    单核CPU场景下,禁止CPU进行中断,那么线程不会切换,保证了同一时刻只有一个线程在执行。

    多核CPU场景下,禁止CPU进行中断,那么线程不会切换,但同一时刻有多个CPU在执行线程,同一时刻有多个线程在执行

    note:解决原子性问题,是要保证中间状态对外不可见。

同一时刻只有一个线程执行,即互斥,这样无论单核还是多核CPU都可以保证原子性。

解决原子性的方案:锁。

  • 简易锁模型

    临界区:一段需要互斥操作的代码,对其在前后进行加锁操作。

简易锁模型

  • 改进锁模型

    资源和锁之间应该有对应关系,好比不能拿你家的锁锁我家的门。

    首先,要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源 R;

    其次,要保护资源 R 需要为它创建一把锁 LR;

    最后,针对这把锁 LR,需在进出临界区时添上加锁操作和解锁操作。

    另外,在锁 LR 和受保护资源之间,图中特地用一条线做了关联,这个关联关系非常重要。

    很多并发 Bug 的出现都是因为把它忽略了,然后就出现了类似锁自家门来保护他家资产的事情,这样的 Bug 非常不好诊断,因为潜意识里认为已经正确加锁了。

改进后的锁模型

synchronized

Java提供synchronized关键字作为一种锁的实现。

  • 用法示例:
class X {
  // 修饰非静态方法
  synchronized void foo() {
    // 临界区
  }
  // 修饰静态方法
  synchronized static void bar() {
    // 临界区
  }
  // 修饰代码块
  Object obj = new Object()void baz() {
    //实例作为锁定资源  
    synchronized(obj) {
      // 临界区
    }
   //Class对象作为锁定资源
    synchronized(Object.class) {
      // 临界区
    }  
  }
}  

Java编译器会在synchronized关键字修饰的方法或代码块上自动执行加锁和解锁动作,这样做的好处是加锁和解锁一定是成对出现。

synchronized修饰静态方法时,默认锁定的是当前类的Class对象;

synchronized修饰非静态方法时,默认锁定的类是当前的实例对象this;

synchronized修饰代码块时,应指定锁定的资源(Class或对象实例)。

  • 代码
public class SafeCalc {
    long value = 0L;

    long get() {
        return value;
    }

    synchronized void addOne() {
        value += 1;
    }

    public static void main(String[] args) throws InterruptedException {
        SafeCalc safeCalc = new SafeCalc();
        Thread t1 = new Thread(new Tc(safeCalc));
        Thread t2 = new Thread(new Tc(safeCalc));
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(safeCalc.get());
    }

    static class Tc implements Runnable {
        private SafeCalc safeCalc;

        public Tc(SafeCalc safeCalc) {
            this.safeCalc = safeCalc;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                safeCalc.addOne();
            }
        }
    }
}

如上代码,加了synchronized后可以完全保证第20行代码打印出来的计算结果一定是200000。

但需要注意的是,由于第4行代码没有加synchronized关键字,因此addOne()方法不Happens-Beforeget()方法。

如果在计算过程中,有其他线程在不断执行get()方法,此时get()方法和addOne()方法是不互斥的,因此不保证每次addOne()方法执行后对get()方法的可见性。

t1t2中执行的addOne()方法之间的可见性和原子性都保证。

如果要保证get()方法和addOne()之间的可见性, 那么get()方法要加上synchronized关键字,或者value变量加volatile关键字。

锁和受保护资源的关系

受保护资源和锁之间的关联关系为N:1的关系

class SafeCalc {
  static long value = 0L;
  synchronized long get() {
    return value;
  }
  synchronized static void addOne() {
    value += 1;
  }
}

如果按上面代码执行,明显可以发现get()addOne()占用的仍然不是一个锁,不存在互斥关系。

临界区的代码是操作受保护资源的路径,要加锁,但不是随便一把锁都能有效。

所以得深入分析锁定的对象和受保护资源的关系,综合考虑受保护资源的访问路径,多方面考量才能用好互斥锁。

思考

下面代码是否正确?

class SafeCalc {
  long value = 0L;
  long get() {
    synchronized (new Object()) {
      return value;
    }
  }
  void addOne() {
    synchronized (new Object()) {
      value += 1;
    }
  }
}

两个方法用的不是同一个锁,多个线程调用方法时,会一直产生新的锁(新的Object实例),不能保证原子性和可见性。

一个合理的受保护资源与锁之间的关联关系应该是 N:1

多把锁保护同一个资源,就像一个厕所坑位,有N多门可以进去,没有丝毫保护效果,管理员一看,还不如把门都撤了,弄成开放式(编译器代码优化)。

——sbwei

别人的总结

加锁本质就是在锁对象的对象头中写入当前线程id,但是new object每次在内存中都是新对象,所以加锁无效。

——nonohony

经过JVM逃逸分析的优化后,这个sync代码直接会被优化掉,所以在运行时该代码块是无锁的。

——w1sl1y

sync锁的对象monitor指针指向一个ObjectMonitor对象,所有线程加入他的entrylist里面,去cas抢锁,更改state加1拿锁,执行完代码,释放锁state减1,和aqs机制差不多,只是所有线程不阻塞,cas抢锁,没有队列,属于非公平锁。
wait的时候,线程进waitset休眠,等待notify唤醒。

——zyl

两把不同的锁,不能保护临界资源。而且这种new出来只在一个地方使用的对象,其它线程不能对它解锁,这个锁会被编译器优化掉。和没有syncronized代码块效果是相同的。

——老杨同志

现实世界里,我们可以用多把锁来保护同一个资源,但在并发领域是不行的。
不能用两把锁锁定同一个资源吗?
如下代码:

>public class X {
 private Object lock1 = new Object();
 private Object lock2 = new Object();
 private int value = 0;

 private void addOne() {
   synchronized (lock1) {
     synchronized (lock2) {
       value += 1;
     }
   }
 }

 private int get() {
   synchronized (lock1) {
     synchronized (lock2) {
       return value;
     }
   }
 }
>}

虽然说这样做没有实际意义,但是也不会导致死锁或者其他不好的结果吧?

——石头剪刀布

note:这种属于lock1保护lock2,lock2保护value。


文章作者: Wendell
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wendell !
评论
  目录