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

本文深入探讨Java中synchronized关键字的使用,解释其如何确保多线程环境下的线程安全与性能优化,同时介绍了生产者/消费者模式下的线程同步机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文转载自:JAVA线程安全及性能的优化笔记(二)——Synchronized关键字


前面的文章说了,java用synchronized关键字作为多线程并发环境的执行有序性的保证手段之一。当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标志了临界区。典型的用法如下:

synchronized(锁){
	临界代码
}

为了保证银行账户的安全,可以操作账户的方法如下:

publicsynchronizedvoidadd(intnum){
	balance=balance+num;
}
publicsynchronizedvoidwithdraw(intnum){
	balance=balance-num;
}

刚才不是说了synchronized的用法是这样的吗:

synchronized(锁){
	临界代码
}

那么对于publicsynchronizedvoidadd(intnum)这种情况,意味着什么呢?其实这种情况,锁就是这个方法所在的对象。同理,如果方法是publicsynchronizedvoidadd(intnum),那么锁就是这个方法所在的class。

理论上,每个对象都可以作为锁,但一个对象作为锁时,应该被多个线程共享,这样显得有意义,在开发环境下,一个没有共享的对象作为锁是没有任何意义的。

例如:

publicclassThreadTest{
	publicvoidtest(){
		Objectlock=newObject();
		synchronized(lock){
			//dosomething
		}
	}
}

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来的,所以任意对象都有这个方法。

Objectlock=newObject();
synchronized(lock){
	balance=balance-num;
	//这里放弃了同步锁,好不容易得到,又放弃了
	Lock.wait();
}

如果一个线程获得了锁,进入了同步块,执行lock.wait(),那么这个线程会进入到lock的阻塞队列。如果调用lock.notify()则会通知阻塞队列的某个线程进入就绪队列。

声明一个盘子只能放一个鸡蛋。

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("测试结束");
	}
}

执行结果:

放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
测试结束

声明一个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线程安全及性能的优化笔记(二)——Synchronized关键字

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值