使用 synchronized修饰,表示该方法是加锁的方法。使用相同this锁的方法,在任意时刻只有一个方法会被执行,在多线程中是竞争关系。除此之外多线程还存在依赖关系。例如,一个线程须等待另一个线程返回结果后,才能继续执行。Java中提供了相应的机制。
考虑一个实际的流水线作业场景,一个线程负责生产产品,另一个线程负责在流水线上装配。由于生产时间不确定,为了不错过传送带上的产品,装配线程需要不断检查当前传送带上有无产品。为了简化逻辑,这里只有一个线程负责生产,一个线程负责装配。
import java.util.Arrays; import java.util.LinkedList; import java.util.Queue; public class ThreadDispatch { public static void main(String[] args) { var pack = new PackQueue(); // 负责每隔1秒装入一次数据 Thread t1 = new Thread() { @Override public void run() { for (int i = 0; i < 10; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } pack.addPack(Integer.toString(i)); } } }; // 每隔0.6秒钟取出数据 Thread t2 = new Thread() { @Override public void run() { for (int i = 0; i < 20; i++) { try { Thread.sleep(600); } catch (InterruptedException e) { e.printStackTrace(); } String s = pack.getPack(); System.out.println(s); } } }; t1.start(); t2.start(); } } class PackQueue { private Queue<String> q = new LinkedList<>(); public void addPack(String s) { this.q.add(s); } public String getPack() { if (this.q.isEmpty()) { return "empty"; } return this.q.remove(); } } //empty //0 //empty //1 //2 //empty //3 //empty //4 //5 //empty //6 //empty //7 //8 //empty //9 //empty //empty //empty
以上逻辑是使用循环实现的。为了不错过传送带上的商品,装配线程t2需不断定时检查。这对设置检查的频率提出了要求。频率稍慢会错过产品,频率过快会浪费性能。最好的方式是,线程1生产好产品后,通知线程2装配,这样解决了“来不及”和“速度过快”的问题。
public class ThreadDispatch { public static void main(String[] args) { var pack = new PackQueue(); // 负责每隔1秒装入一次数据 Thread t1 = new Thread() { @Override public void run() { for (int i = 0; i < 10; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } pack.addPack(Integer.toString(i)); } } }; // 检查有无数据,没有就等待 Thread t2 = new Thread() { @Override public void run() { try { while(true) { String s = pack.getPack(); System.out.println(s); } } catch (InterruptedException e) { e.printStackTrace(); } } }; t1.start(); t2.start(); } } class PackQueue { private Queue<String> q = new LinkedList<>(); public synchronized void addPack(String s) { this.q.add(s); this.notify(); } public synchronized String getPack() throws InterruptedException { if (this.q.isEmpty()) { // return "empty"; this.wait(); } return this.q.remove(); } } // 0 // 1 // 2 // 3 // 4 // 5 // 6 // 7 // 8 // 9
优化之后,装配线程t2 不再“无节制”检查工作区,而是等待t1线程通知后工作。需要注意的是,这里只有一个装配线程t2,如果有多个线程,需要调用this.notifyAll取代this.notify,表示唤醒所有正在等待this锁的线程。唤醒多个线程,最终也只会有一个线程获取this锁,其余线程继续等待。另外,t2 线程在 t1 线程停止生产后永远也醒不过来了。考虑为 t2 线程指定一个wait超时时间,超时后会自动醒来。
java5中引入了高级的处理并发的java.util.concurrent包,相比较synchronized机制,提供了尝试获取锁、超时等待等更多功能;不同于synchronized 在Java语言层面实现自动释放锁而不必考虑异常,ReentrantLock由Java代码实现,因此需要正确捕获异常和释放锁。使用 ReentrantLock 重写示例:
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class PackQueue { private Queue<String> q = new LinkedList<>(); private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); public void addPack(String s) { lock.lock(); try { this.q.add(s); condition.signalAll(); } finally { lock.unlock(); } } public String getPack() throws InterruptedException { lock.lock(); try { if (this.q.isEmpty()) { condition.await(2, TimeUnit.SECONDS); } return this.q.remove(); } finally { lock.unlock(); } } }
值得注意的是,判断队列为空后,调用 condition.await(2, TimeUnit.SECONDS); 表示线程会自动超时醒来。由于此时队列为空,调用remove方法会报错;不过线程可以自动唤醒了。总结一下:
synchronized |
ReentrantLock |
|
加锁 | 通常使用 synchronized 修饰方法,表示使用this实例加锁 |
private final Lock lock = new ReentrantLock(); lock.lock |
释放锁 | 自动释放 |
lock.unlock(); |
线程等待 |
this.wait(); |
private final Condition condition = lock.newCondition(); condition.await(); |
线程唤醒 |
this.notify(); this.notifyAll(); |
condition. signal(); condition.signalAll(); |
锁类型 | 可重入锁 | 可重入锁 |
尝试获取锁 | 不支持 |
lock.tryLock(1, TimeUnit.SECONDS) |
超时自动唤醒 |
不支持 |
condition.await(2, TimeUnit.SECONDS); |