java并发编程笔记(三)--管程(五)

8.活跃度

死锁发生的条件

QQ截图20220223180150

QQ截图20220223180159

定位死锁的方法

QQ截图20220224165705

死锁的典型问题–哲学家问题

QQ截图20220224165932

QQ截图20220224170016

我们检测下死锁

QQ截图20220224170043

QQ截图20220224170344

就比如,我们把随后一个哲学家阿基米德拿筷子的顺序更改,就不会发生死锁。

变成:

new Philosopher("阿基米德", c5, c1).start();

但是这种解锁方法会引起饥饿。使得阿基米德哲学家很难吃饭。

更好的解决办法后面有叙述。。。

活锁

活锁出现在两个线程互相改变对方的结束条件,后谁也无法结束。

避免活锁的方法

在线程执行时,中途给予不同的间隔时间即可。

死锁与活锁的区别

  • 死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞停止运行的现象。
  • 活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。

public class TestLiveLock {
 
    static volatile int count = 10;
  
    static final Object lock = new Object();
 
public static void main(String[] args) {
 new Thread(() -> {
 // 期望减到 0 退出循环

     while (count > 0) {
 
         sleep(0.2);
 
         count--;
 
         log.debug("count: {}", count);

     }
     
 }, "t1").start();
 
 new Thread(() -> {
 // 期望超过 20 退出循环

     while (count < 20) {
 
         sleep(0.2);
 
         count++;
 
         log.debug("count: {}", count);
 
     }
 
 }, "t2").start();
 }
}

饥饿

某些线程因为优先级太低,导致一直无法获得资源的现象。,在使用顺序加锁时,可能会出现饥饿现象。如上面我说的更改阿基米德拿筷子的顺序。

原来的死锁问题分析:

QQ截图20220224171112

顺序加锁解决方案

QQ截图20220224171124

9.ReentrantLock

对于上述的问题,使用ReentrantLock类会有更好的解决办法。

下面我们来详细说说它的方法。

基本语法:

//获取ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
//加锁
lock.lock();
try {
	//需要执行的代码
}finally {
	//释放锁
	lock.unlock();
}

ReentrantLock的特点

ReentrantLock和synchronized相比具有的的特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁 (先到先得)
  • 支持多个条件变量( 具有多个waitset)

相同点:可重入

可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

QQ截图20220224171607

假如不具有可重入性,在method2时就没办法获得锁。

可中断-lock.lockInterruptibly()

如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,获得锁失败。

简而言之就是:处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行

public static void main(String[] args) {
		ReentrantLock lock = new ReentrantLock();
		Thread t1 = new Thread(()-> {
			try {
				//加锁,可打断锁
				lock.lockInterruptibly();
			} catch (InterruptedException e) {
				e.printStackTrace();
                //被打断,返回,不再向下执行
				return;
			}finally {
				//释放锁
				lock.unlock();
			}

		});

		lock.lock();
		try {
			t1.start();
			Thread.sleep(1000);
			//打断
			t1.interrupt();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

可以设置超时时间(锁超时)-lock.tryLock

使用lock.tryLock方法会返回获取锁是否成功。如果成功则返回true,反之则返回false。

并且tryLock方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit), 其中timeout为最长等待时间,TimeUnit为时间单位

简而言之就是:获取失败了、获取超时了或者被打断了,不再阻塞,直接停止运行

不设置等待时间

使用方法
public static void main(String[] args) {
		ReentrantLock lock = new ReentrantLock();
		Thread t1 = new Thread(()-> {
            //未设置等待时间,一旦获取失败,直接返回false
			if(!lock.tryLock()) {
				System.out.println("获取失败");
                //获取失败,不再向下执行,返回
				return;
			}
			System.out.println("得到了锁");
			lock.unlock();
		});


		lock.lock();
		try{
			t1.start();
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
设置等待时间
public static void main(String[] args) {
		ReentrantLock lock = new ReentrantLock();
		Thread t1 = new Thread(()-> {
			try {
				//判断获取锁是否成功,最多等待1秒
				if(!lock.tryLock(1, TimeUnit.SECONDS)) {
					System.out.println("获取失败");
					//获取失败,不再向下执行,直接返回
					return;
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
				//被打断,不再向下执行,直接返回
				return;
			}
			System.out.println("得到了锁");
			//释放锁
			lock.unlock();
		});


		lock.lock();
		try{
			t1.start();
			//打断等待
			t1.interrupt();
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
使用 tryLock 解决哲学家就餐问题

QQ截图20220224172325

可以设置为公平锁

在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。

//默认是不公平锁,需要在创建时指定为公平锁
ReentrantLock lock = new ReentrantLock(true);

这样就是先进入entryList的线程等锁释放就先运行。

公平锁一般没有必要,会降低并发度,后面分析原理时会讲解。

测试

QQ截图20220224172529

QQ截图20220224172537

支持多个条件变量(多个等待区)

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执

有了多个等待区,我们就可以避免虚假唤醒,从而简化while-wait:

我们回到很久之前的外卖和烟的问题

QQ截图20220224172808 QQ截图20220224172825

QQ截图20220224172830

使用了两个等待区,就不用担心错误的唤醒了(虚假唤醒)。

10.同步模式之顺序控制

我们在工作中有可能遇到这样一种情况,线程一要完成1操作,线程二要完成 2操作,但是规定2 操作必须在 1操作之前。

我们要如何保证呢?

wait notify 解决

// 用来同步的对象
static Object obj = new Object();

// t2 运行标记, 代表 t2 是否执行过
static boolean t2runed = false;

public static void main(String[] args) {

 
 Thread t1 = new Thread(() -> {
 
     synchronized (obj) {
 
         // 如果 t2 没有执行过
 
         while (!t2runed) { 
 try {
 
     // t1 先等一会
 
     obj.wait(); 
 } catch (InterruptedException e) {
 
     e.printStackTrace();
  
 }}}
 
     System.out.println(1);
 });
 
 Thread t2 = new Thread(() -> {
 
     System.out.println(2);
 synchronized (obj) {
 
     // 修改运行标记
 
     t2runed = true;
 
     // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
 
     obj.notifyAll();
 }
 });
 
    t1.start();
 
    t2.start();
}

Park Unpark解决

可以看到,实现上很麻烦:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait
  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个

我们使用park来解决

Thread t1 = new Thread(() -> {
 try { 
     Thread.sleep(1000); 
 } catch (InterruptedException e) {
 
 	}
 // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行

    LockSupport.park();
 
    System.out.println("1");
});
Thread t2 = new Thread(() -> {
 
    System.out.println("2");
 
    // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
    LockSupport.unpark(t1);
});

t1.start();
t2.start();

注意,不要用join因为先唤醒的如果是2线程,就起不到作用。

11.交替输出问题

在生产过程中,我们也可能遇到这样的问题,三个线程,按顺序依次进行操作,我们必须保证操作顺序符合,同时也要保证执行了一定次数。

wait notify解决

public class Test4 {
	static Symbol symbol = new Symbol();
	public static void main(String[] args) {
		new Thread(()->{
			symbol.run("a", 1, 2);
		}).start();

		new Thread(()->{
			symbol.run("b", 2, 3);

		}).start();
		
		new Thread(()->{
        symbol.run("c", 3, 1);
		}).start();
	}
}

class Symbol {
	public synchronized void run(String str, int flag, int nextFlag) {
		for(int i=0; i<loopNumber; i++) {
			while(flag != this.flag) {
				try {
					this.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(str);
			//设置下一个运行的线程标记
			this.flag = nextFlag;
			//唤醒所有线程
			this.notifyAll();
		}
	}

	/**
	 * 线程的执行标记, 1->a 2->b 3->c
	 */
	private int flag = 1;
	private int loopNumber = 5;

	public int getFlag() {
		return flag;
	}

	public void setFlag(int flag) {
		this.flag = flag;
	}

	public int getLoopNumber() {
		return loopNumber;
	}

	public void setLoopNumber(int loopNumber) {
		this.loopNumber = loopNumber;
	}
}

await/signal解决

public class Test5 {
	static AwaitSignal awaitSignal = new AwaitSignal();
	static Condition conditionA = awaitSignal.newCondition();
	static Condition conditionB = awaitSignal.newCondition();
	static Condition conditionC = awaitSignal.newCondition();
	public static void main(String[] args) {
		new Thread(()->{
			awaitSignal.run("a", conditionA, conditionB);
		}).start();

		new Thread(()->{
			awaitSignal.run("b", conditionB, conditionC);
		}).start();

		new Thread(()->{
			awaitSignal.run("c", conditionC, conditionA);
		}).start();


		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		awaitSignal.lock();
		try {
            //唤醒一个等待的线程
			conditionA.signal();
		}finally {
			awaitSignal.unlock();
		}
	}
}

class AwaitSignal extends ReentrantLock{
	public void run(String str, Condition thisCondition, Condition nextCondition) {
		for(int i=0; i<loopNumber; i++) {
			lock();
			try {
                //全部进入等待状态
				thisCondition.await();
				System.out.print(str);
				nextCondition.signal();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				unlock();
			}
		}
	}

	private int loopNumber=5;

	public int getLoopNumber() {
		return loopNumber;
	}

	public void setLoopNumber(int loopNumber) {
		this.loopNumber = loopNumber;
	}
}

Park Unpark解决

class SyncPark {
 
    private int loopNumber;
 
    private Thread[] threads;
 
 public SyncPark(int loopNumber) {
 
     this.loopNumber = loopNumber;
 }
 
 public void setThreads(Thread... threads) {
 
     this.threads = threads;
 }
 
 public void print(String str) {
 for (int i = 0; i < loopNumber; i++) {
 
     LockSupport.park();
 
     System.out.print(str);
 
     LockSupport.unpark(nextThread());
 }
 }

private Thread nextThread() {

    Thread current = Thread.currentThread();
 
    int index = 0;
 for (int i = 0; i < threads.length; i++) {
 
     if(threads[i] == current) {
 
         index = i;
 
         break;
 }
 }
 if(index < threads.length - 1) {
 
     return threads[index+1];

 } else {
 
     return threads[0];
 }
 }
 public void start() {
 for (Thread thread : threads) {

     thread.start();
 }
 
     LockSupport.unpark(threads[0]);
 }
}


SyncPark syncPark = new SyncPark(5);
Thread t1 = new Thread(() -> {

    syncPark.print("a");
});
Thread t2 = new Thread(() -> {
 
    syncPark.print("b");
});
Thread t3 = new Thread(() -> {
 
    syncPark.print("c\n");
});

syncPark.setThreads(t1, t2, t3);

syncPark.start();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值