力扣1115题目:给定类,两个不同的线程分别执行调用同一个FooBar实例,修改程序实现”foobar“被交替打印n次。
题目
给定类如下:
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次。
方法① 利用synchronized同步锁
- wait、notify/notifyAll方法都是Object的本地final方法,
无法被重写
。 - wait让当前线程阻塞等待,前提是首先获得锁,配合synchronized关键字使用;在synchronized同步代码中使用wait、notify/notifyAll方法。
- 由于wait、notify/notifyAll在synchronized代码中执行,则说明当前线程必定有锁。
- 若线程执行wait方法,释放当前锁,给出CPU资源,进入等待状态。
- 只有当notify/notifyAll方法执行,才会唤醒一个或者多个处于等待状态的线程,从而继续往下执行,一直到执行完synchronized代码块中的代码或者中间遇到了wait,再次释放锁lock。
- 也就是说,notify/nofityAll执行只是唤醒沉睡的线程,不会立即释放锁,锁的释放需要查看代码块的具体执行情况。在具体业务处理中,尽量在使用了notify/notifyAll后立即退出临界区,唤醒其他等待的线程。
临界区:一个访问公用资源,例如公用设备或者公用存储器,的程序片段;这些公用资源无法同时被多个线程访问的特性;当有线程进入临界区段时,其他线程必须wait等待(例如:bounded waiting 等待算法),有一些同步的机制会在临界区的进入点或者离开点实现,保证公用资源是互斥获得使用权,例如:信号量semaphore机制;生活中的例子有,打印机资源的互斥使用。
- wait方法需要被try–catch异常机制包裹,即使发生异常也能让wait等待的线程唤醒。
- notify和wait的执行顺序不可以出错,若A线程优先执行了notify方法,B线程执行了wait方法,则B线程是无法唤醒的。
- notify和notifyAll方法的差异:
- 首先给出如下两个概念:等待池,设定线程A调用了对象的wait方法,则线程A释放该对象锁之后,进入了该对象的等待池,等待池的线程不会去竞争该对象的锁;锁池,只有获取了对象的锁,县城才可以执行对象的synchronized代码块,对象的锁每次只有一个线程可以获取,其他线程只能够等待。
- notify方法随机唤醒对象的等待池的一个线程,进入锁池;
- nofiyAll方法则唤醒对象的等待池中的所有线程,进入锁池。
给出测试代码如下:
package java_thread_run;
public class TestNotifyNotifyAll {
private static Object obj = new Object();
public static void main(String[] args) {
//测试RunnableImplA wait方法
Thread t1 = new Thread(new RunnableImplA(obj));
Thread t2 = new Thread(new RunnableImplA(obj));
t1.start();
t2.start();
// //RunnableImplB notify()
// Thread t3 = new Thread(new RunnableImplB(obj));
// t3.start();
//RunnableImplC notifyAll()
Thread t4 = new Thread(new RunnableImplC(obj));
t4.start();
}
}
class RunnableImplA implements Runnable {
private Object obj;
public RunnableImplA(Object obj) {
this.obj = obj;
}
@Override
public void run() {
System.out.println("run on RunnableImplA");
synchronized (obj) {
System.out.println("obj to wait on RunnableImplA");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("obj continue to run on RunnableImplA");
}
}
}
class RunnableImplB implements Runnable {
private Object obj;
public RunnableImplB(Object obj) {
this.obj = obj;
}
@Override
public void run() {
System.out.println("run on RunnableImplB");
System.out.println("睡眠3秒...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println("notify obj on RunnableImplB");
obj.notify();
}
}
}
class RunnableImplC implements Runnable {
private Object obj;
public RunnableImplC(Object obj) {
this.obj = obj;
}
@Override
public void run() {
System.out.println("run on RunnableImplC");
System.out.println("睡眠3秒...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println("notifyAll obj on RunnableImplC");
obj.notifyAll();
}
}
}
调用notify方法,则始终有一个线程等待被唤醒,程序不会终止;调用notifyAll方法,则线程t1、t2都会执行完毕。
7. 多线程测试某个条件变化,使用if还是while?
注意,notify唤醒沉睡的县城之后,线程会继续执行上次的步骤,进行条件判定,可以把wait语句忽略不计;但是显然,需要确保程序一定会执行,并且保证程序直至满足一定的条件在执行,则需要while等待,直至满足条件后继续往下执行。
方法① 代码
class FooBar {
private int n;
private Object obj = new Object();
private volatile boolean fooExec = true;
public FooBar(int n) {
this.n = n;
}
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
synchronized (obj) {
if (!fooExec) {
//若fooExec为false,则线程等待,true执行下面步骤
obj.wait();
}
printFoo.run();
fooExec = false;
//唤醒其他线程
obj.notifyAll();
}
// printFoo.run() outputs "foo". Do not change or remove this line.
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
// printBar.run() outputs "bar". Do not change or remove this line.
synchronized (obj) {
if (fooExec) {
obj.wait();
}
printBar.run();
fooExec = true;
obj.notifyAll();
}
}
}
}
关键点:volatile
Java语言提供了一种稍弱的同步机制,也就是volatile 变量,保证变量的更新操作会告知其他线程。
若把某个变量声明为volatile,编译器在运行时都会注意到该变量存在共享行为,不会把该变量的操作和内存其他操作一起重排序。volatile 变量不会被缓存在寄存器或者其他处理器不可见的地方,在读取volatile 类型的变量时总是会返回最新写入的数值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
对于非volatile 变量读写时,每一个线程会把内存拷贝变量到CPU cache缓存中,若计算机存在多个CPU,每个线程可能会在不同的CPU处理,意味每一个线程都可以拷贝到不同的cache中。
声明为volatile 之后,JVM保证每次变量读取都从内存中,跳过CPU cache这一步。
- 当一个变量为volatile 之后,存在两种特点:
- 保证变量对所有线程可见,若某个线程修改了该变量,会保证新数值立刻更新同步到主内存中;同时每次读取也从主内存读取。
- 禁止指令重排优化,被volatile 修饰的变量,赋值之后会执行load addl $0x0, (%esp) 操作,类似内存屏障。(指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)