多线程编程核心技术(四)互斥锁:解决原子性问题

上一个文章已经介绍出了线程切换导致无法实现原子性。那么一个问题:单核CPU是否有原子性问题,是依然有安全性问题的。

public class OneCpuCoreTest implements Runnable {

    private static int count;

    @Override
    public void run() {
        int idx = 0;
        while (idx++ < 100000) {
            count += 1;
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new OneCpuCoreTest());
        Thread thread2 = new Thread(new OneCpuCoreTest());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

 可以上自己的学生服务器上去试一试。

“同一时刻只有一个线程执行”这个条件非常重要,我们称之为互斥。如果我们能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了。

重点就是保持“同一时刻只有一个线程执行”。

 

 

 进行操作之前先进行加锁,就保证了只有一个线程可以对这一部分代码进行处理。相对而且权限比较低的就是synchronized

当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;当修饰非静态方法的时候,锁定的是当前实例对象 this。像最上面的代码只需要修改成这样就可以

public class OneCpuCoreTest implements Runnable {

    private static Integer count=new Integer(0);

    @Override
    public  void run() {
        synchronized (count){
            int idx = 0;
            while (idx++ < 100000) {
                count += 1;
            }
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new OneCpuCoreTest());
        Thread thread2 = new Thread(new OneCpuCoreTest());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

 但是如果是在方法(run方法)上加synchronized是没什么作用的,原因就是加在方法上的锁是锁当前的this对象。而在下面的主方法中是new了两个对象,虽然所以依然都可以进去。如果要在方法上修改的话,需求这样做。

public class OneCpuCoreTest implements Runnable {

    private static Integer count=new Integer(0);

    @Override
    public synchronized void run() {
            int idx = 0;
            while (idx++ < 100000) {
                count += 1;
        }

    }

    public static void main(String[] args) throws InterruptedException {
        OneCpuCoreTest test = new OneCpuCoreTest();
        Thread thread1 = new Thread(test);
        Thread thread2 = new Thread(test);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

  为什么synchronized修饰了方法,最后另外的线程会知道最新的值呢?可以从happend-before方法看出,解锁总是在加锁之前,所以另外一个线程要知道已经解锁了,就会根据传递性知道最新值。synchronized也被称为管程

互斥锁,在并发领域的知名度极高,只要有了并发问题,大家首先容易想到的就是加锁,因为大家都知道,加锁能够保证执行临界区代码的互斥性。这样理解虽然正确,但是却不能够指导你真正用好互斥锁。临界区的代码是操作受保护资源的路径,类似于球场的入口,入口一定要检票,也就是要加锁,但不是随便一把锁都能有效。所以必须深入分析锁定的对象和受保护资源的关系,综合考虑受保护资源的访问路径,多方面考量才能用好互斥锁。synchronized 是 Java 在语言层面提供的互斥原语,其实 Java 里面还有很多其他类型的锁,但作为互斥锁,原理都是相通的:锁,一定有一个要锁定的对象,至于这个锁定的对象要保护的资源以及在哪里加锁 / 解锁,就属于设计层面的事情了。

 

然后就是加锁本质就是在锁对象的对象头中写入当前线程id这个是原理一定要记得。new对象的要注意,下面的代码就是无效的。另外JVM逃逸分析的优化后,这个sync代码直接会被优化掉

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

  

 

如果说N个资源只对应了一把锁,就会有一定的问题,当然不是安全问题,是线程性能问题。例如下面这个代码,就会导致同一时间只可以进行一个方法,存钱了就不能查。

class Account {
  // 锁:保护账户余额
  private final Object balLock
    = new Object();
  // 账户余额  
  private Integer balance;
  // 锁:保护账户密码
  private final Object pwLock
    = new Object();
  // 账户密码
  private String password;

  // 取款
  void withdraw(Integer amt) {
    synchronized(balLock) {
      if (this.balance > amt){
        this.balance -= amt;
      }
    }
  } 
  // 查看余额
  Integer getBalance() {
    synchronized(balLock) {
      return balance;
    }
  }

  // 更改密码
  void updatePassword(String pw){
    synchronized(pwLock) {
      this.password = pw;
    }
  } 
  // 查看密码
  String getPassword() {
    synchronized(pwLock) {
      return password;
    }
  }
}

 这样的做,可以保证安全,但是会让别的线程去自旋。

如果用不同的锁对受保护资源进行精细化管理,就能够提升性能。这种锁叫细粒度锁。这种锁可以通过加锁位置的不同来实现不同的颗粒度,给需要加锁的地方,进行最小颗粒度的加锁。这种叫减少锁的时间

 

 

 又例如ConcurrentHashMap中的切割思想,每一个行加上小锁

 

 

 

看一段代码,这个方法是否是安全的。看起来方法上加了锁是安全的,但是有个问题就是锁加的位置就是方法,也就是锁住当前的实例。如果说传入的Account是无法保护住的。

class Account {
  private int balance;
  // 转账
  synchronized void transfer(
      Account target, int amt){
    if (this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  } 
}

如果要给这个两个对象都同时上锁,方法有很多,一种是锁Account.class对象,也可以在内部构造的时候传入一个lock对象。

posted @ 2020-12-25 14:39  smartcat994  阅读(195)  评论(0编辑  收藏  举报