震惊!他居然使用这种方式实现多线程交替打印数字0-100

1. 前言

今天听到某个群友去面试,挂在了多线程交替打印0-100算法上,当时我都震惊了,心想这种题目不是有手就行么,哈哈。但嘴上还是安慰他,很正常的,继续加油骚年

OK 进入正题!

两个线程交替打印数字0 - 100

线程1 : 1
线程2 : 2
线程1 : 3
线程1 : 4

其实这里,你需要解决两个问题;

问题一:操作数 i 线程安全问题

举例,两个线程对同一个数加50次,最终总数肯定加不到100,不信你可以试一下(手动狗头)

解决这个问题很简单,使用 volatile 关键字?

根据JMM规定,每个线程都有自己的独立工作内存空间, volatile 关键字确实能保证可见性(a线程加了操作数i,b线程可见)与有序性,但是不能保证原子性

线程A加了两次操作数 i ,从工作内存推到主内存中,并通知其他线程工作线程中操作数 i 失效


线程B加了一次操作数 i,准备将操作数 i 写入主内存中,却被通知自己手中的操作数失效了
于是重新去主内存中拿操作数 i ,那么此时这一次加相当于白执行了

那怎么办呢?简单方法就是使用AtomicInteger

AtomicInteger

AtomicInteger i = new AtomicInteger(1);
// native方法 原子操作
i.getAndIncrement()

AtomicInteger 是 JUC 包下的原子类,底层是 volatile + CAS算法

问题二:线程同步问题
线程同步问题,即交叉打印问题

2. 实现

2.1 信号量

概念

信号量最早是由学者Dijkstra提出了线程同步工具,没错,就是Dijkstra最短路径算法那个

可以讲信号量理解为一个数字S

PV原语操作:

P操作:将信号量S - 1,若 >= 0, 则继续执行,若信号量 < 0,则阻塞,放入阻塞队列中

V操作:将信号量S + 1,若信号量S > 0,则继续执行,若 <= 0,则继续在阻塞队列中,并从阻塞队列中唤醒一个其他线程执行

Semaphore

Java对信号量的实现

代码
    static AtomicInteger i = new AtomicInteger(0);
    static Semaphore s1 = new Semaphore(1);
    static Semaphore s2 = new Semaphore(0);



    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            while(i.get() < 100){
                try {
                    s1.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程-a:" + i.getAndIncrement());
                s2.release();
            }
        });

        Thread b = new Thread(() -> {
            while(i.get() < 100){
                try {
                    s2.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程-b:" + i.getAndIncrement());
                s1.release();
            }
        });
        a.start();
        b.start();
    }

2.2 synchronized + wait/notify

synchronized 锁住当前对象,使得一次只能执行一个线程

使用wait/notify 实现线程同步

实现

   static AtomicInteger i = new AtomicInteger(0);

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                synchronized (this) {
                    while (i.get() < 100) {
                        this.notify();
                        System.out.println("线程a:" + i.getAndIncrement());
                        this.wait();
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                synchronized (this) {
                    while (i.get() < 100) {
                        this.wait();
                        System.out.println("线程a:" + i.getAndIncrement());
                        this.notify();
                    }
                }
            }
        });
    }

wait/notify 只能在synchronized方法中使用,这是一个历史原因 Lost wake-up problem,假设下面一个例子

Lost wake-up problem 问题

int i = 0;
1: i ++
2: this.notify();


3: while(i <= 0){
4: this.wait()
5: }
6: i –

wait/notify 为啥要放在synchronized呢?

假如,
i = 0
操作3 条件成立
操作4 执行一半
重点,此时线程切换,执行完了线程1的操作1、操作2
线程切换回来继续执行 操作4
操作4执行成功

大家发现了一个什么问题么,就是此时 i = 1

消费者应该执行消费操作,现在却被wait住了

相当于this.notify() 这次操作就白执行了

这就是lost wake-up problem, 丢失醒来问题

如何解决这个问题,就是让wait/notify 必须与所执行操作保证原子性,即操作1、2为一块,操作3、4、5、6为一块

这也是JMM规范

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值