同步工具-Synchronizer

1.状态依赖性
  在一个单线程环境中,如果调用一个方法之前 没有满足进入该方法的先决条件(比如要去一个空的队列里获取对象),那么这个方法就永远不能进入。因为这个先决条件永远不会改变,所以这个方法就只能返回失败了。该方法依赖对象的状态。
  而在多线程的环境中,这个先决条件很有可能被其他线程所改变,例如A线程想要从一个空的连接池中获取一个连接,当然此刻A线程的操作是失败的,但是后续的B线程归还了一个连接对象,这时如果A线程再去尝试的话会成功。下面有几种方法来实现为客户端构建获取连接的方法。

1.1 轮询重试(sleep)

public V take(){
	//先获取保护对象状态的锁,使得在接下来的操作中对象的状态不会被改变。
	acquire lock on object
	//判断对象状态是否满足先决条件(此时是容器中没有对象了)
	while(state not hold){
			//不满足条件则释放锁,等待其他线程能过获取锁并改变对象状态(别的线程能够添加对象)
			release lock
			//当前线程等待一段时间
			wait for preconditon might hold
			//重新尝试获取锁,继续轮询
			reacquire lock
	}
	//此时是已经获取了条件的 可以进行业务逻辑 
	V v=doTavke();
	//释放锁
	release lock;
	return v;
}

轮询休眠费力不讨好的解决状态依赖性的问题。鬼知道你唤醒的时候,当前的状态是否是满足你的条件呢?这样导致线程频繁进行上下文切换,累死cpu了。或者你睡过头了呢(线程休眠的时候状态已经满足了,没有及时的醒来)。
  轮询休眠间隔时间越小性能越好,cpu消耗越高。时间间隔越大,性能越差,cpu消耗越小。

1.2条件队列实现方式(wait notify notifyAll)
  条件队列可以解决轮询休眠的睡过头,或者太早唤醒的问题。

public V take(){
	//先获取对象的锁 保证运行时对象状态不变
	synchronized(this){
		//判断是否满足先决条件
		while(notEmpty()){
			//不满足条件 先释放锁,然后阻塞等待其他线程唤醒当前线程,
			//唤醒之后自动获取当前对象的锁 然后继续轮询
			this.wait();
		}
		//满足执行条件 获取返回对象
		//返回队列中的对象 改变当前对象的状态(size-1);
		V v= doTake();
		//唤醒其他线程
		this.notifyAll();
		//返回 
		return v;
	}
	//自动释放对象的锁
}
public void put(V v){
	//获取当前对象的锁 保持对象状态不变
	synchronized(this){
		//判断是否满足先决条件
		while(notFull()){
			//不满足条件 先释放锁,然后阻塞等待其他线程唤醒当前线程,
			//唤醒之后自动获取当前对象的锁 然后继续轮询
			this.wait();
		}
		//改变当前对象的状态(size+1);
		doPut(v);
		//唤醒其他线程
		this.notifyAll();
	}
	//自动释放对象锁
}

·
  每个对象都能构成条件队列(条件队列的方法时Object提供的),条件队列包括this对象,条件队列中的每个元素都是Thread,通过this对象的api来控制队列中的线程阻塞或者运行。
  和轮询的方案很像,take()时wait会释放调用对象的锁,然后当前线程加入条件队列,阻塞等待被唤醒,被唤醒时首先去获取对象的锁,然后判断先决条件,满足的话才执行操作。最后notifyAll会唤醒所有条件队列中等待被唤醒的线程。
  1.注意notify与notifyAll,notifyAll唤醒所有处于条件队列中的线程,notify唤醒一个条件队列的线程。如果put()唤醒的是另一个等待put()的线程,那么尽管对象的状态满足take(),但是等待take()的线程将不会执行。
  2.注意wait notify notifyAll必须在synchronized里面调用。
  3.注意synchronized对象和调用wait notify notifyAll的对象必须是同一个。
  每次调用wait时都会隐式的与条件谓词相关联,而条件谓词则与对象的状态有关,而对象的状态是由对象来保护的,所以每次wait时调用者必须持有了与条件队列相关的锁,这个锁同时还保护着对象的状态
  Q1 为什么wait notify notifyAll 必须要在synchronized里调用?
  因为"要先获取对象的状态"才能“判断是否需要等待”;“先要能够改变对象的状态”才能“唤醒等待队列中休眠的线程”。
  Q2 为什么synchronized的对象 与wait notify notifyAll的对象是同一个?
  因为每个对象对应这个一个条件队列和同步队列,wait的操作首先会释放对象的锁,然后把当前线程放入前面锁住的对象的条件队列里。如果当前线程获取o1的锁,然后调用o2的wait方法(这里其实是会抛异常,我们假设不会),释放o2的锁,那么o1的锁将永远不会释放,线性休眠,并且没有别的线程能够获取o1的锁。
  优点:通过唤醒这一机制能够“及时的通知等待的线程”。
  缺点:notifyAll效率不高,所有的线程都会是串行的,唤醒的时间复杂度最高是O(n^2),n个线程等待n中条件,最欢的情况就是条件一到第N个线程才被唤醒 条件2到第N-1个线程才被唤醒唤醒的次数n+(n-1)+(n-2)+…+1 。
  条件队列可以对应多个条件谓词 例如一个队列中存在多个等待被唤醒的put和take线程。所以当notifyAll的时候不能做到精确的唤醒某部分等待的线程。当线程被唤醒的时候很有可能在他获取锁之前 条件谓词已经变成假了,或者说条件谓词没有真过。
  
1.3使用显示锁与显式条件队列对象(Lock-Contion)
  正如上面所说的notifyAll问题,唤醒了条件队列中的wait线程,而这些线程都是要串行的获取锁
并判断是否能够执行,很有可能被唤醒,获取锁之后得到对象状态并不是自己期望的那样,从而继续调用wait,这样效率实在是太低了。
这里的Lock Condition对象能够处理这一问题。就像显示锁(Lock)与内部锁(Synchronzied)一样显式锁也必须满足锁,条件谓词,条件变量一样。涉及的变量由Lock保护,检查条件谓词以及调用await和signal时必须持有Lock对象。
不同的是内部锁 锁:条件队列:条件谓词 满足1:1:N,而显式锁可以满足1:N:N;
这就使得显式锁可以针对不同的条件谓词唤醒不同的条件队列(精确唤醒)

	// 显式锁
	private Lock lock = new ReentrantLock();
	// 条件谓词 未满
	private Condition notFull = lock.newCondition();
	// 条件谓词 未空
	private Condition notEmpty = lock.newCondition();

	public void put(V v) throws InterruptedException {
		lock.lock();
		try {
			while (size == maxSize) {
				notFull.await();
			}
			doPut();
			++size;
			notEmpty.signal();
		} finally {
			lock.unlock();
		}
	}

	public V take() throws InterruptedException {
		lock.unlock();
		try {
			while (size == 0) {
				notEmpty.await();
			}
			size--;
			notFull.signal();
			return doTake();
		} finally {
			lock.unlock();
		}
	}


  这里因为能够精准的通知对应的条件队列,所以不需要时用signalAll。对于ReentrantLock于 synchronized之间的选择,如果需要使用高级特性的话,例如tryLock,公平锁,多个条件队列的话,我相信我已经做出了选择。

你可能感兴趣的:(并发)