Java多线程之synchronized

引入问题

在初步接触多线程之后,我们知道多线程的一个问题是‘竞态条件’,导致程序变得不稳定或者不安全。我们看下面的例子(《Java编程的逻辑 15.2》)

public class Counter {
    private int count;
    public void incr(){
        count ++;
    }
    public int getCount() {
        return count;
    }
}

public class CounterThread extends Thread {
    Counter counter;
    public CounterThread(Counter counter) {
        this.counter = counter;
    }
    @Override
    public void run() {
        for(int i = 0; i < 1000; i++) {
            counter.incr();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        int num = 1000;
        
        Counter counter = new Counter();
        Thread[] threads = new Thread[num];
        for(int i = 0; i < num; i++) {
            threads[i] = new CounterThread(counter);
            threads[i].start();
        }
        
        for (int i = 0; i < num; i++) {
            threads[i].join();
        }
        System.out.println(counter.getCount());
    }
}

从main函数开始看:创建了‘一’个counter对象,创建了1000个CounterThread对象;然后调用start方法执行run方法体,执行了1000次incur;最后输出counter对象的count数值。

理想结果是1000*1000,但真实结果并非如此。

通过理论分析,问题出现在incur方法中的count++并非一个原子操作,它可以拆分为以下子操作:

  1. 先读取count的值
  2. count的值+1
  3. 赋值给count

这样就可能会出现一种情况,

  • 某个线程A执行count++时,执行到第一个子操作,另一个线程B也抢占CPU执行count++。
  • 导致线程A还未完成执行count++中的赋值操作,线程B就开始读取count的数值,这时线程B读取到的数值是线程A操作前的count数值,
  • 最终导致了A的修改丢失了。

用synchronized解决

给incur方法加上synchronized关键字,再执行发现获得的结果与预期相同。

public class Counter {
    private int count;
    public synchronized void incr(){
        count ++;
    }
    public int getCount() {
        return count;
    }
}

理解synchronized

这里我们最直观的理解是:

  • synchronized保护了incr方法中的变量,让incr方法可以作为一个原子操作,
  • 即incr方法执行完成后,才会让其它线程访问这个变量。

但这个理解是不准确的。

准确的理解是,synchronized保护的是‘对象’而非代码(方法):

  • 只要访问的是‘同一个对象’的方法,即使是不同的方法(这些方法需要由synchronized修饰),也会被同步顺序访问。
  • 访问‘不同对象’的同一synchronized方法,不能保证顺序访问。

对应下面两个例子(只展示了主要的部分):

//同一对象调用不同synchronized方法
public class Counter {
    private int count = 0;

    public synchronized void incr(){
        count++;
    }
    public synchronized void decr(){
        count--;
    }

    public synchronized int getCount() {
        return count;
    }
}

public void run() { // 总是输出0
    for (int i = 0; i < 100; i++) {
        counter.incr(); // incr执行完会释放锁,即使是其它线程获得了锁,再执行incr,也不会影响总共会执行1000*100次decr,且都是顺序执行。
        counter.decr();
    }
}



//不同对象调用同一synchronized方法
Counter counter1 = new Counter();
Counter counter2 = new Counter();
Thread t1 = new CounterThread(counter1);
Thread t2 = new CounterThread(counter2);
t1.start();
t2.start();

synchronized方法不能防止非synchronized方法被同时执行。一般在保护变量(某个对象的变量)时,需要在所有访问该变量的方法上加上synchronized。

  • 例如,如果将上面的decr方法的synchronized去掉,则不能再保证decr会顺序执行。

执行synchronized实例方法的过程大致如下:
1)尝试获得‘锁’,如果能够获得锁,继续下一步,否则加入‘等待队列’,阻塞并等待唤醒。
2)执行实例方法体代码。
3)释放锁,如果等待队列上有等待的线程,从中取一个并唤醒,如果有多个等待的线程,唤醒哪一个是不一定的,不保证公平性。

上面所说的‘锁’和‘等待队列’的概念,对于任意一个对象都有一个锁和等待队列:

  • 这里说的锁就是对象本身,不同的线程抢夺这个对象,抢到了就说获得了锁,执行完相应逻辑后就释放锁。
  • 等待队列可以视为一个排队的队列,队列里面是等待需要这个对象的线程。

多种写法

掌握了上面的概念后,不同的写法万变不离其宗。

  • 写在具体的类中
    • 实例方法
    • 静态方法
    • 代码块
  • 写在线程类的run方法中
    • 代码块
// 1.写在具体类

// 实例方法
public class StaticCounter {
    private int count = 0;
    public synchronized void incr() {
        count++;
    }
}

// 静态方法
public class StaticCounter {
    private static int count = 0;
    public static synchronized void incr() {
        count++;
    }
}

//代码块
public class Counter {
    private int count;
    public void incr(){
        
        synchronized(this){ // 这里的this就是具体的Counter类的实例对象
            count ++;
        }
        
    }
}

public class StaticCounter {
    private static int count = 0;
    public static void incr() {
        
        synchronized(StaticCounter.class){ // StaticCounter.class是StaticCounter类
            count++;
        }
        
    }
}

// 2.写在线程类的run方法中
public void run() {
    synchronized (counter) { // 这里counter是保护的对象。这里如果写this代表的是线程类的实例对象。
        counter.incr();
        counter.decr();
    }
}

总结

  • synchronized保护的是‘对象’而非代码(方法)
  • 任意一个对象都有一个锁和等待队列

参考

《Java编程的逻辑》


欢迎讨论指正
  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值