廖雪峰Java学习笔记 — 线程同步与死锁

1. 线程同步

当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。

所以当多个线程任务读写同一变量时,会产生数据不一致的情况。

为了解决这个问题,就需要在线程中对该变量进行加锁解锁来保证数据的一致性。

Java程序使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {
    n = n + 1;
}

注意必须是对同一个对象进行加锁,此外,Java中规定了如下几个原子操作:

  • 基本类型的赋值,例如int a = b;
  • 引用类型赋值。

如果方法中只有一句原子语句,则不需要加锁。



2. 同步方法

在多线程任务中,有时需要锁住的对象非常多,让线程自己选择要锁住的对象容易使得代码逻辑混乱,通常的方法是将synchronized逻辑封装起来。

例如,编写一个计数器如下:

public class Counter {
    private int count = 0;
    
    public void add(int n) {
        synchronized(this) {
            count += n;
        }
    }
    
    public void dec(int n) {
        synchronized(this) {
            count -= n;
        }
    }
    
    public int get() {	// 因为赋值时原子指令,所以不需要同步
        return count;
    }
}

这样,线程在调用add()dec()方法时,不必关心内部的同步逻辑就可以正常使用,我们称这样的类为线程安全的类

注意,synchronized锁住的是this对象,所以在创建多个Counter实例时,它们之间互不影响。同时,synchronized可直接对方法进行修饰,与上述代码功能一致。

public class Counter {
    private int count = 0;
    
    public synchronized void add(int n) {
        count += n;
    }
    
    public synchronized void dec(int n) {
        count -= n;
    }
    
    public int get() {	// 因为赋值时原子指令,所以不需要同步
        return count;
    }
}



3. 可重入锁

一个线程对对象A进行加锁后,其他线程需等待该线程释放对象A的锁后才能访问对象A

那么是否允许同一个线程对某一个对象加锁多次呢?例子如下:

public class Counter {
   private int count = 0;
   
   public synchronized void add_abs(int n) {	// 相加后然后取绝对值
       add(n);
       return count >= 0 ? count : -count;
   }
   
   public synchronized void add(int n) {
       count += n;
   }
   
   public synchronized void dec(int n) {
       count -= n;
   }
}

实际上,如果调用add_abs()方法,加锁和释放锁的顺序如下:

  1. Counter实例进行第一次加锁,记录+1。
  2. 调用add()方法时,对Counter实例进行第二次加锁,记录+1。
  3. add()方法结束,记录-1。
  4. add_abs()方法结束,记录-1,此时记录为0,释放该锁。

所以,JVM允许一个线程重复获取同一个锁,这种锁被称为可重复锁



4. 死锁

不同线程获取多个不同对象的锁时,可能造成死锁,如下:

public void add(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value += m;
        synchronized(lockB) { // 获得lockB的锁
            this.another += m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

public void dec(int m) {
    synchronized(lockB) { // 获得lockB的锁
        this.another -= m;
        synchronized(lockA) { // 获得lockA的锁
            this.value -= m;
        } // 释放lockA的锁
    } // 释放lockB的锁
}

死锁发生后就只能手动结束JVM进程才可以恢复。

那么如何避免死锁呢?应使线程获取锁的顺序要一致,修改上述代码如下:

public void add(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value += m;
        synchronized(lockB) { // 获得lockB的锁
            this.another += m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

public void dec(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.another -= m;
        synchronized(lockB) { // 获得lockB的锁
            this.value -= m;
        } // 释放lockA的锁
    } // 释放lockB的锁
}



5. 参考

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值