JAVA线程安全及性能的优化笔记(二)——Synchronized关键字

前期回顾:


前面的文章说了,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的就绪队列,等到得到锁后才可以执行。

一个线程执行临界区代码过程如下:

  1. 获得同步锁
  2. 清空工作内存
  3. 从主存拷贝变量副本到工作内存
  4. 对这些变量计算
  5. 讲变量从工作内存写会到主存
  6. 释放锁

可见,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专门纳鸡蛋,假设:

  1. 开始,A调用plate.putEgg()方法,此时eggs.size()为0,因此顺利将鸡蛋放到盘子,还之行了notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列还没有线程。
  2. 又有一个A线程对象调用plate.putEgg方法,此时eggs.size()不为0,调用wait()方法,自己进入了锁对象的阻塞线程
  3. 此时,来了一个B线程对象,调用plate.getEgg()方法,eggs.size()不为0,顺利的拿到了一个鸡蛋,还执行了notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列有一个A线程对象,唤醒后,它进入到就绪队列,就绪队列也就它一个,因此马上得到锁,开始往盘子里放鸡蛋,此时盘子是空的,因此放鸡蛋成功。
  4. 假设接着来了线程A,就重复2;假设来线程B,就重复3.

未完待续


  • 针对于Java程序员,笔者最近整理了一些面试真题,思维导图,程序人生等PDF学习资料;
  • 关注私信我"86",即可获取!
  • 希望读到这的您能点个小赞和关注下我,以后还会更新技术干货,谢谢您的支持!
  • JAVA线程安全及性能的优化笔记(二)——Synchronized关键字


    分享到:


    相關文章: