线程同步(超详细)(二)

1 使用wait和notify实现线程同步

wait、notify、notifyAll是Object对象的属性,并不属于线程

  • wait:使持有该对象的线程把该对象的控制权交出去,然后处于等待状态(注:当调用wait的时候会释放锁并处于等待的状态)
  • notify:通知某个正在等待这个对象的控制权的线程可以继续运行(被唤醒的线程需要先获取锁,才能使自己的程序开始执行,只会唤醒一个线程,选择哪个线程取决于操作系统对多线程管理的实现)
  • notifyAll:会通知所有等待这个对象控制权的线程继续运行(和上面一样,只不过是唤醒所有等待的线程让它们竞争锁,获得锁权限的线程继续执行)

下文中notify方法指notify或notifyAll

调用wait方法时会将当前的线程挂起直到notify被方法唤醒,这和sleep方法有点类似,但是wait会释放当前线程所持有的调用wait方法的锁。wait,notify方法的调用必须持有synchronized的对象锁,并且调用wait和notify的对象必须是synchronized锁住的对象,否则会抛出java.lang.IllegalMonitorStateException异常。使用wait和notify实现线程同步常用于生产者和消费者问题。

有关wait和notify的调用必须要配合synchronized一起使用的原因可以参见这篇文章,https://blog.csdn.net/sufu1065/article/details/123102819,很详细。

我们继续使用篇一中的售票案例,之前我们卖门票前通过判断票数是否大于0来判断是否还有门票,当没有门票时任务结束,并且只有消费者。现在我们增加消费者,当没有门票时消费者使用wait挂起,直到生产者notify通知有门票后继续执行。

注意代码中的consumer和producer属于不同的类,synchronized需要锁住同一对象,所以我们在抽象类中增加了静态对象object,并调用该对象的wait和notify,否则会抛出异常java.lang.IllegalMonitorStateException

package test;

public class TicketThreadTest {

    public static void main(String[] args) throws InterruptedException {
        TicketThread ticketConsumer = new TicketThread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        sellOneTicket();
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        TicketThread ticketProducer = new TicketThread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        addOneTicket();
                        Thread.sleep(4000);  // sleep 3000ms
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        Thread thread1 = new Thread(ticketConsumer, "窗口1");
        Thread thread2 = new Thread(ticketConsumer, "窗口2");
        Thread thread3 = new Thread(ticketConsumer, "窗口3");
        Thread thread4 = new Thread(ticketProducer, "印票处");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

abstract class TicketThread implements Runnable {

    private static int ticketNum = 20; // 总票数

    public static int sellNum = 0; // 统计卖出总票数

    private static final Object object = new Object();

    protected void sellOneTicket() throws InterruptedException {
        synchronized (object) {
            if (ticketNum > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum + " sellNum:" + ++sellNum);  // 卖出一张票
            } else {
                System.out.println(Thread.currentThread().getName() + " waiting");
                object.wait(); //没票时线程阻塞并释放锁
                System.out.println(Thread.currentThread().getName() + " exit waiting");
            }
        }
    }

    protected void addOneTicket() {
        synchronized (object) {
            ticketNum += 5;
            System.out.println(Thread.currentThread().getName() + "新增了5张票,剩余:" + ticketNum );  // 新增5张票
            object.notifyAll();    //增加票后通知消费者
        }
    }
}


2 使用特殊域变量volatile实现线程同步

这里我们先引入“缓存一致性”、“乐观锁”和“悲观锁”的概念。

  • **缓存一致性:**指最终存储在多个本地缓存中的共享数据的一致性。当多个客户端都维护同一个内存资源的缓存时,就可能出现数据不一致的问题。这种情况在多CPU并行系统中尤其常见。
  • **悲观锁:**顾名思义,悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能变动,一个线程拿到悲观锁后,其他任何线程都不能对该数据进行修改,只能等待锁被释放才可以执行。在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延迟,引起性能问题,一个线程持有锁会导致其他需要此锁的线程挂起
  • **乐观锁:**与悲观锁相对,乐观锁的“乐观情绪”体现在,它认为数据的变动不会太频繁。因此,它允许多个线程同时对数据进行变动,如果发现并发冲突,则返回错误信息,需要用户去决定如何操作。乐观锁因为时通过我们人为实现的,它仅仅适用于我们自己业务中,如果有外来事务插入,那么就可能发生错误。

显然synchronized属于悲观锁,即将介绍的volatile则属于乐观锁机制

对于某个共享变量,每个操作单元都缓存一个该变量的副本。当一个操作单元更新其副本时,其他的操作单元可能没有及时发现,进而产生缓存一致性问题。例如上篇最开始我们提到的售票问题
悲观锁假设更新很可能冲突,每次都要先获取锁才能去操作要同步的对象,其他线程要进入该同步块的时候就要被阻塞,进入锁等待队列。事实上,在读远多于写的场景下,我们应该“乐观”一点。多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。 用final域,有锁保护的域和volatile域可以避免非同步的问题。

  • volatile是Java虚拟机提供的轻量级的同步机制
  • volatile可以保证可见性, 禁止指令重排, 但是不保证原子性比如 num++ 这个操作实际上分为三步, 拿到num值, 对num值加一, 放回num值

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。
JMM规定如果多个线程同时操作volatile变量,那么对该变量的写操作必须在读操作之前执行(禁止重排序),并且写操作的结果对读操作可见(强缓存一致性)。由于volatile只能保证可见性,并不能保证原子性,所以仅靠volatile并不能实现多个写线程同步。

  • volatile关键字保证了拿到number的值是正确的,但是在执行对num值加一, 放回num值这些指令的时候,其他线程可能已经把number的值改变了,而操作栈顶的值就变成了过期的数据,所以就可能把较小的number值同步回主内存之中

由于volatile只能保证可见性,并不能保证原子性,所以仅靠volatile并不能实现多个写线程同步。我们将当时售票问题的案例中的ticketNum和sellNum使用volatile修饰并不能解决问题。

package test;

import java.io.*;

public class VolatileTest {

    public static void main(String[] args) throws IOException, InterruptedException {
        TicketThread2 ticket1 = new TicketThread2();
        Thread thread1 = new Thread(ticket1, "窗口1");
        Thread thread2 = new Thread(ticket1, "窗口2");
        Thread thread3 = new Thread(ticket1, "窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join(); // 等待三个线程结束后打印卖出总数
        thread2.join();
        thread3.join();
        System.out.println("sellNum: " + TicketThread2.sellNum);
    }
}

class TicketThread2 implements Runnable {

    private static volatile int ticketNum = 20; // 总票数

    public static volatile int sellNum = 0; // 统计卖出总票数

    @Override
    public void run() {
        while (true) {
            if (ticketNum > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum);  // 卖出一张票
                sellNum++;  // 卖出总票数加1
            } else {
                break;
            }

            try {
                Thread.sleep(10);  // 每次sleep 10ms,提高出错可能
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

某次运行结果,很显然问题仍然存在。

窗口1卖出了一张票,剩余:19
窗口3卖出了一张票,剩余:18
窗口2卖出了一张票,剩余:17
窗口1卖出了一张票,剩余:16
窗口3卖出了一张票,剩余:15
窗口2卖出了一张票,剩余:16
窗口3卖出了一张票,剩余:14
窗口2卖出了一张票,剩余:14
窗口1卖出了一张票,剩余:13
窗口1卖出了一张票,剩余:12
窗口3卖出了一张票,剩余:11
窗口2卖出了一张票,剩余:12
窗口1卖出了一张票,剩余:10
窗口2卖出了一张票,剩余:8
窗口3卖出了一张票,剩余:9
窗口3卖出了一张票,剩余:6
窗口1卖出了一张票,剩余:7
窗口2卖出了一张票,剩余:7
窗口3卖出了一张票,剩余:5
窗口1卖出了一张票,剩余:4
窗口2卖出了一张票,剩余:3
窗口2卖出了一张票,剩余:2
窗口3卖出了一张票,剩余:0
窗口1卖出了一张票,剩余:1
sellNum: 24
如果只有一个线程进行写操作,其余线程都是读操作时,volatile就可以实现线程同步。
package test;

import java.io.*;

public class VolatileTest {

    public static void main(String[] args) throws IOException, InterruptedException {
        TicketThread2 reader = new TicketThread2(false);
        TicketThread2 writer = new TicketThread2(true);
        Thread thread1 = new Thread(reader, "读1");
        Thread thread2 = new Thread(reader, "读2");
        Thread thread3 = new Thread(writer, "窗口");
        thread3.start();
        thread1.start();
        thread2.start();
        thread1.join(); // 等待三个线程结束后打印卖出总数
        thread2.join();
        thread3.join();
        System.out.println("sellNum: " + TicketThread2.sellNum);
    }
}

class TicketThread2 implements Runnable {

    private static volatile int ticketNum = 20; // 总票数

    public static volatile int sellNum = 0; // 统计卖出总票数

    public final boolean flag;

    TicketThread2(boolean flag) {
        this.flag = flag;
    }


    @Override
    public void run() {
        if (flag) {
            while (true) {
                if (ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum);  // 卖出一张票
                    sellNum++;  // 卖出总票数加1
                } else {
                    break;
                }

                try {
                    Thread.sleep(10);  // 每次sleep 10ms,提高出错可能
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        } else {
            while (true) {
                if (ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + "读取当前票数:" + ticketNum + " 当前sellNum" + sellNum);  // 卖出一张票
                } else {
                    break;
                }
                try {
                    Thread.sleep(10);  // 每次sleep 10ms
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值