索引链接:畅游多线程问题
1114. 按序打印
方法1:synchronized
- 基础知识
1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
3、 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
4、wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。
5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
6、notify 和 notifyAll的区别
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
7、在多线程中要测试某个条件的变化,使用if 还是while?
要注意,notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑;显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while进行等待,直到满足条件才继续往下执行
class Foo {
public Foo() {
}
private int signal = 1;
private Object obj = new Object();
public void first(Runnable printFirst) throws InterruptedException {
synchronized (obj) {
while (signal != 1) {
obj.wait();
}
printFirst.run();
signal = 2;
obj.notifyAll();
}
}
public void second(Runnable printSecond) throws InterruptedException {
synchronized (obj) {
while (signal != 2) {
obj.wait();
}
printSecond.run();
signal = 3;
obj.notifyAll();
}
}
public void third(Runnable printThird) throws InterruptedException {
synchronized (obj) {
while (signal != 3) {
obj.wait();
}
printThird.run();
signal = 1;
obj.notifyAll();
}
}
}
方法2:synchronized
- 变量的方式控制线程的执行
class Foo {
boolean first = false;
boolean second = false;
Object obj = new Object();
public Foo() {
}
public void first(Runnable printFirst) throws InterruptedException {
synchronized (obj) {
first = true;
printFirst.run();
obj.notifyAll();//唤醒其他线程
}
}
public void second(Runnable printSecond) throws InterruptedException {
synchronized (obj) {
while (!first) {//当线程1执行的时候,线程2在等待,线程1设置了first=true,while条件不成立,线程2开始执行
obj.wait();
}
printSecond.run();
second = true;
obj.notifyAll();
}
}
public void third(Runnable printThird) throws InterruptedException {
synchronized (obj) {
while (!second) {
obj.wait();
}
printThird.run();
obj.notifyAll();
}
}
}
方法3:Semaphore
Semaphore
是一个计数信号量。- 从概念上将,
Semaphore
包含一组许可证。 - 如果有需要的话,每个
acquire()
方法都会阻塞,直到获取一个可用的许可证。 - 每个
release()
方法都会释放持有许可证的线程,并且归还Semaphore
一个可用的许可证。 - 然而,实际上并没有真实的许可证对象供线程使用,
Semaphore
只是对可用的数量进行管理维护 - 总结:如果线程要访问一个资源就必须先获得信号量。如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0.当信号量使用完时,必须释放
class Foo {
Semaphore s12 = new Semaphore(0);
Semaphore s23 = new Semaphore(0);
public Foo() {
}
public void first(Runnable printFirst) throws InterruptedException {
printFirst.run();
s12.release();//释放后s12的值会变成1
}
public void second(Runnable printSecond) throws InterruptedException {
s12.acquire();//没有会阻塞 当为1的时候,说明线程2可以拿到s12了
printSecond.run();
s23.release();//释放后s23的值会变成1
}
public void third(Runnable printThird) throws InterruptedException {
s23.acquire();//0的时候拿不到,1的时候可以拿到
printThird.run();
}
}
方法4:CountDownLatch
-
CountDownLatch
这个类使一个线程等待其他线程各自执行完毕后再执行。 -
是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };
class Foo {
CountDownLatch latch12 = new CountDownLatch(1);
CountDownLatch latch23 = new CountDownLatch(1);
public Foo() {
}
public void first(Runnable printFirst) throws InterruptedException {
printFirst.run();
latch12.countDown();//唤醒线程2
}
public void second(Runnable printSecond) throws InterruptedException {
latch12.await();//latch12的值为0会执行下面的语句,否则会在此次阻塞
printSecond.run();
latch23.countDown();//准备唤醒线程3
}
public void third(Runnable printThird) throws InterruptedException {
latch23.await();//latch23的值为0会执行下面的语句,否则会在此次阻塞
printThird.run();
}
}
方法5:SynchronousQueue
SynchronousQueue
是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put
的时候),如果当前没有人想要消费产品(即当前没有线程执行take
),此生产线程必须阻塞,等待一个消费线程调用take
操作,take
操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take
后put
,原理是一样的)
class Foo {
//阻塞队列 //同步队列,没有容量,进去一个元素,必须等待取出来以后,才能再往里面放一个元素
BlockingQueue<Integer> block12 = new SynchronousQueue<Integer>();
BlockingQueue<Integer> block23 = new SynchronousQueue<Integer>();
public Foo() {
}
public void first(Runnable printFirst) throws InterruptedException {
printFirst.run();
block12.put(1);
}
public void second(Runnable printSecond) throws InterruptedException {
block12.take();//
printSecond.run();
block23.put(1);
}
public void third(Runnable printThird) throws InterruptedException {
block23.take();
printThird.run();
}
}
方法6:ReentrantLock+Condition
Condition可以通俗的理解为条件队列。当一个线程在调用了await方法以后,直到线程等待的某个条件为真的时候才会被唤醒。这种方式为线程提供了更加简单的等待/通知模式。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
- await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
- await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
- awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
- awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
- awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
- signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
- signal()All :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。
class Foo {
int num;
Lock lock;
//精确的通知和唤醒线程
Condition condition1, condition2, condition3;
public Foo() {
num = 1;
lock = new ReentrantLock();
condition1 = lock.newCondition();
condition2 = lock.newCondition();
condition3 = lock.newCondition();
}
public void first(Runnable printFirst) throws InterruptedException {
lock.lock();
try {
while (num != 1) {//不是1的时候,阻塞
condition1.await();
}
// printFirst.run() outputs "first". Do not change or remove this line.
printFirst.run();
num = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void second(Runnable printSecond) throws InterruptedException {
lock.lock();
try {
while (num != 2) {//不是2的时候,阻塞
condition2.await();
}
// printSecond.run() outputs "second". Do not change or remove this line.
printSecond.run();
num = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void third(Runnable printThird) throws InterruptedException {
lock.lock();
try {
while (num != 3) {//不是3的时候,阻塞
condition3.await();
}
// printThird.run() outputs "third". Do not change or remove this line.
printThird.run();
num = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
方法7:LockSupport
- LockSupport类的核心方法其实就两个:
park()
和unpark()
,其中park()
方法用来阻塞当前调用线程,unpark()
方法用于唤醒指定线程。
这其实和Object类的wait()和signal()方法有些类似,但是LockSupport的这两种方法从语意上讲比Object
类的方法更清晰,而且可以针对指定线程进行阻塞和唤醒。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。
初始时,permit为0,当调用unpark()
方法时,线程的permit加1,当调用park()
方法时,如果permit为0,则调用线程进入阻塞状态。
class Foo {
private AtomicInteger counter = new AtomicInteger(0);
private Map<String, Thread> threads = new HashMap<>();
public Foo() {
}
public void first(Runnable printFirst) throws InterruptedException {
while (counter.get() != 0) {
threads.put("first", Thread.currentThread());
LockSupport.park();
}
printFirst.run();
counter.getAndIncrement();
threads.forEach((k, v) -> LockSupport.unpark(v));
}
public void second(Runnable printSecond) throws InterruptedException {
while (counter.get() != 1) {
threads.put("second",Thread.currentThread());
LockSupport.park();
}
printSecond.run();
counter.getAndIncrement();
threads.forEach((k, v) -> LockSupport.unpark(v));
}
public void third(Runnable printThird) throws InterruptedException {
while (counter.get() != 2) {
threads.put("third",Thread.currentThread());
LockSupport.park();
}
printThird.run();
counter.getAndIncrement();
threads.forEach((k, v) -> LockSupport.unpark(v));
}
}