[JUC-02] Synchronized

1、Synchronized

  在 Java 中,每个对象有且仅有一把锁(lock),也称为监视器(monitor)。

  synchronized 是 Java 中的关键字,是一种同步锁。它修饰的目标有以下几种:
  1、修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象(锁)是调用这个代码块的对象
  2、修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  3、修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
  4、修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用的对象是这个类的所有对象

  在并发编程中存在线程安全问题,主要原因有:
  1.存在共享数据
  2.多线程共同操作共享数据。

  关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。

2、Synchronized的使用

  使用并发编程的原则是:线程操作资源类。

2.1 修饰代码块

public class Demo01 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) ticket.sale();
        }, "A").start();
        
        new Thread(() -> {
            for (int i = 0; i < 40; i++) ticket.sale();
        }, "B").start();
        
        new Thread(() -> {
            for (int i = 0; i < 40; i++) ticket.sale();
        }, "C").start();
    }
}

class Ticket {
    private int number = 30;

    public void sale() {
        synchronized (this) {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出 "+this.toString()+" 的第 " + (30 - (number--) + 1) + " 张票,还剩余 " + number + " 张票。");
            }
        }
    }
}

  运行结果:
在这里插入图片描述
  分析:资源类为Ticket,三个线程分别是 A、B、C,this就是ticket对象。
  1、三个线程启动后,三个线程都可以操作ticket对象;
  2、当三个线程都进入了sale()方法,遇到了同步代码块,此时,哪个线程手快先拿到ticket对象的锁,谁就进入,其他两个就在此等待,因为一个对象只有一个锁;
  3、当执行完同步代码块,就把锁释放了,即此时ticket对象又有锁了。

  结果就是,同一时间,只有拿到锁的线程才能访问同步代码块,其他没拿到锁的只能处于阻塞状态等待。这是只有一个对象的情况,如果有ticket2对象,那么,操作这个ticket2对象的线程和操作ticket对象的线程没有什么关系。

2.2 修饰非静态方法

public class Demo02 {
    public static void main(String[] args) {

        Ticket ticket = new Ticket();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) ticket.sale();
        }, "A").start();

        new Thread(() -> {
            Ticket ticketB = new Ticket();
            for (int i = 0; i < 40; i++) ticket.sale();
        }, "B").start();

        new Thread(() -> {
            Ticket ticketC = new Ticket();
            for (int i = 0; i < 40; i++) ticket.sale();
        }, "C").start();
    }
}

class Ticket {
    private int number = 30;

    public synchronized void sale() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出 "+this.toString()+" 的第 " + (30 - (number--) + 1) + " 张票,还剩余 " + number + " 张票。");
        }
    }
}

  运行结果:
在这里插入图片描述
  分析:和修饰代码块的分析一样,此时的锁也是从this对象获取。

2.3 修饰静态方法

public class Demo03 {
    public static void main(String[] args) {

        new Thread(() -> {
            for (int i = 0; i < 40; i++) new Ticket().sale();
        }, "A").start();

        new Thread(() -> {
            Ticket ticketB = new Ticket();
            for (int i = 0; i < 40; i++) new Ticket().sale();
        }, "B").start();

        new Thread(() -> {
            Ticket ticketC = new Ticket();
            for (int i = 0; i < 40; i++) new Ticket().sale();
        }, "C").start();
    }
}

class Ticket {
    private static int number = 30;

    public static synchronized void sale() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出第 " + (30 - (number--) + 1) + " 张票,还剩余 " + number + " 张票。");
        }
    }
}

  运行结果:
在这里插入图片描述
  分析:A、B、C 三个线程操作的是三个对象,根据结果可知,此时的锁已经不是Ticket对象了,因为synchronized修饰的是静态方法,此时的锁是Ticket类的所有对象,即所有对象共用一把锁。

3、生产者消费者问题

3.1 一个生产者和一个消费者

  所谓生产者消费者问题,指的是一个线程负责生产数据,一个线程负责消费数据。消费者消费的数据必须是来源于生产者生产的数据,要做到这一点就得进行线程同步操作。

  下面举一个例子,一个资源类有一个number变量,初始值为 0,资源类有两个方法,分别是 increment()decrement() 分别对number进行加一、减一操作。

  下面是一个资源类:

class Data { // 数字 资源类
    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        if (number != 0) { //0
            // 等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知其他线程,我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if (number == 0) { // 1
            // 等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知其他线程,我-1完毕了
        this.notifyAll();
    }
}

  测试:

public class ProdAndCons {
    public static void main(String[] args) {

        Data data = new Data();
        
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}

  运行结果如下:
在这里插入图片描述
  运行很正常。
  1、生产者A判断出number为0,则进行加一操作,然后执行notifyAll()方法告知消费者B来消费。如果生产者A判断出number不为0,说明消费者B还没消费,则生产者A进入 WAITING 状态,直到被唤醒。

  2、消费者B判断出number不为 0,则进行减一操作,然后执行notifyAll()方法告知生产者A继续生产。如果消费者B判断出number为0,说明生产者A还没生成,则消费者B进入 WAITING 状态,直到被唤醒。

3.2 多个生产者和多个消费者

  现在多增加一个生产者和一个消费者:

public class ProdAndCons {
    public static void main(String[] args) {

        Data data = new Data();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

  运行效果:
在这里插入图片描述
  可以发现一个诡异的现象:出现了非1非0的数字。

3.3 虚假唤醒

  原因诊断:出现这些2、3、4的原因就是多个生产者线程进入increment()方法,这些线程进入的时候,number为0,然后这些线程都给number进行了加一操作。

  更加诡异:加一方法和减一方法都是用了 synchronized 关键字修饰了的,怎么可能出现多个线程同时进去的情况?

  这个现象在 Java 官方称为“虚假唤醒”,究其原因就出现在wait()方法上:
在这里插入图片描述
  官方的意思说:只要把wait()的触发判断改为循环,就可以避免,我们来尝试一下:

class Data { // 数字 资源类
    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        while (number != 0) { //0
            // 等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知其他线程,我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        while (number == 0) { // 1
            // 等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知其他线程,我-1完毕了
        this.notifyAll();
    }
}

  把 if 改成了 while,运行结果如下:
在这里插入图片描述
  还真的是正常了,这又是什么原理??

3.4 虚假唤醒的本质

  问题就出在wait()方法上,我们看这个方法的官方文档:
在这里插入图片描述
  文档说的很晦涩,一句一句翻译一下:

  当前的线程必须拥有该对象的显示器:显示器其实来源于单词(monitor),其实就是锁的意思,即:要调用wait()方法,当前线程必须拥有调用wait()方法的对象的锁;

  该线程释放此监视器的所有权:调用了wait()方法后,锁也给扔了;

  并等待另一个线程通知等待该对象监视器的线程通过调用notify方法或notifyAll方法notifyAll:等着被其他线程唤醒。

  看明白了这三句话,就知道虚假唤醒问题出在哪了,就在这关键的一句话:该线程释放此监视器的所有权。也就是说,锁扔了。那锁扔了会有什么后果呢?后果就是阻塞中的、等待获取这个锁的线程直接就拿到锁了,然后进入 synchronized 方法了。

  要知道,上面的代码中,wait()方法是在 synchronized 修饰的方法里面,还没出去。这就导致多个线程同时进入了 synchronized 修饰的方法里面。

  我们引入 synchronized 来修饰方法,就是为了避免多个线程对变量的一个状态同时进行判断,比如:number 为 0,然后多个线程在同一时刻判断它,结果多个线程都认为该加一,导致的后果就是每个线程都给它加一,最终效果就不是加一了,而是执行了多次加一。

  下面仔细分析出现虚假唤醒后的逻辑:

  A、B 线程是生产者,对 number 进行加一操作。

  1、A 线程进入increment()方法,判断出 number 等于1,于是 this.wait(),丢掉了锁。

  2、线程 B 拿到了锁,进入了increment()方法,此时 number 已经被消费了,为 0,线程 B 判断出 number 为 0,于是进行了加一操作,然后线程 B 离开 synchronized 方法,释放了锁并this.notifyAll()通知所有等待中的线程;

  3、线程 A 被唤醒,同时也拿到了锁,继续往下执行了加一操作,这时,number 就变为 2了。

  我们分析出,只要线程 A 在恢复并拿到锁之后,再判断一次 number 的状态,即可避免再加一,这就是官方文档要我们使用循环的原因了。

  线程 A 在丢掉锁之后,这个 synchronized 方法已经被其他的线程 B 进入了,为了避免出错,线程 A 应对当前状态再进行判断,这是最安全的操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值