synchronized 同步操作应该是细粒度

本文介绍了Java中多线程并发访问的问题,并通过实例演示如何使用synchronized关键字解决并发问题,包括同步方法、同步代码块及锁的使用。

synchronized
Java良好的支持多线程。使用java,我们可以很轻松的编程一个多线程程序。但是使用多线程可能会引起并发访问的问题。synchronized和ThreadLocal都是用来解决多线程并发访问的问题。大家可能对synchronized较为熟悉,而对ThreadLocal就要陌生得多了。
并发问题。当一个对象被两个线程同时访问时,可能有一个线程会得到不可预期的结果。

一个简单的java类Studnet

Java代码 复制代码
  1. publicclassStudent{
  2. privateintage=0;
  3. publicintgetAge(){
  4. returnthis.age;
  5. }
  6. publicvoidsetAge(intage){
  7. this.age=age;
  8. }
  9. }
public class Student {
  private int age=0;
  
  public int getAge() {
	  return this.age;
	  
  }
  
  public void setAge(int age) {
	  this.age = age;
  }
}


一个多线程类ThreadDemo.
这个类有一个Student的私有变量,在run方法中,它随机产生一个整数。然后设置到student变量中,从student中读取设置后的值。然后睡眠5秒钟,最后再次读student的age值。

Java代码 复制代码
  1. publicclassThreadDemoimplementsRunnable{
  2. Studentstudent=newStudent();
  3. publicstaticvoidmain(String[]agrs){
  4. ThreadDemotd=newThreadDemo();
  5. Threadt1=newThread(td,"a");
  6. Threadt2=newThread(td,"b");
  7. t1.start();
  8. t2.start();
  9. }
  10. /*(non-Javadoc)
  11. *@seejava.lang.Runnable#run()
  12. */
  13. publicvoidrun(){
  14. accessStudent();
  15. }
  16. publicvoidaccessStudent(){
  17. StringcurrentThreadName=Thread.currentThread().getName();
  18. System.out.println(currentThreadName+"isrunning!");
  19. //System.out.println("firstreadageis:"+this.student.getAge());
  20. Randomrandom=newRandom();
  21. intage=random.nextInt(100);
  22. System.out.println("thread"+currentThreadName+"setageto:"+age);
  23. this.student.setAge(age);
  24. System.out.println("thread"+currentThreadName+"firstreadageis:"+this.student.getAge());
  25. try{
  26. Thread.sleep(5000);
  27. }
  28. catch(InterruptedExceptionex){
  29. ex.printStackTrace();
  30. }
  31. System.out.println("thread"+currentThreadName+"secondreadageis:"+this.student.getAge());
  32. }
  33. }
public class ThreadDemo implements Runnable{
  Student student = new Student();
  public static void main(String[] agrs) {
	 ThreadDemo td = new ThreadDemo();
	 Thread t1 = new Thread(td,"a");
	 Thread t2 = new Thread(td,"b");
    t1.start();
    t2.start();

  }
/* (non-Javadoc)
 * @see java.lang.Runnable#run()
 */
 public void run() {
	 accessStudent();
 }
 
 public void accessStudent() {
	    String currentThreadName = Thread.currentThread().getName();
	    System.out.println(currentThreadName+" is running!");
	   // System.out.println("first  read age is:"+this.student.getAge());
	    Random random = new Random();
	    int age = random.nextInt(100);
	    System.out.println("thread "+currentThreadName +" set age to:"+age);
	   
	    this.student.setAge(age);
	    System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());
	    try {
	    Thread.sleep(5000);
	    }
	    catch(InterruptedException ex) {
	    	ex.printStackTrace();
	    }
	    System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
	     
 }
  
}

运行这个程序,屏幕输出如下:
a is running!
b is running!
thread b set age to:33
thread b first read age is:33
thread a set age to:81
thread a first read age is:81
thread b second read age is:81
thread a second read age is:81

需要注意的是,线程a在同一个方法中,第一次读取student的age值与第二次读取值不一致。这就是出现了并发问题。

synchronized
上面的例子,我们模似了一个并发问题。Java提供了同步机制来解决并发问题。synchonzied关键字可以用来同步变量,方法,甚至同步一个代码块。
使用了同步后,一个线程正在访问同步对象时,另外一个线程必须等待。
Synchronized同步方法
现在我们可以对accessStudent方法实施同步。
public synchronized void accessStudent()
再次运行程序,屏幕输出如下:
a is running!
thread a set age to:49
thread a first read age is:49
thread a second read age is:49
b is running!
thread b set age to:17
thread b first read age is:17
thread b second read age is:17

加上了同步后,线程b必须等待线程a执行完毕后,线程b才开始执行。

对方法进行同步的代价是非常昂贵的。特别是当被同步的方法执行一个冗长的操作。这个方法执行会花费很长的时间,对这样的方法进行同步可能会使系统性能成数量级的下降。

Synchronized同步块
在accessStudent方法中,我们真实需要保护的是student变量,所以我们可以进行一个更细粒度的加锁。我们仅仅对student相关的代码块进行同步。

Java代码 复制代码
  1. synchronized(this){
  2. Randomrandom=newRandom();
  3. intage=random.nextInt(100);
  4. System.out.println("thread"+currentThreadName+"setageto:"+age);
  5. this.student.setAge(age);
  6. System.out.println("thread"+currentThreadName+"firstreadageis:"+this.student.getAge());
  7. try{
  8. Thread.sleep(5000);
  9. }
  10. catch(InterruptedExceptionex){
  11. ex.printStackTrace();
  12. }
  13. }
	    synchronized(this) {
	    Random random = new Random();
	    int age = random.nextInt(100);
	    System.out.println("thread "+currentThreadName +" set age to:"+age);
	   
	    this.student.setAge(age);
	   
	    System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());
	    try {
	    Thread.sleep(5000);
	    }
	    catch(InterruptedException ex) {
	    	ex.printStackTrace();
	    }
	    }

运行方法后,屏幕输出:
a is running!
thread a set age to:18
thread a first read age is:18
b is running!
thread a second read age is:18
thread b set age to:62
thread b first read age is:62
thread b second read age is:62

需要特别注意这个输出结果。
这个执行过程比上面的方法同步要快得多了。
只有对student进行访问的代码是同步的,而其它与部份代码却是异步的了。而student的值并没有被错误的修改。如果是在一个真实的系统中,accessStudent方法的操作又比较耗时的情况下。使用同步的速度几乎与没有同步一样快。

使用同步锁
稍微把上面的例子改一下,在ThreadDemo中有一个私有变量count,。
private int count=0;
在accessStudent()中, 线程每访问一次,count都自加一次, 用来记数线程访问的次数。

Java代码 复制代码
  1. try{
  2. this.count++;
  3. Thread.sleep(5000);
  4. }catch(InterruptedExceptionex){
  5. ex.printStackTrace();
  6. }
	    try {
	    this.count++;
	    Thread.sleep(5000);
	    }catch(InterruptedException ex) {
	    	ex.printStackTrace();
	    }

为了模拟线程,所以让它每次自加后都睡眠5秒。
accessStuden()方法的完整代码如下:

Java代码 复制代码
  1. StringcurrentThreadName=Thread.currentThread().getName();
  2. System.out.println(currentThreadName+"isrunning!");
  3. try{
  4. this.count++;
  5. Thread.sleep(5000);
  6. }catch(InterruptedExceptionex){
  7. ex.printStackTrace();
  8. }
  9. System.out.println("thread"+currentThreadName+"readcount:"+this.count);
  10. synchronized(this){
  11. Randomrandom=newRandom();
  12. intage=random.nextInt(100);
  13. System.out.println("thread"+currentThreadName+"setageto:"+age);
  14. this.student.setAge(age);
  15. System.out.println("thread"+currentThreadName+"firstreadageis:"+this.student.getAge());
  16. try{
  17. Thread.sleep(5000);
  18. }
  19. catch(InterruptedExceptionex){
  20. ex.printStackTrace();
  21. }
  22. }
  23. System.out.println("thread"+currentThreadName+"secondreadageis:"+this.student.getAge());
   	    String currentThreadName = Thread.currentThread().getName();
	    System.out.println(currentThreadName+" is running!");
			    try {
	    this.count++;
	    Thread.sleep(5000);
	    }catch(InterruptedException ex) {
	    	ex.printStackTrace();
	    }
		    System.out.println("thread "+currentThreadName+" read count:"+this.count);
	    
	   
	    synchronized(this) {
	    Random random = new Random();
	    int age = random.nextInt(100);
	    System.out.println("thread "+currentThreadName +" set age to:"+age);
	   
	    this.student.setAge(age);
	   
	    System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());
	    try {
	    Thread.sleep(5000);
	    }
	    catch(InterruptedException ex) {
	    	ex.printStackTrace();
	    }
	    }
	    System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());

运行程序后,屏幕输出:
a is running!
b is running!
thread a read count:2
thread a set age to:49
thread a first read age is:49
thread b read count:2
thread a second read age is:49
thread b set age to:7
thread b first read age is:7
thread b second read age is:7

我们仍然对student对象以synchronized(this)操作进行同步。
我们需要在两个线程中共享count失败。

所以仍然需要对count的访问进行同步操作。

Java代码 复制代码
  1. synchronized(this){
  2. try{
  3. this.count++;
  4. Thread.sleep(5000);
  5. }catch(InterruptedExceptionex){
  6. ex.printStackTrace();
  7. }
  8. }
  9. System.out.println("thread"+currentThreadName+"readcount:"+this.count);
  10. synchronized(this){
  11. Randomrandom=newRandom();
  12. intage=random.nextInt(100);
  13. System.out.println("thread"+currentThreadName+"setageto:"+age);
  14. this.student.setAge(age);
  15. System.out.println("thread"+currentThreadName+"firstreadageis:"+this.student.getAge());
  16. try{
  17. Thread.sleep(5000);
  18. }
  19. catch(InterruptedExceptionex){
  20. ex.printStackTrace();
  21. }
  22. }
  23. System.out.println("thread"+currentThreadName+"secondreadageis:"+this.student.getAge());
  24. longendTime=System.currentTimeMillis();
  25. longspendTime=endTime-startTime;
  26. System.out.println("花费时间:"+spendTime+"毫秒");
		 synchronized(this) {
	    try {
	    this.count++;
	    Thread.sleep(5000);
	    }catch(InterruptedException ex) {
	    	ex.printStackTrace();
	    }
	    }
	    System.out.println("thread "+currentThreadName+" read count:"+this.count);
	    
	   
	    synchronized(this) {
	    Random random = new Random();
	    int age = random.nextInt(100);
	    System.out.println("thread "+currentThreadName +" set age to:"+age);
	   
	    this.student.setAge(age);
	   
	    System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());
	    try {
	    Thread.sleep(5000);
	    }
	    catch(InterruptedException ex) {
	    	ex.printStackTrace();
	    }
	    }
	    System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
	    long endTime = System.currentTimeMillis();
	    long spendTime = endTime - startTime;
	    System.out.println("花费时间:"+spendTime +"毫秒");


程序运行后,屏幕输出
a is running!
b is running!
thread a read count:1
thread a set age to:97
thread a first read age is:97
thread a second read age is:97
花费时间:10015毫秒
thread b read count:2
thread b set age to:47
thread b first read age is:47
thread b second read age is:47
花费时间:20124毫秒

我们在同一个方法中,多次使用synchronized(this)进行加锁。有可能会导致太多额外的等待。
应该使用不同的对象锁进行同步。

设置两个锁对象,分别用于student和count的访问加锁。

Java代码 复制代码
  1. privateObjectstudentLock=newObject();
  2. privateObjectcountLock=newObject();
  3. accessStudent()方法如下:
  4. longstartTime=System.currentTimeMillis();
  5. StringcurrentThreadName=Thread.currentThread().getName();
  6. System.out.println(currentThreadName+"isrunning!");
  7. //System.out.println("firstreadageis:"+this.student.getAge());
  8. synchronized(countLock){
  9. try{
  10. this.count++;
  11. Thread.sleep(5000);
  12. }catch(InterruptedExceptionex){
  13. ex.printStackTrace();
  14. }
  15. }
  16. System.out.println("thread"+currentThreadName+"readcount:"+this.count);
  17. synchronized(studentLock){
  18. Randomrandom=newRandom();
  19. intage=random.nextInt(100);
  20. System.out.println("thread"+currentThreadName+"setageto:"+age);
  21. this.student.setAge(age);
  22. System.out.println("thread"+currentThreadName+"firstreadageis:"+this.student.getAge());
  23. try{
  24. Thread.sleep(5000);
  25. }
  26. catch(InterruptedExceptionex){
  27. ex.printStackTrace();
  28. }
  29. }
  30. System.out.println("thread"+currentThreadName+"secondreadageis:"+this.student.getAge());
  31. longendTime=System.currentTimeMillis();
  32. longspendTime=endTime-startTime;
  33. System.out.println("花费时间:"+spendTime+"毫秒");
 private Object studentLock = new Object();
private Object countLock = new Object();

accessStudent()方法如下:
	 long startTime = System.currentTimeMillis();
	    String currentThreadName = Thread.currentThread().getName();
	    System.out.println(currentThreadName+" is running!");
	   // System.out.println("first  read age is:"+this.student.getAge());

		 synchronized(countLock) {
	    try {
	    this.count++;
	    Thread.sleep(5000);
	    }catch(InterruptedException ex) {
	    	ex.printStackTrace();
	    }
	    }
	    System.out.println("thread "+currentThreadName+" read count:"+this.count);
	    
	   
	    synchronized(studentLock) {
	    Random random = new Random();
	    int age = random.nextInt(100);
	    System.out.println("thread "+currentThreadName +" set age to:"+age);
	   
	    this.student.setAge(age);
	   
	    System.out.println("thread "+currentThreadName+" first  read age is:"+this.student.getAge());
	    try {
	    Thread.sleep(5000);
	    }
	    catch(InterruptedException ex) {
	    	ex.printStackTrace();
	    }
	    }
	    System.out.println("thread "+currentThreadName +" second read age is:"+this.student.getAge());
	    long endTime = System.currentTimeMillis();
	    long spendTime = endTime - startTime;
	    System.out.println("花费时间:"+spendTime +"毫秒");


这样对count和student加上了两把不同的锁。

运行程序后,屏幕输出:
a is running!
b is running!
thread a read count:1
thread a set age to:48
thread a first read age is:48
thread a second read age is:48
花费时间:10016毫秒
thread b read count:2
thread b set age to:68
thread b first read age is:68
thread b second read age is:68
花费时间:20046毫秒
与两次使用synchronized(this)相比,使用不同的对象锁,在性能上可以得到更大的提升。

由此可见synchronized是实现java的同步机制。同步机制是为了实现同步多线程对相同资源的并发访问控制。保证多线程之间的通信。
可见,同步的主要目的是保证多线程间的数据共享。同步会带来巨大的性能开销,所以同步操作应该是细粒度的。如果同步使用得当,带来的性能开销是微不足道的。使用同步真正的风险是复杂性和可能破坏资源安全,而不是性能。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值