题目描述
给你一个类:
class FooBar { public void foo() { for (int i = 0; i < n; i++) { print("foo"); } } public void bar() { for (int i = 0; i < n; i++) { print("bar"); } } }两个不同的线程将会共用一个
FooBar
实例:
- 线程 A 将会调用
foo()
方法,而- 线程 B 将会调用
bar()
方法请设计修改程序,以确保
"foobar"
被输出n
次。示例 1:
输入:n = 1 输出:"foobar" 解释:这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,"foobar" 将被输出一次。示例 2:
输入:n = 2 输出:"foobarfoobar" 解释:"foobar" 将被输出两次。
这道题是与多线程有关的问题,我们需要保证foo线程和bar线程有序调用。输出n个foobar,也就是两个线程各调用n次,且必须有序。
方法一 信号量Semaphore
信号量的原理具体可以看操作系统里的信号量机制,在这里简单概述一下。
信号量的使用可以适用于两个场景: 同步和互斥。
当信号量用于互斥的时候,初始值一般设为1,表示只有一个资源可以使用。当一个线程访问共享资源时,会执行获取的操作,使信号量减 1 ,当这个线程即将结束的时候,会执行释放的操作,使信号量加 1 。
当信号量用于同步的时候,举个小例子:
生产者-消费者问题:
生产者负责生产数据,消费者负责处理数据。这里的同步确保消费者不会再没有数据的时候还进行消费的操作,也保证生产者不会再缓冲区已经满的情况下生产数据。
我们用empty作为 缓冲区空闲的位置。用full作为缓冲区已经有的数据的数量。我们假设缓冲区最大容量为1。
生产者生产数据前,需要调用empty的获取操作,使其值减 1 , 生产完数据后,我们对full进行释放的操作,使其值加 1 ,表示缓冲区中有新的数据可以消费。
此时如果我的生产者进程想再次被调用的话,我们需要获取empty,但是empty的值已经为0,没有空闲位置,所以此时生产者进程不可被调用。
那么这个时候,如果调用消费者进程,他会先获取full , 使其值减 1 ,使 full 为 0 ,消费者执行完,会进行empty的释放操作,使empty 的值加 1 。表明缓冲区又有新的空闲位置,生产者可以进行生产。
如果这个时候,消费者进程再次被调用,消费者消费数据之前,会执行full的获取操作,可是此时full已经为0 , 所以消费者进程会被阻塞。只能生产者进程生产完数据之后,消费者进程才可以再次被调用。
这个流程就保证了生产者消费者的同步操作。
那么对于这道题也是相同。我们定义两个信号量表示:
如果foo线程想执行,就必须获取fooSema信号量,如果bar线程像执行,就必须获取barSema信号量。
private Semaphore fooSema = new Semaphore(1);
private Semaphore barSema = new Semaphore(0);
我们需要让 foo 线程先被调用,所以让fooSema的初始值为1,这就相当于生产者消费者问题,我们先让empty的值为1。
在进入 foo 线程之前,先获取fooSema , 让其值减 1 ,之后执行 foo 线程,执行完之后,释放barSema ,使其值加1。
for (int i = 0; i < n; i++) {
fooSema.acquire();
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
barSema.release();
}
bar 线程同理。
for (int i = 0; i < n; i++) {
barSema.acquire();
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
fooSema.release();
}
完整代码展示
class FooBar {
private int n;
private Semaphore fooSema = new Semaphore(1);
private Semaphore barSema = new Semaphore(0);
public FooBar(int n) {
this.n = n;
}
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
fooSema.acquire();
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
barSema.release();
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
barSema.acquire();
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
fooSema.release();
}
}
}
方法二 Thread.yield()
关键点
Thread.yield():使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。但是需要注意,如果A线程使用这个方法后,变成就绪态,此时还是可以被调用的。
volatile 关键词定义的变量,如果发生变化,那么所有线程都可见这个变化。
方法二中我们使用这两个知识点来解决问题。
/*
如果fooExec为真,那么foo可以执行,如果为假,bar执行。
*/
volatile boolean fooExec = true;
如果想 foo 线程想被执行,fooExec就必须为真,如果fooExec不为真,那么就让他变成就绪状态。直到 bar 线程执行完,让fooExec 为正之后,才可以继续执行 foo 线程。这就保证了线程之间的同步。
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n;) {
if(fooExec){
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
i++;
fooExec = false;
}else{
Thread.yield();
}
}
}
bar线程同理。
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n;) {
if(!fooExec){
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
i++;
fooExec = true;
}else{
Thread.yield();
}
}
}
完整代码展示
class FooBar {
private int n;
volatile boolean fooExec = true;
public FooBar(int n) {
this.n = n;
}
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n;) {
if(fooExec){
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
i++;
fooExec = false;
}else{
Thread.yield();
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n;) {
if(!fooExec){
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
i++;
fooExec = true;
}else{
Thread.yield();
}
}
}
}