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规范