线程通信
synchronized 关键字
wait、notify、notifyAll 方法调用需要获取对象监视器。
synchronized (obj)
获取方式是通过synchronized 关键字实现的,synchronized 是一个重量级锁。
Java中的每一个对象都可以作为锁,这是为什么呢?
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
synchronized正是通过锁标志来判断当前对象锁是否可用,在获取锁的过程中可能会涉及到锁的升级。
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
wait/notify
public class WaitNotifyDemo {
public static void main(String[] args) {
Object object = new Object();
Thread thread=new Thread(()->{
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" over");
},"waitThread");
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
object.notify();//唤醒
}
}
没有获取对象锁,就执行wait方法,会抛出异常IllegalMonitorStateException
Exception in thread "waitThread" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at thread.WaitNotifyDemo.lambda$main$0(WaitNotifyDemo.java:10)
at java.lang.Thread.run(Thread.java:748)
对wait和notify操作加锁以后就可以正常执行了
//wait
synchronized(object){
object.wait();
}
synchronized(object){
object.notify();
}
还有如果父线程中 notify
方法不幸比子线程wait
方法先执行,那么子线程将一直等待。
上面只是一个简单例子,通常 wait / notify 不会这么应用。wait、 notify典型的应用方式是生产者、消费者。
等待方遵(消费者)循如下原则。
- 获取对象的锁。
- 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
- 条件满足则执行对应的逻辑
wait 方法的调用通常符合下面逻辑
synchronized(obj) {
//防止假醒
while(条件不满足) {
obj.wait();
}
//处理逻辑
}
通知方(生产者)遵循如下原则。
- 获得对象的锁。
- 改变条件。
- 通知所有等待在对象上的线程。
synchronized(obj) {
// 改变条件
obj.notifyAll();
}
下面实现一个 简单的无界阻塞队列。
public class BlockingArrayObject<T> {
private List<T> list = new ArrayList<>();
//生产元素
public synchronized void put(T t) {
list.add(t);
this.notifyAll();
}
//消费元素
public synchronized T pop() throws InterruptedException {
while (list.size() == 0)
this.wait();
return list.remove(0);
}
}
上面使用BlockingArrayObject 对象作为 对象监视器。
测试程序
class Demo {
@Test
public void test() {
BlockingArrayObject<String> demo = new BlockingArrayObject<>();
new Thread(() -> {
int num = 0;
for (int i = 0; i < 5; i++) {
demo.put("str" + num);
System.out.println("生产:str" + num++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
consume(demo);
// consume(demo);
}
private void consume(BlockingArrayObject demo) {
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + "消费:" + demo.pop());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
输出结果:
生产:str0
Thread-1消费:str0
生产:str1
Thread-1消费:str1
生产:str2
Thread-1消费:str2
生产:str3
Thread-1消费:str3
生产:str4
Thread-1消费:str4
Condition
在jdk1.5之后,除了使用synchronized
关键字可以实现线程同步之外还可以使用 Lock
类实现同步。
与synchronized 对比:
- Lock有更大的自主权,可以指定锁的公平性,锁的释放时机。 而synchronized 是非公平锁。
- Lock是jdk层面的锁(使用 等待队列 和 LockSupport实现 ),使用Lock 对象作对独占锁。synchronized用的锁是存在Java对象头里的。
- 二者都提供了 消息通信的方式。synchronized 使用监视器对象(调用wait/notify方法)进行通信。Lock 使用 Condition 对象(调用await/signal方法)进行通信
Lock 常用实现类:
- ReentrantLock :可重入独占锁。
- ReentrantReadWriteLock。 可重入读写锁
Lock接口中提供了 newCondition 方法来获取Condition
/**
* Returns a new {@link Condition} instance that is bound to this
* {@code Lock} instance.
*
* <p>Before waiting on the condition the lock must be held by the
* current thread.
* A call to {@link Condition#await()} will atomically release the lock
* before waiting and re-acquire the lock before the wait returns.
*
* <p><b>Implementation Considerations</b>
*
* <p>The exact operation of the {@link Condition} instance depends on
* the {@code Lock} implementation and must be documented by that
* implementation.
*
* @return A new {@link Condition} instance for this {@code Lock} instance
* @throws UnsupportedOperationException if this {@code Lock}
* implementation does not support conditions
*/
Condition newCondition();
使用Condition 的await
、signal
、signalAll
方法可以实现线程通信,功能和使用方式都与 wait、notify、notifyAll 一致。同样的 await方法 也会抛出异常InterruptedException。
下面是通Condition实现的简单的阻塞队列。
public class BlockingArrayDemo<T> {
final Lock lock = new ReentrantLock(); //锁
final Condition notFull = lock.newCondition();//条件: 队列不满
final Condition notEmpty = lock.newCondition();//条件:队列不空
final Object [] items = new Object[1]; // 队列存储元素
int putIndex, takeIndex, count;
/**
* 入队
* @param o
* @throws InterruptedException
*/
public void put(T o) throws InterruptedException {
//获取锁
lock.lock();
try{
// 队列已满等待
while(count==items.length)
notFull.await();
items[putIndex] = o;
putIndex = (putIndex++) % items.length;
count++;
//添加元素,此时队列非空
notEmpty.signal();
}finally {
//释放锁
lock.unlock();
}
}
/**
* 出队
* @return
* @throws InterruptedException
*/
public T take()throws InterruptedException{
//获取锁
lock.lock();
try{
// 队列已空等待
while(count==0)
notEmpty.await();
T takeObj = (T) items[takeIndex];
takeIndex = (takeIndex++) % items.length;
count--;
//已移除元素,此时队列非满
notFull.signal();
return takeObj;
}finally {
lock.unlock();
}
}
}
测试程序:
public static void main(String[] args) {
BlockingArrayDemo blockingArray = new BlockingArrayDemo();
new Thread(()->{
for (int i=0;i<5;i++){
try {
System.out.println("take: "+blockingArray.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
for (int i=0;i<5;i++){
try {
Thread.sleep(1000);
blockingArray.put("num: "+i);
System.out.println("put num: "+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出结果
put num: 0
take: num: 0
put num: 1
take: num: 1
put num: 2
take: num: 2
put num: 3
take: num: 3
put num: 4
take: num: 4
可以看到 使用方式和wait/notify 是一致的,本就是synchronized / (wait/notify) 替换方案。当然完全可以将Lock替换为synchronized,将Condition 替换为 普通对象
Object notFull = new Object();
Object notEmpty = new Object();
public void put(T o) throws InterruptedException {
while (count == items.length)
synchronized (notFull) {
notFull.wait();
}
items[putIndex] = o;
putIndex = (putIndex++) % items.length;
count++;
//添加元素,此时队列非空
synchronized (notEmpty) {
notEmpty.signal();
}
}
有兴趣可以了解一下博客: ReentrantLock 源码浅读 和 阻塞队列-ArrayBlockingQueue源码解析
Join
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。Thread中还提供了具有超时参数的方法,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回
介绍方法的时候提到过 join 实际上是使用wait 方法实现的。
join的使用场景是控制线程执行顺序。 比如:主线程需要等待子线程的返回结果。
public class JoinThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread previous = Thread.currentThread();
for (int i = 0; i < 4; i++) {
// 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + " terminate.");
}
static class Domino implements Runnable {
private Thread thread;
public Domino(Thread thread) {
this.thread = thread;
}
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
输出结果
main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
如果只是为了多线程计算数据,最后合并结果的话。 在juc 包下提供了join的替代方案。比如CountDownLatch,CyclicBarrier。
参考:
- 疯狂java讲义
- java并发编程的艺术