欢迎访问悦橙教程(wld5.com),关注java教程。悦橙教程  java问答|  每日更新
页面导航 : > > 文章正文

[Java] Java synchronized详解,

来源: javaer 分享于  点击 22868 次 点评:272

[Java] Java synchronized详解,


一、概述

synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A,没有的话,直接运行 它包括两种用法:synchronized 方法和 synchronized 块。

1. synchronized 方法:

  通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:

  

    public synchronized void accessVal(int newVal);

  synchronized 对类成员变量实现访问控制

synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。

synchronized 对类的静态成员变量实现访问控制

  在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。

缺点

  synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。

 

2. synchronized 块:

  通过 synchronized关键字来声明synchronized 块。语法如下:

  

    synchronized(syncObject) {
      //允许访问控制的代码
  }

  synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。

 

二、深入理解

1.对synchronized(this)的一些理解

  一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

  二、当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

  三、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的除synchronized(this)同步代码块以外的部分。

  四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

  五、以上规则对其它对象锁同样适用

 

2.例子 (1)    synchronized的一个简单例子


package rpc.thread;

public class TextThread
{
/**
*@author mango
* @param args
*/
public static void main(String[] args)
{
// TODO 自动生成方法存根
        TxtThread tt = new TxtThread();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
        new Thread(tt).start();
}
}
class TxtThread implements Runnable
{
	int num = 100;
	String str = new String();
	public void run()
	{
		while (true)
		{
			//通过获取str对象的锁,实现对下述代码块的互斥执行
			//synchronized(str)
			//{
				if (num>0)
				{
					try
					{
						Thread.sleep(10);
					}
					catch(Exception e)
					{
						e.getMessage();
					}
					System.out.println(Thread.currentThread().getName()+ "this is "+ num--);
				}
			//}
		}
	}
}

上面的例子中为了制造一个时间差,也就是出错的机会,使用了Thread.sleep(10)。

a) 注销掉synchronized(str)后,多线程并发执行tt实例的run()方法时,对tt.num未能实现互斥访问,而出现线程安全错误,其运行结果如下:

Thread-0this is 10
Thread-1this is 9
Thread-0this is 8
Thread-3this is 7
Thread-2this is 6
Thread-1this is 5
Thread-0this is 4
Thread-3this is 3
Thread-2this is 2
Thread-1this is 1
Thread-0this is 0
Thread-2this is -2
Thread-3this is -1

b)未销掉synchronized(str)时,多线程并发执行tt.run()时,run()中由于通过synchronized(str)实现了对操作num代码块的的互斥执行,其执行结果如下:

Thread-0this is 10
Thread-3this is 9
Thread-2this is 8
Thread-2this is 7
Thread-1this is 6
Thread-1this is 5
Thread-1this is 4
Thread-1this is 3
Thread-2this is 2
Thread-2this is 1

c)将synchronized(str)换成synchronized(this),同样能达到目的

                        synchronized(this)
			{
				if (num>0)
				{
					try
					{
						Thread.sleep(100);
					}
					catch(Exception e)
					{
						e.getMessage();
					}
					System.out.println(Thread.currentThread().getName()+ "this is "+ num--);
				}else{
					return;
				}
			}

例子2 ,通过synchronized实现生产者-消费者同步问题

class Resource {
	
	int currentNum;
	static final int MAX_NUM=10;
	public Resource(int num){
		this.currentNum=num;
	}
	
	 public synchronized void procuder(int num,String name) throws InterruptedException{
		
		if(num<=0 ||num >MAX_NUM){return;}
		 
		while( (num+this.currentNum)>MAX_NUM ){
			
			System.out.println(name+
					"等待,"+"当前面包为:"+this.currentNum+",要求生产:"+num+ ",当前面包充足,不需要生产");
			
			wait();
			System.out.println(name+" 线程被唤醒!");
		}
		//生产
		
		this.currentNum+=num;
		System.out.println(name +" 生产"+num+",总数为:"+this.currentNum);
		
		notifyAll();
		
		System.out.println(name+" 线程执行完毕,唤醒其他线程!");
	 }
	
	 public synchronized void consumer(int num,String name) throws InterruptedException{
		 if(num<=0 || num>MAX_NUM){return;}
		 
		 while( (num>this.currentNum)){
			 System.out.println(name+
						"等待,"+"当前面包为:"+this.currentNum+",要求消费:"+num+ ",当前面包太少,不能满足消费要求");
			 
			 wait();
			 System.out.println(name+" 线程被唤醒!");
			 
		 }
		 //consume
		 
		 this.currentNum-=num;
		 System.out.println(name +" 消费 "+num+",消费后还剩 "+this.currentNum);
		 notifyAll();
		 System.out.println(name+" 线程执行完毕,唤醒其他线程!");
	 }
	
}

class Producer extends Thread{
	private int produceNum;
	private Resource rs;
	public Producer(Resource rs,int produceNum,String name){
		this.rs=rs;
		this.produceNum=produceNum;
		this.setName(name);
	}
	//
	@Override
	public void run(){
		try {
			//
			System.out.println(this.getName()+" 生产者准备生产:"+this.produceNum+"个面包");
			rs.procuder(this.produceNum,this.getName());
			System.out.println(this.getName()+" 生产者已经完成任务");
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
class Consumer extends Thread{
	private int consumerNum;
	private Resource rs;
	public Consumer(Resource rs,int consumerNum,String name){
		this.rs=rs;
		this.consumerNum=consumerNum;
		this.setName(name);
	}
	//
	@Override
	public void run(){
		try {
			//
			System.out.println(this.getName()+" 消费者准备消费"+this.consumerNum+"个面包");
			rs.consumer(this.consumerNum,this.getName());
			System.out.println(this.getName()+" 消费者已经完成任务");
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}


public class ProducerConsumer {

	/**
	 * @param args
	 * @throws InterruptedException 
	 */
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub

		System.out.println("begin");
		
		Resource rs=new Resource(5);
		
		Thread c1=new Consumer(rs,8,"c1");
		Thread c2=new Consumer(rs,3,"c2");
		Thread p1=new Producer(rs,9,"p1");
		Thread p2=new Producer(rs,5,"p2");
		Thread c3=new Consumer(rs,2,"c3");
		
		Thread c4=new Consumer(rs,4,"c4");
		
		c1.start();
		Thread.sleep(10);
		c2.start();
		Thread.sleep(10);
		p1.start();
		Thread.sleep(10);
		p2.start();
	    Thread.sleep(10);
		c3.start();
		Thread.sleep(10);
	    c4.start();
		Thread.sleep(100);
		System.out.println("end");
		
	}

}

其运行结果如下:

begin
c1 消费者准备消费8个面包
c1等待,当前面包为:5,要求消费:8,当前面包太少,不能满足消费要求
c2 消费者准备消费3个面包
c2 消费 3,消费后还剩 2
c2 线程执行完毕,唤醒其他线程!
c2 消费者已经完成任务
c1 线程被唤醒!
c1等待,当前面包为:2,要求消费:8,当前面包太少,不能满足消费要求
p1 生产者准备生产:9个面包
p1等待,当前面包为:2,要求生产:9,当前面包充足,不需要生产
p2 生产者准备生产:5个面包
p2 生产5,总数为:7
p2 线程执行完毕,唤醒其他线程!
p1 线程被唤醒!
p2 生产者已经完成任务
p1等待,当前面包为:7,要求生产:9,当前面包充足,不需要生产
c1 线程被唤醒!
c1等待,当前面包为:7,要求消费:8,当前面包太少,不能满足消费要求
c3 消费者准备消费2个面包
c3 消费 2,消费后还剩 5
c3 线程执行完毕,唤醒其他线程!
c1 线程被唤醒!
c1等待,当前面包为:5,要求消费:8,当前面包太少,不能满足消费要求
c3 消费者已经完成任务
p1 线程被唤醒!
p1等待,当前面包为:5,要求生产:9,当前面包充足,不需要生产
c4 消费者准备消费4个面包
c4 消费 4,消费后还剩 1
c4 线程执行完毕,唤醒其他线程!
p1 线程被唤醒!
p1 生产9,总数为:10
p1 线程执行完毕,唤醒其他线程!
p1 生产者已经完成任务
c1 线程被唤醒!
c1 消费 8,消费后还剩 2
c1 线程执行完毕,唤醒其他线程!
c1 消费者已经完成任务
c4 消费者已经完成任务
end


3.进一步阐述

Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题。到底如何?――还得对synchronized关键字的作用进行深入了解才可定论。

总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。

在进一步阐述之前,我们需要明确几点:

A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。

B.每个对象只有一个锁(lock)与之相关联。

C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

接着来讨论synchronized用到不同地方对代码产生的影响:

 

假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。

 

1. 把synchronized当作函数修饰符时,示例代码如下:

public synchronized void methodAAA()
{
//….
}

这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。

上边的示例代码等同于如下代码:

public void methodAAA()
{
	synchronized (this)      // (1)
	{
      	 //…..
	}
}

(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:(

2.同步块,示例代码如下:

public void method3(SomeObject so)
{
    synchronized(so)
    {
       //…..
    }
}


这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Foo implements Runnable
{
        private byte[] lock = new byte[0]; // 特殊的instance变量
        Public void methodA()
        {
           synchronized(lock) { //… }
        }
        //…..
}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

3.将synchronized作用于static 函数,示例代码如下:

Class Foo
{
    public synchronized static void methodAAA()   // 同步的static 函数
    {
        //….
    }
    public void methodBBB()
    {
       synchronized(Foo.class)   // class literal(类名称字面常量)
    }
}

   代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。

记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。

可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。

 

小结:

搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。

 

还有一些技巧可以让我们对共享资源的同步访问更加安全:

1. 定义private 的instance变量+它的 get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。

2. 如果instance变量是一个对象,如数组或ArrayList什么的,那上述方法仍然不安全,因为当外界对象通过get方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。 这个时候就需要将get方法也加上synchronized同步,并且,只返回这个private对象的clone()――这样,调用端得到的就是对象副本的引用了

还有,比较常用的就有:Collections.synchronizedMap(new HashMap()),当然这个MAP就是生命在类中的全局变量,就是一个 线程安全的HashMap,web的application是全web容器公用的,所以要使用线程安全来保证数据的正确。

 


本文主要摘自:http://javacrazyer.iteye.com/blog/759300


相关文章

    暂无相关文章
相关栏目:

用户点评