前期回顾:
前面的文章说了,java用synchronized关键字作为多线程并发环境的执行有序性的保证手段之一。当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标志了临界区。典型的用法如下:
<code>synchronized(锁){
临界代码
}/<code>
为了保证银行账户的安全,可以操作账户的方法如下:
<code>publicsynchronizedvoidadd(intnum){
balance=balance+num;
}
publicsynchronizedvoidwithdraw(intnum){
balance=balance-num;
}/<code>
刚才不是说了synchronized的用法是这样的吗:
<code>synchronized(锁){
临界代码
}/<code>
那么对于publicsynchronizedvoidadd(intnum)这种情况,意味着什么呢?其实这种情况,锁就是这个方法所在的对象。同理,如果方法是publicsynchronizedvoidadd(intnum),那么锁就是这个方法所在的class。
理论上,每个对象都可以作为锁,但一个对象作为锁时,应该被多个线程共享,这样显得有意义,在开发环境下,一个没有共享的对象作为锁是没有任何意义的。
例如:
<code>publicclassThreadTest{
publicvoidtest(){
Objectlock=newObject();
synchronized(lock){
//dosomething
}
}
}/<code>
lock变量作为一个锁存在根本没有意义,因为它根本不是共享对象,每个线程进来都会执行Objectlock=newObject();每个线程都有自己的lock,根本不存在锁竞争。
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个线程被唤醒(notify)后,才会进入到就绪队列,等待cpu的调度。当一开始线程a第一次执行account.add方法时,jvm会检查锁对象account的就绪队列是否已经有线程在等待,如果有则表明了account的锁已经被占用了,由于是第一次执行,account的就绪队列为空,所以线程a获得了锁,执行account.add方法。如果恰好在这个时候,线程a要执行account.withdraw方法,因为线程a已经获得了锁还没有释放,所以线程b要进入account的就绪队列,等到得到锁后才可以执行。
一个线程执行临界区代码过程如下:
- 获得同步锁
- 清空工作内存
- 从主存拷贝变量副本到工作内存
- 对这些变量计算
- 讲变量从工作内存写会到主存
- 释放锁
可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。
1. 生产者/消费者
生产者/消费者模式其实是一种很经典的线程同步模型,很对时候,并不是光保证多个线程对某共享资源操作的互斥性就可以了,往往多个线程之间都是有协作的。
假设有这样一种情况,有一个桌子,桌子上面有一个盘子,盘子里只能放一颗鸡蛋,A专门往盘子里放鸡蛋,如果盘子里有鸡蛋,则一直等到盘子里没有鸡蛋,B专门从盘子里拿鸡蛋,如果盘子里没有鸡蛋,则等待直到盘子里有鸡蛋。其实盘子就是一个互斥区,每次往盘子里放鸡蛋应该都是互斥的,A的等到其实就是主动放弃锁,B等待时还要提醒A放鸡蛋。
很简单,调用锁的wait()方法就好。Wait方法是从Object来的,所以任意对象都有这个方法。
<code>Objectlock=newObject();
synchronized(lock){
balance=balance-num;
//这里放弃了同步锁,好不容易得到,又放弃了
Lock.wait();
}/<code>
如果一个线程获得了锁,进入了同步块,执行lock.wait(),那么这个线程会进入到lock的阻塞队列。如果调用lock.notify()则会通知阻塞队列的某个线程进入就绪队列。
<code>importjava.util.ArrayList;
importjava.util.List;
publicclassPlate{
List<object>eggs=newArrayList<object>();
publicsynchronizedObjectgetEgg(){
if(eggs.size()==0){
try{
wait();
}
catch(InterruptedExceptione){
}
}
Objectegg=eggs.get(0);
eggs.clear();
//清空盘了
notify();
//唤醒阻塞队列的某线程到就绪队列
System.out.println("拿到鸡蛋");
returnegg;
}
publicsynchronizedvoidputEgg(Objectegg){
if(eggs.size()>0){
try{
wait();
}
catch(InterruptedExceptione){
}
}
eggs.add(egg);
//往盘子里放鸡蛋
notify();
//唤醒阻塞队列的某线程到就绪队列
System.out.println("放入鸡蛋");
}
}
publicclassAddThreadextendsThread{
privatePlateplate;
privateObjectegg=newObject();
publicAddThread(Plateplate){
this.plate=plate;
}
publicvoidrun(){
for (inti=0;i<5;i++){
plate.putEgg(egg);
}
}
}
publicclassGetThreadextendsThread{
privatePlateplate;
publicGetThread(Plateplate){
this.plate=plate;
}
publicvoidrun(){
for (inti=0;i<5;i++){
plate.getEgg();
}
}
publicstaticvoidmain(String[]args){
try{
Plateplate=newPlate();
Threadadd=newThread(newAddThread(plate));
Threadget=newThread(newGetThread(plate));
add.start();
get.start();
add.join();
get.join();
}
catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println("测试结束");
}
}/<object>/<object>/<code>
执行结果:
<code>放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
测试结束/<code>
声明一个Plate对象为plate,被线程A和线程B共享,A专门放鸡蛋,B专门纳鸡蛋,假设:
- 开始,A调用plate.putEgg()方法,此时eggs.size()为0,因此顺利将鸡蛋放到盘子,还之行了notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列还没有线程。
- 又有一个A线程对象调用plate.putEgg方法,此时eggs.size()不为0,调用wait()方法,自己进入了锁对象的阻塞线程
- 此时,来了一个B线程对象,调用plate.getEgg()方法,eggs.size()不为0,顺利的拿到了一个鸡蛋,还执行了notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列有一个A线程对象,唤醒后,它进入到就绪队列,就绪队列也就它一个,因此马上得到锁,开始往盘子里放鸡蛋,此时盘子是空的,因此放鸡蛋成功。
- 假设接着来了线程A,就重复2;假设来线程B,就重复3.
未完待续
閱讀更多 Java架構師丨蘇先生 的文章