竞争与同步是多线程中无可避免的问题,同步机制有很多。下文就java中的常见的同步机制进行简述。

java线程有五种状态跟操作系统所描述的五种状态有些区别:

New(新生,刚出生的准备工作还没做足,还不可执行)

Runable(可运行,处于这种状态的线程可能已经处于运行当中了也可能没有运行,但准备工作做好了可以被调用了,是操作系统中就绪与运行态的集合)

Blocked(被阻塞)

Waiting(等待)

Timed waiting(计时等待)

Terminated(被终止)

首先得说说java线程的暂停,正在执行的线程暂停了包括上述状态的三种情况一种是阻塞(Blocked)一种是等待(Waiting),还有一种是计时等待(Timed waiting),阻塞与等待看似相同实则有本质的区别。阻塞是一种被动的方式,是线程与另一线程去竞争某一资源(为对象锁)时失败了,没办法虽然自己不愿意但也只得被卡在那里知道竞争成功的线程释放此资源。而等待为一个主动的行为,可以无私奉献调用Thread的实例方法join跟到别的线程屁股后面去执行,也可以调用Thread.sleep静态方法去睡会觉让其他线程去执行自己睡醒了在来执行,而Thread.yield静态方法更高风亮节了直接死后投胎回到了可执行状态等待下一次被调用。另一种就是当锁对象的条件不满足时可调用Object的实例方法wait或Condition的实例方法await进入等待状态直到被其他的线程唤醒。

最后一种也应该算是等待,只不过是计时等待,Thread.sleep方法是个计时等待,wait,await,join方法都有计时版。

1.Lock/Condition显式锁:

这是java的显式锁java.util.concurrent.locks,一种常用的是可重入锁ReentranLock,每个java对象都可拥有一个或多个显式锁。锁机制确任何时刻只有一个线程进入临界区。线程通过调用锁对象的lock实例方法获得锁,调用unlock实例方法释放锁。如:

 

public class MyClass{

  private Lock lock = ReentrantLock();

  public void myMethod(){

    lock.lock();

    ...

    lock.unlock();

  }

}

通常,线程进入某一临界区后,发现在某一条件满足之后才能执行,此时可以使用一个条件对象来管理那些已经获得了一个锁但是却因缺乏某些条件而无法工作的线程,一个锁对象可拥有一个或多个条件对象,以管理不同的条件。如:

public class MyClass{

  private Lock lock = ReentranLock();

  private Condition con;

  ...

  public void myMethod(){

  ...

  con = lock.newConditon();

  ...

}

...

}

当发现条件不满足时调用await方法:condition.await(),一旦线程调用条件对象的await方法他将进入该条件的等待集合里,直到其他的进程调用条件的signalAll方法:condition.signalAll(),此方法的调用会激活因这个条件而等待的所有线程。处于此条件等待集的对象再次成为可运行状态,并且同时又竞争这个条件所属的锁,一旦获得锁,获得锁的线程会从await方法返回继续执行。对await方法的调用形式如下:

while(条件不满足)

  condition.await();

需要注意的是一旦线程调用await方法,自身将处于等待状态没办法自己激活自身。所以要保证当该线程所需条件满足是有其他的线程唤醒他,要不就永远等待了。Condition对象还有个signal方法,这个方法随机解除该条件等待集合中的一个线程。

另一个比较好用的锁是可重入读写锁ReentrantReadWriteLock,写入锁提供了一个 Condition 实现,对于写入锁来说,该实现的行为与 ReentrantLock.newCondition() 提供的 Condition 实现对 ReentrantLock 所做的行为相同。当然,此 Condition 只能用于写入锁。 读取锁不支持 Condition,readLock().newCondition() 会抛出 UnsupportedOperationException。使用读写锁的步骤:

1)构造ReentrantReadWriteLock对象

private ReentrantReadWrite rwl = new ReentrantReadWriteLock();

2)抽取读写锁:

private Lock readLock = rwl.readLock();

private Lock writeLock = writeLock();

3)加读锁,读锁会排斥所以的写操作,但允许共享读操作:

...

readLock.lock()

try{...}

finally{readLock.unlock();}

...

4)加写锁,写锁排斥所以的读写操作

...

writeLock.lock();

try{...}

finally {writeLock.unlock();}

...

 锁机制中要注意的一个是,因为java是面向对象的。所以无论是阻塞还是条件等待等行为都是对同一对象的同一个锁的同一个条件来说的,只有不同的线程在调用同一个对象想要竞争同一锁时才会发生竞争才会有阻塞。同样signal/signalAll方法唤醒的是在同一个对象的同一个锁下的等待集中的线程。而不是相对于某个类的所有对象来说的,不同的对象即使同属于一个类,但它们的锁以及锁的条件是相互没有关系的。所以锁及锁的条件是对象持有的。

2.隐式锁:synchronized关键字

java的每一对象都有唯一的一个内部锁,没个内部锁都有唯一的一个条件。如果一个方法用了synchronizedf关键字声明,那么对象的内部锁将保护整个方法。也即说,要调用该对象的这个方法,线程必须同其他想获得锁的线程竞争这个唯一的内部锁。由于内部锁及其条件只有一个,所以Object对象实现了wait实例方法,wait方法同Condition方法具有一样的功能,将调用该方法的线程加入到唯一的等待集中。而类似于Condition的signal/signalAll方法,Object实现了notify/notifyAll方法解除等待的阻塞状态。这也是为什么Condition类的类似方法被命名为await、signal、signalAll的原因,因为Object已经把“正宗的“方法名称用了。
内部锁虽然简单但有些限制:

1.不能中断正在试图获得锁的线程

2.试图获得锁时不能设置超时

3.锁只有一个并且内部锁只有一个单一的条件,可能不够用

如何选择?能够用synchronized解决的尽量使用,而不使用Lock/Condition机制。最好都不用,而用java.util.concurrent包中的其他机制解决同步问题。下面就将一个不用锁的同步机制阻塞队列BlockingQueue:

3.阻塞队列BlockingQueue

阻塞队列在线程协调工作时很有用,比如生产者与消费者的问题。生产者生产产品投入缓冲池中,消费者从缓冲池中拿取产品,当缓存池满时生存者阻塞直到缓存池中有空位,同样消费者不能从空池中拿产品,缓存池空时消费者阻塞直到生产者向池中投入产品。阻塞队列就是做这事的。如果将阻塞队列当作线程管理工具时要使用put和take方法(其他所有方法可参考API)。当阻塞队列满时调用put方法会阻塞直到队列空出位置,当阻塞队列空时调用take方法也会阻塞直到对不为空。阻塞队列会管理所有使用此队列的线程,平衡线程的运行,而无需人工干扰。阻塞队列有几个变种:

LinkedBlockingQueue,这中阻塞队列的容量没上线等于是缓存池无限,但也可以指定容量。

ArrayBlockingQueue,有Link一般都会有Array的,ArrayBlockingQueue在构造时必须指定容量,并且有个可选参数来指定是否需要公平性。

PriorityBlockingQueue是一个带有优先级别的阻塞队列,该队列没有上线,但如果是空的也会阻塞take操作。

其他的同步机制还有Volatile域同步器等机制,没研究,要使用的时候在去看看。