为了账号安全,请及时绑定邮箱和手机立即绑定

juc-03-synchronized、notify、notifyAll、wait、volatile

标签:
Java

这篇文章,给大家介绍一下几个与线程间同步相关的关键字:synchronized、notify、notifyAll、wait、volatile。

1 synchronized 内置锁

synchronized 是Java提供的一个并发控制的关键字。

synchronized 内置锁分两种

  • 对象锁:锁的是类的对象实例。作用在实例方法,或者实例代码块上。
  • 类锁:锁的是类的Class对象,每个类的的Class对象在一个JVM中只有一个,所以类锁也只有一个。作用在 static 方法,或者 static 代码块上。

1.1 没有锁,多线程并发对SyncObj中的value值+1

public class SyncObj {

    //数值
    private static int value;

    public SyncObj() {
    }

    /**
     * 不加锁,SyncClazz中的类变量value +1
     */
    public void addNoLock() {
        for (int i = 0; i < 3; i++) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " -no lock- " + ++value);
        }
    }

}

public class TestSynchronized {
    /**
     * 测试没有锁
     *
     * @throws InterruptedException
     */
    @Test
    public void testNoLock() throws InterruptedException {
        SyncObj so1 = new SyncObj();
        List<Thread> list = new ArrayList<>();
        // 使用多线程去执行 SyncObj.sayHello()
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 不加锁,不同线程,并发去对 so1 中的类变量value +1
                    // addNoLock() 没有加锁,线程不安全
                    so1.addNoLock();
                }
            });
            list.add(thread);
        }
        for (Thread thread : list) {
            thread.start();
        }
        TimeUnit.SECONDS.sleep(3);
    }
}

运行结果,可以看到有很多重复的数值,4个重复的1,2个重复的7…:

Thread-3 -no lock- 1
Thread-4 -no lock- 1
Thread-2 -no lock- 2
Thread-1 -no lock- 1
Thread-0 -no lock- 1
Thread-3 -no lock- 2
Thread-0 -no lock- 3
Thread-1 -no lock- 4
Thread-2 -no lock- 4
Thread-4 -no lock- 5
Thread-3 -no lock- 6
Thread-4 -no lock- 7
Thread-1 -no lock- 7
Thread-0 -no lock- 8
Thread-2 -no lock- 8

1.2 对象锁

对象锁:锁的是类的对象实例。作用在实例方法,或者实例代码块上。

public synchronized void print() {
    ...
}

等价于,锁的都是当前的实例对象(this)。

public void print() {
    synchronized (this){
        ...
    }
}    
        

1.2.1 对象锁,测试不同实例持有同一把对象锁,SyncObj中的类变量 value 还是线程安全地递增

public class SyncObj {

    //数值
    private static int value;

    //对象锁
    private Object lock = null;

    public SyncObj() {
    }

    public SyncObj(Object lock) {
        this.lock = lock;
    }

    /**
     * 对象锁,进行打印
     * synchronized 作用在实例方法(未使用static修饰的方法)
     * 用的锁是对象锁,是当前实例对象,也就是 this
     */
    public synchronized void print() {
        // 对于所有请求同一个对象锁资源的线程来说,下面的代码是线程安全的
        // 其他试图访问该对象锁线程将被阻塞
        for (int i = 0; i < 3; i++) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " -obj lock print- " + ++value);
        }
    }

    /**
     * 对象锁,当前实例类变量value +1
     */
    public void add() {
        // synchronized 作用在代码块上
        // 如果 synchronized 作用的是类的对象实例,是对象锁
        // 如果 synchronized 作用的是类的Class对象,是类锁
        synchronized (this.lock) {
            // 对于所有请求同一个对象锁资源的线程来说,下面的代码是线程安全的
            // 其他试图访问该对象锁线程将被阻塞
            for (int i = 0; i < 3; i++) {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " -obj lock add- " + ++value);
            }
        }
    }

    /**
     * 不加锁,SyncClazz中的类变量value +1
     */
    public void addNoLock() {
        for (int i = 0; i < 3; i++) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " -no lock- " + ++value);
        }
    }
}


    /**
     * 测试不同实例持有同一把对象锁,SyncObj中的类变量 value 还是线程安全地递增
     *
     * @throws InterruptedException
     */
    @Test
    public void testSyncObjSameLock() throws InterruptedException {
        //对象锁
        Object lock = new Object();
        SyncObj so1 = new SyncObj(lock);
        SyncObj so2 = new SyncObj(lock);
        List<Thread> list = new ArrayList<>();
        // 使用多线程去执行 SyncObj.sayHello()
        for (int i = 0; i < 5; i++) {
            //当前线程执行的实例
            SyncObj instance = i < 3 ? so1 : so2;
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 加对象锁,不同线程,并发去对持有同一把对象锁不同实例中的类变量value +1
                    // add() 加了同一把对象锁,类变量value还是线程安全递增
                    instance.add();
                }
            });
            list.add(thread);
        }
        for (Thread thread : list) {
            thread.start();
        }
        TimeUnit.SECONDS.sleep(3);
    }

运行结果:

Thread-0 -obj lock add- 1
Thread-0 -obj lock add- 2
Thread-0 -obj lock add- 3
Thread-2 -obj lock add- 4
Thread-2 -obj lock add- 5
Thread-2 -obj lock add- 6
Thread-1 -obj lock add- 7
Thread-1 -obj lock add- 8
Thread-1 -obj lock add- 9
Thread-4 -obj lock add- 10
Thread-4 -obj lock add- 11
Thread-4 -obj lock add- 12
Thread-3 -obj lock add- 13
Thread-3 -obj lock add- 14
Thread-3 -obj lock add- 15

1.2.1 对象锁,测试不同实例持有不同对象锁,SyncObj中的类变量 value 递增不是线程安全的


    /**
     * 测试不同实例持有不同对象锁,SyncObj中的类变量 value 递增不是线程安全的
     *
     * @throws InterruptedException
     */
    @Test
    public void testSyncObjDiffLock() throws InterruptedException {
        //对象锁
        Object lock1 = new Object();
        Object lock2 = new Object();
        SyncObj so1 = new SyncObj(lock1);
        SyncObj so2 = new SyncObj(lock2);
        List<Thread> list = new ArrayList<>();
        // 使用多线程去执行 SyncObj.sayHello()
        for (int i = 0; i < 5; i++) {
            //当前线程执行的实例
            SyncObj instance = i < 3 ? so1 : so2;
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 加对象锁,不同线程,并发去对 so1 中的类变量value +1
                    // add() 加了同一把对象锁,类变量value还是线程安全递增
                    instance.add();
                }
            });
            list.add(thread);
        }
        for (Thread thread : list) {
            thread.start();
        }
        TimeUnit.SECONDS.sleep(3);
    }

运行结果,可以看到有重复的数值,2个1,2个10:

Thread-0 -obj lock add- 1
Thread-3 -obj lock add- 1
Thread-0 -obj lock add- 2
Thread-3 -obj lock add- 3
Thread-3 -obj lock add- 4
Thread-0 -obj lock add- 5
Thread-4 -obj lock add- 6
Thread-2 -obj lock add- 7
Thread-4 -obj lock add- 8
Thread-2 -obj lock add- 9
Thread-2 -obj lock add- 10
Thread-4 -obj lock add- 10
Thread-1 -obj lock add- 11
Thread-1 -obj lock add- 12
Thread-1 -obj lock add- 13

1.3 类锁

类锁:锁的是类的Class对象,每个类的的Class对象在一个JVM中只有一个,所以类锁也只有一个。作用在 static 方法,或者 static 代码块上。

public static synchronized void print() {
    ...
}

等价于,锁的都是类的Class对象(SyncClazz.class)。

public static void print() {
    synchronized (SyncClazz.class){
        ...
    }
}    
        

1.3.1 测试类锁,不同线程,不同的SyncClazz实例 并发去对SyncClazz中的类变量value +1 都是线程安全的

public class SyncClazz {

    // 当前数值
    private static int value;

    /**
     * synchronized 作用在static方法是类锁,锁的是类的Class对象
     */
    public static synchronized void print() {
        // 获得类锁后,下面的代码是线程安全的
        for (int i = 0; i < 3; i++) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " -class lock print- " + ++value);
        }
    }

    /**
     * 类锁,SyncClazz中的类变量value +1
     */
    public void add() {
        // synchronized 作用在代码块上
        // 如果 synchronized 作用的是类的对象实例,是对象锁
        // 如果 synchronized 作用的是类的Class对象,是类锁
        synchronized (SyncClazz.class) {
            // 获得类锁后,下面的代码是线程安全的
            for (int i = 0; i < 3; i++) {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " -class lock add- " + ++value);
            }
        }
    }

    /**
     * 不加锁,SyncClazz中的类变量value +1
     */
    public void addNoLock() {
        for (int i = 0; i < 3; i++) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " -no lock- " + ++value);
        }
    }
}

    /**
     * 测试类锁,add() 加类锁,线程安全
     * 不同线程,不同的SyncClazz实例 并发去对SyncClazz中的类变量value +1 都是线程安全的,value数值有序地加一递增
     *
     * @throws InterruptedException
     */
    @Test
    public void testSyncClazz() throws InterruptedException {
        SyncClazz sc1 = new SyncClazz();
        SyncClazz sc2 = new SyncClazz();
        List<Thread> list = new ArrayList<>();
        // 使用多线程去执行 SyncClazz.sayHello()
        for (int i = 0; i < 5; i++) {
            //当前线程执行的实例
            SyncClazz instance = i < 3 ? sc1 : sc2;
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 不同线程,不同的SyncClazz实例 并发去对SyncClazz中的类变量value +1
                    // add() 加类锁,线程安全
                    instance.add();
                }
            });
            list.add(thread);
        }
        for (Thread thread : list) {
            thread.start();
        }
        TimeUnit.SECONDS.sleep(3);
    }

运行结果:

Thread-0 -class lock add- 1
Thread-0 -class lock add- 2
Thread-0 -class lock add- 3
Thread-4 -class lock add- 4
Thread-4 -class lock add- 5
Thread-4 -class lock add- 6
Thread-2 -class lock add- 7
Thread-2 -class lock add- 8
Thread-2 -class lock add- 9
Thread-3 -class lock add- 10
Thread-3 -class lock add- 11
Thread-3 -class lock add- 12
Thread-1 -class lock add- 13
Thread-1 -class lock add- 14
Thread-1 -class lock add- 15

2 等待和通知:notify、notifyAll、wait

notify、notifyAll、wait 这三个方法都是 Object 类中定义的方法。
前提: 持有当前对象的锁,才能调用当前的对象的notify、notifyAll、wait 这三个方法。

  • wait() 等待,当业务执行的某个条件不成立时,执行wait()方法,放弃持有的锁,阻塞当前的线程,等待需要的条件成立。
  • notify/notifyAll 唤醒,唤醒的是所有等待当前锁的线程,这些线程重新回到就绪状态,争抢cpu资源。

2.1 notifyAll和wait,演示生产者和消费者

//生产者
public class Producer implements Runnable{

    private Factory factory;

    public Producer(Factory factory) {
        this.factory = factory;
    }

    @Override
    public void run() {
        // 每个生产者生产3次
        for (int i = 0; i < 3; i++) {
            try {
                // 生产时间设置 200 ms
                TimeUnit.MILLISECONDS.sleep(200);
                // 生产者,通过 factory 类生产
                factory.produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 消费者
public class Consumer implements Runnable {

    private Factory factory;

    public Consumer(Factory factory) {
        this.factory = factory;
    }

    @Override
    public void run() {
        // 每个消费者生产3次
        for (int i = 0; i < 3; i++) {
            try {
                // 消费时间设置 200 ms
                TimeUnit.MILLISECONDS.sleep(200);
                // 消费者通过 factory 消费
                factory.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//工厂
public class Factory {

    // 可生产的最大数量
    private final int maxCount = 5;

    // 当前已有数量
    private int curCount;

    /**
     * 生产
     */
    public synchronized void produce() throws InterruptedException {
        // 抢到锁
        while (curCount >= maxCount) {
            System.out.println("生产已满,等待消费--" + Thread.currentThread().getName());
            // wait() 释放当前持有的锁
            this.wait();
        }
        this.curCount++;
        System.out.println(Thread.currentThread().getName() + "生产,当前数量--" + curCount);
        // 唤醒当前正在阻塞等待this对象锁的线程
        this.notifyAll();
    }

    /**
     * 消费
     */
    public synchronized void consume() throws InterruptedException {
        while (curCount <= 0) {
            System.out.println("无货消费,等待生产====" + Thread.currentThread().getName());
            // wait() 释放当前持有的锁
            this.wait();
        }
        this.curCount--;
        System.out.println(Thread.currentThread().getName() + "消费,当前剩余数量--" + curCount);
        // 唤醒当前正在阻塞等待this对象锁的线程
        this.notifyAll();
    }
}

// 测试生产者消费者
public class TestProducerConsumer {

    @Test
    public void testProducerConsumer() throws InterruptedException {
        Factory factory = new Factory();
        new Thread(new Producer(factory), "生产者A").start();
        new Thread(new Producer(factory), "生产者B").start();
        new Thread(new Producer(factory), "生产者C").start();
        new Thread(new Consumer(factory), "消费者1").start();
        new Thread(new Consumer(factory), "消费者2").start();
        new Thread(new Consumer(factory), "消费者3").start();
        TimeUnit.SECONDS.sleep(20);
    }
}

运行结果:

生产者C生产,当前数量--1
生产者A生产,当前数量--2
生产者B生产,当前数量--3
消费者2消费,当前剩余数量--2
消费者3消费,当前剩余数量--1
消费者1消费,当前剩余数量--0
生产者A生产,当前数量--1
生产者B生产,当前数量--2
消费者1消费,当前剩余数量--1
生产者C生产,当前数量--2
消费者3消费,当前剩余数量--1
消费者2消费,当前剩余数量--0
无货消费,等待生产====消费者3
生产者C生产,当前数量--1
消费者1消费,当前剩余数量--0
无货消费,等待生产====消费者2
生产者B生产,当前数量--1
生产者A生产,当前数量--2
消费者2消费,当前剩余数量--1
消费者3消费,当前剩余数量--0

3 volatile

volatile: 当多个线程进行操作共享数据时,可以保证内存中的数据可见,相较于 synchronized 是一种更为轻量级的同步策略。

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的,也就是一个线程修改的结果,另一个线程马上就能看到。用volatile修饰的变量,该变量就具有可见性。

volatile修饰的变量不允许线程内部缓存和重排序,即直接读写内存,所以对其他线程是可见的。
注意:volatile只能保证所修饰内容具有可见性,但不能保证原子性。
比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

3.1 演示变量未加volatile时,线程间可见性问题

public class TestVolatile {

    public static void main(String[] args) throws InterruptedException {
        FlagThread thread = new FlagThread();
        thread.start();
        while (!thread.isFlag()){
            if(thread.isFlag()){
                System.out.println("读取到 flag = true");
                break;
            }
        }
    }
}

class FlagThread extends Thread {

    private boolean flag;

    public boolean isFlag() {
        return flag;
    }

    @Override
    public void run() {
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.flag = true;
    }
}

运行结果,主线程检测不到 FlagThread 中 flag 的变化,一直在while循环中,跳不出来:
图片描述

3.2 使用 volatile 修饰变量,保证变量修改后,线程间可见

public class TestVolatile {

    public static void main(String[] args) throws InterruptedException {
        FlagThread thread = new FlagThread();
        thread.start();
        while (!thread.isFlag()){
            if(thread.isFlag()){
                System.out.println("读取到 flag = true");
                break;
            }
        }
    }
}

class FlagThread extends Thread {

    // 使用 volatile 修饰变量,保证变量修改后,线程间可见
    private volatile boolean flag;

    public boolean isFlag() {
        return flag;
    }

    @Override
    public void run() {
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.flag = true;
    }
}

运行结果:

读取到 flag = true

上面简单地演示了 synchronizedvolatile 关键字的用法,还通过 wait() 和 notifyAll() 写了一个生产者消费者,线程间同步的Demo。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
11
获赞与收藏
8

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消