索引链接:畅游多线程问题
1116. 打印零与奇偶数
方法1:Semaphore
Semaphore
是一个计数信号量。- 从概念上将,
Semaphore
包含一组许可证。 - 如果有需要的话,每个
acquire()
方法都会阻塞,直到获取一个可用的许可证。 - 每个
release()
方法都会释放持有许可证的线程,并且归还Semaphore
一个可用的许可证。 - 然而,实际上并没有真实的许可证对象供线程使用,
Semaphore
只是对可用的数量进行管理维护 - 总结:如果线程要访问一个资源就必须先获得信号量。如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0.当信号量使用完时,必须释放
class ZeroEvenOdd {
private int n;
private Semaphore zeroSema = new Semaphore(1);
private Semaphore oddSema = new Semaphore(0);//奇数
private Semaphore evenSema = new Semaphore(0);//偶数
public ZeroEvenOdd(int n) {
this.n = n;
}
public void zero(IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i++) {
zeroSema.acquire();
printNumber.accept(0);
if ((i & 1) == 1) {//奇数
oddSema.release();
} else {
evenSema.release();
}
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i++) {
if ((i & 1) == 0) {//偶数 打印偶数 并释放zero的线程
evenSema.acquire();
printNumber.accept(i);
zeroSema.release();
}
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i++) {
if ((i & 1) == 1) {//奇数,打印奇数,并释放zero的线程
oddSema.acquire();
printNumber.accept(i);
zeroSema.release();
}
}
}
}
方法2: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() { };
CyclicBarrier 与 CountDownLatch 区别
- CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
- CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的
class ZeroEvenOdd {
private int n;
private CountDownLatch zeroLatch = new CountDownLatch(0);
private CountDownLatch evenLatch = new CountDownLatch(1);//偶数
private CountDownLatch oddLatch = new CountDownLatch(1);//奇数
public ZeroEvenOdd(int n) {
this.n = n;
}
public void zero(IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i++) {
zeroLatch.await();
printNumber.accept(0);//打印0
zeroLatch = new CountDownLatch(1);
if ((i & 1) == 1) oddLatch.countDown();//如果是奇数,就打印奇数
else evenLatch.countDown();
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i++) {
if ((i & 1) == 0) {
evenLatch.await();//开始打印偶数
printNumber.accept(i);
evenLatch = new CountDownLatch(1);
zeroLatch.countDown();//是否zero线程
}
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i++) {
if ((i & 1) == 1) {
oddLatch.await();//开始打印奇数
printNumber.accept(i);
oddLatch = new CountDownLatch(1);
zeroLatch.countDown();//是否zero线程
}
}
}
}
方法3:Thread.yield()
Thread.yield():使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。
class ZeroEvenOdd {
private int n;
private volatile int state;
private volatile boolean control = true;
public ZeroEvenOdd(int n) {
this.n = n;
}
public void zero(IntConsumer printNumber) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (state != 0) {
Thread.yield();
}
printNumber.accept(0);
if (control) {
state = 1;
} else {
state = 2;
}
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
for (int i = 2; i <= n; i += 2) {
while (state != 2) {//当state不为2的时候,为就绪状态
Thread.yield();
}
printNumber.accept(i);
control = true;
state = 0;
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i += 2) {
while (state != 1) {
Thread.yield();
}
printNumber.accept(i);
control = false;
state = 0;
}
}
}
方法4: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 ZeroEvenOdd {
private int n;
private Map<String, Thread> map = new ConcurrentHashMap<>();
volatile int state = 0;//0 打印 0 , 1 打印奇数, 2 打印偶数
public ZeroEvenOdd(int n) {
this.n = n;
}
public void zero(IntConsumer printNumber) throws InterruptedException {
map.put("zero", Thread.currentThread());
for (int i = 1; i <= n; i++) {
while (state != 0) {
LockSupport.park();
}
printNumber.accept(0);
if ((i & 1) == 0) {//偶数
state = 2;
} else {
state = 1;
}
map.forEach((k, v) -> LockSupport.unpark(v));//通知其他两个线程
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
map.put("even", Thread.currentThread());
for (int i = 2; i <= n; i += 2) {
while (state != 1) {//当为2的时候,一直在这里阻塞着
LockSupport.park();
}
printNumber.accept(i);
state = 0;
LockSupport.unpark(map.get("zero"));//通知zero线程
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
map.put("odd", Thread.currentThread());
for (int i = 1; i <= n; i += 2) {
while (state != 2) {
LockSupport.park();
}
printNumber.accept(i);
state = 0;
LockSupport.unpark(map.get("zero"));
}
}
}
方法5:Thread.yield()
AtomicInteger
变量控制
class ZeroEvenOdd {
private int n;
private AtomicInteger ai = new AtomicInteger(0);
public ZeroEvenOdd(int n) {
this.n = n;
}
public void zero(IntConsumer printNumber) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (ai.get() != 0 && ai.get() != 2) {
Thread.yield();
}
printNumber.accept(0);
ai.incrementAndGet();
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
for (int i = 2; i <= n; i += 2) {
while (ai.get() != 3) {
Thread.yield();
}
printNumber.accept(i);
ai.set(0);
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
for (int i = 1; i <= n; i++) {
while (ai.get() != 1) {
Thread.yield();
}
printNumber.accept(i);
ai.set(2);
}
}
}
方法6:ReentrantLock+Condition
背景知识
实现原理
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现
class ZeroEvenOdd {
private int n;
private volatile int start = 1;
private volatile int state;
private Lock lock = new ReentrantLock();
private Condition zero = lock.newCondition();
private Condition even = lock.newCondition();
private Condition odd = lock.newCondition();
public ZeroEvenOdd(int n) {
this.n = n;
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void zero(IntConsumer printNumber) throws InterruptedException {
lock.lock();
try {
while (start <= n) {
if (state != 0) {
zero.await();
}
printNumber.accept(0);
if (start % 2 == 0) {
state = 2;
even.signal();
} else {
state = 1;
odd.signal();
}
zero.await();
}
odd.signal();
even.signal();
} finally {
lock.unlock();
}
}
//偶数
public void even(IntConsumer printNumber) throws InterruptedException {
lock.lock();
try {
while (start <= n) {
if (state != 2) {
even.await();
} else {
printNumber.accept(start++);
state = 0;
zero.signal();
}
}
} finally {
lock.unlock();
}
}
//基数
public void odd(IntConsumer printNumber) throws InterruptedException {
lock.lock();
try {
while (start <= n) {
if (state != 1) {
odd.await();
} else {
printNumber.accept(start++);
state = 0;
zero.signal();
}
}
} finally {
lock.unlock();
}
}
}