java中的synchronized理解

原文网址:http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html


synchronized是java语言的关键字(跟static一样属于一种修饰符),貌似有的地方翻译成同步锁(阻止同步执行的),这个名称挺形象的,从他的名字中就能猜出他大致的用法:在同一时刻对于每一个类实例,其所有被声明为synchronized的成员函数或成员变量至多只能有一个处于可执行的状态,从而避免了类成员变量的访问冲突;除了类实例,对于类,我们也能将类成员函数或变量(static修饰)声明为synchronized,以控制对其静态成员的访问。

    使用synchronized应该注意下面几点:

    一、当两个并发线程(同时执行的)访问同一个对象Object中的synchronized(this)同步代码块的时候,同一时间之内只能有一个线程得到执行,其他的线程必须等待当前线程执行完synchronized修饰的代码之后才能开始执行该代码块。

    二、关于第一点需要补充的是,当一个线程在访问synchronized修饰的代码的时候,其他的线程可以访问Object中没有被synchronized修饰的代码块。

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

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


    synchronized也有自己的缺陷:如果将一个大的方法或者代码块声明为synchronized将会影响程序的执行效率,例如,若将线程中的run方法声明为synchronized,由于线程的整个生命周期都在运行这个方法,因此会导致它对本类中的任何synchronized修饰的内容的访问都不会成功。


synchronized关键字的用法,有两种,synchronized方法和synchronized代码块。

1、synchronized方法:在声明方法的时候加入synchronized关键字,如:

public synchronized void fun();

synchronized方法控制对类成员的访问,每个类实例对应着一把锁,每个synchronized方法都必须获得调用该方法的类实例的锁才能执行,否则所属的线程就会被阻塞,方法一旦执行,就会独占该锁直到该方法执行完毕才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

2、synchronized代码块:

synchronized(syncObject){

//允许访问的控制的代码块

}

synchronized代码块其中的代码必须获得对象syncObject(可以是类或者对象,不一定是改代码块所在的类或对象)的锁方能执行。由于可以针对任意代码块,且可以任意指定上锁的对象,故灵活性较高。


下面对上面的四点分别举例说明

代码1:说明同步锁中的内容在同一时刻只能被一个线程访问

public class Test1 implements Runnable {

	@Override
	public void run() {
		// TODO Auto-generated method stub
		synchronized (this) {
			for (int i = 0; i < 3; i++) {
				System.out.println(Thread.currentThread().getName()
						+ " synchronized loop " + i);
			}
		}
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Test1 t1 = new Test1();
		Thread ta = new Thread(t1, "A");
		Thread tb = new Thread(t1, "B");
		//开启两个名称分别为A和B的线程
		ta.start();
		tb.start();
	}
}
运行结果:

A synchronized loop 0
A synchronized loop 1
A synchronized loop 2
B synchronized loop 0
B synchronized loop 1
B synchronized loop 2

线程A和B几乎是同时开启的(但是线程A肯定还是领先线程B一点的),线程A首先获得了同步锁,于是线程B就只能一直等着线程A访问完同步锁中的内容自己才能访问。


代码2:说明档同步锁中的内容正在被其他的线程访问的时候,对象中的非同步锁的中内容能被其他的线程访问

public class Test3 {

	public void m4t1() {
		synchronized (this) {//同步代码块
			int i = 5;
			while (i-- > 0) {
				System.out
						.println(Thread.currentThread().getName() + " : " + i);
				try {
					Thread.sleep(500);
				} catch (InterruptedException ie) {
					ie.printStackTrace();
				}
			}
		}
	}

	public void m4t2() {
		int i = 5;
		while (i-- > 0) {
			System.out.println(Thread.currentThread().getName() + " : " + i);
			try {
				Thread.sleep(500);
			} catch (InterruptedException ie) {
			}
		}
	}

	public static void main(String[] args) {
		final Test3 myt2 = new Test3();
		Thread t1 = new Thread(new Runnable() {
			public void run() {
				myt2.m4t1();
			}
		}, "t1");
		Thread t2 = new Thread(new Runnable() {
			public void run() {
				myt2.m4t2();
			}
		}, "t2");
		t1.start();
		t2.start();
	}
}

运行结果:

t1 : 4
t2 : 4
t1 : 3
t2 : 3
t2 : 2
t1 : 2
t1 : 1
t2 : 1
t2 : 0
t1 : 0

t1获得了同步锁,m4t1()方法中的代码被synchronized修饰了,一次只能被一个线程访问,但是m4t2()方法并没有被synchronized修饰,所以能够同时被多个线程访问。


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

/**将代码2中的m4t2方法改为下面的*/
	 public void m4t2() {  
         synchronized(this) {  
              int i = 5;  
              while( i-- > 0) {  
                   System.out.println(Thread.currentThread().getName() + " : " + i);  
                   try {  
                        Thread.sleep(500);  
                   } catch (InterruptedException ie) {  
                   }  
              }  
         }
    }
运行结果:

t1 : 4
t1 : 3
t1 : 2
t1 : 1
t1 : 0
t2 : 4
t2 : 3
t2 : 2
t2 : 1
t2 : 0
m4t1()和m4t2()中的方法体是Object对象中的两个同步锁,一旦有一个线程获得了这个同步锁,其他的线程就不能访问这个Object对象中的所有的同步锁。


代码4:以上规则对其它对象锁同样适用。

public class Test4 {
	class Inner {
		private void m4t1() {
			int i = 5;
			while (i-- > 0) {
				System.out.println(Thread.currentThread().getName()
						+ " : Inner.m4t1()=" + i);
				try {
					Thread.sleep(500);
				} catch (InterruptedException ie) {
				}
			}
		}

		private void m4t2() {
			int i = 5;
			while (i-- > 0) {
				System.out.println(Thread.currentThread().getName()
						+ " : Inner.m4t2()=" + i);
				try {
					Thread.sleep(500);
				} catch (InterruptedException ie) {
				}
			}
		}
	}

	private void m4t1(Inner inner) {
		synchronized (inner) { // 使用对象锁
			inner.m4t1();
		}
	}

	private void m4t2(Inner inner) {
		inner.m4t2();
	}

	public static void main(String[] args) {
		final Test4 myt3 = new Test4();
		final Inner inner = myt3.new Inner();
		Thread t1 = new Thread(new Runnable() {
			public void run() {
				myt3.m4t1(inner);
			}
		}, "t1");
		Thread t2 = new Thread(new Runnable() {
			public void run() {
				myt3.m4t2(inner);
			}
		}, "t2");
		t1.start();
		t2.start();
	}
}
运行结果:

t1 : Inner.m4t1()=4
t2 : Inner.m4t2()=4
t1 : Inner.m4t1()=3
t2 : Inner.m4t2()=3
t1 : Inner.m4t1()=2
t2 : Inner.m4t2()=2
t1 : Inner.m4t1()=1
t2 : Inner.m4t2()=1
t1 : Inner.m4t1()=0
t2 : Inner.m4t2()=0

t1获得了Inner的对象锁,但是t2访问的是Inner对象的中的非同步部分,所以两个线程互不干扰(代码中让线程休眠0.5s是为了掩盖代码执行的时间差,可以让结果更明显)
如果把Inner中的m4t2改为:

private synchronized void m4t2() {
	int i = 5;
	while (i-- > 0) {
		System.out.println(Thread.currentThread().getName()
				+ " : Inner.m4t2()=" + i);
		try {
			Thread.sleep(500);
		} catch (InterruptedException ie) {
		}
	}
}
运行结果:

t1 : Inner.m4t1()=4
t1 : Inner.m4t1()=3
t1 : Inner.m4t1()=2
t1 : Inner.m4t1()=1
t1 : Inner.m4t1()=0
t2 : Inner.m4t2()=4
t2 : Inner.m4t2()=3
t2 : Inner.m4t2()=2
t2 : Inner.m4t2()=1
t2 : Inner.m4t2()=0

t1首先获得了Inner对象所,所以t2对Inner对象的中同步代码的访问就会被阻塞,直到t1执行完毕。


补充一下:

打个比方:一个object就像一个大房子,大门永远打开。房子里有 很多房间(也就是方法)。

这些房间有上锁的(synchronized方法), 和不上锁之分(普通方法)。房门口放着一把钥匙(key),这把钥匙可以打开所有上锁的房间。另外我把所有想调用该对象方法的线程比喻成想进入这房子某个 房间的人。所有的东西就这么多了,下面我们看看这些东西之间如何作用的。在此我们先来明确一下我们的前提条件。该对象至少有一个synchronized方法,否则这个key还有啥意义。当然也就不会有我们的这个主题了。一个人想进入某间上了锁的房间,他来到房子门口,看见钥匙在那儿(说明暂时还没有其他人要使用上锁的 房间)。于是他走上去拿到了钥匙

,并且按照自己 的计划使用那些房间。注意一点,他每次使用完一次上锁的房间后会马上把钥匙还回去。即使他要连续使用两间上锁的房间,中间他也要把钥匙还回去,再取回来。

因此,普通情况下钥匙的使用原则是:“随用随借,用完即还。”这时其他人可以不受限制的使用那些不上锁的房间,一个人用一间可以,两个人用一间也可以,没限制。但是如果当某个人想要进入上锁的房间,他就要跑到大门口去看看了。有钥匙当然拿了就走,没有的话,就只能等了。

要是很多人在等这把钥匙,等钥匙还回来以后,谁会优先得到钥匙?Not guaranteed。象前面例子里那个想连续使用两个上锁房间的家伙,他中间还钥匙的时候如果还有其他人在等钥匙,那么没有任何保证这家伙能再次拿到。 (JAVA规范在很多地方都明确说明不保证,象Thread.sleep()休息后多久会返回运行,相同优先权的线程那个首先被执行,当要访问对象的锁被 释放后处于等待池的多个线程哪个会优先得到,等等。我想最终的决定权是在JVM,之所以不保证,就是因为JVM在做出上述决定的时候,绝不是简简单单根据 一个条件来做出判断,而是根据很多条。而由于判断条件太多,如果说出来可能会影响JAVA的推广,也可能是因为知识产权保护的原因吧。SUN给了个不保证 就混过去了。无可厚非。但我相信这些不确定,并非完全不确定。因为计算机这东西本身就是按指令运行的。即使看起来很随机的现象,其实都是有规律可寻。学过 计算机的都知道,计算机里随机数的学名是伪随机数,是人运用一定的方法写出来的,看上去随机罢了。另外,或许是因为要想弄的确定太费事,也没多大意义,所 以不确定就不确定了吧。)

再来看看同步代码块。和同步方法有小小的不同。

1.从尺寸上讲,同步代码块比同步方法小。你可以把同步代码块看成是没上锁房间里的一块用带锁的屏风隔开的空间。

2.同步代码块还可以人为的指定获得某个其它对象的key。就像是指定用哪一把钥匙才能开这个屏风的锁,你可以用本房的钥匙;你也可以指定用另一个房子的钥匙才能开,这样的话,你要跑到另一栋房子那儿把那个钥匙拿来,并用那个房子的钥匙来打开这个房子的带锁的屏风。

    记住你获得的那另一栋房子的钥匙,并不影响其他人进入那栋房子没有锁的房间。

    为什么要使用同步代码块呢?我想应该是这样的:首先对程序来讲同步的部分很影响运行效率,而一个方法通常是先创建一些局部变量,再对这些变量做一些 操作,如运算,显示等等;而同步所覆盖的代码越多,对效率的影响就越严重。因此我们通常尽量缩小其影响范围。如何做?同步代码块。我们只把一个方法中该同 步的地方同步,比如运算。

    另外,同步代码块可以指定钥匙这一特点有个额外的好处,是可以在一定时期内霸占某个对象的key。还记得前面说过普通情况下钥

匙的使用原则吗。现在不是普通情况了。你所取得的那把钥匙不是永远不还,而是在退出同步代码块时才还。

    还用前面那个想连续用两个上锁房间的家伙打比方。怎样才能在用完一间以后,继续使用另一间呢。用同步代码块吧。先创建另外一个线程,做一个同步代码 块,把那个代码块的锁指向这个房子的钥匙。然后启动那个线程。只要你能在进入那个代码块时抓到这房子的钥匙,你就可以一直保留到退出那个代码块。也就是说 你甚至可以对本房内所有上锁的房间遍历,甚至再sleep(10*60*1000),而房门口却还有1000个线程在等这把钥匙呢。很过瘾吧。

    在此对sleep()方法和钥匙的关联性讲一下。一个线程在拿到key后,且没有完成同步的内容时,如果被强制sleep()了,那key还一直在 它那儿。直到它再次运行,做完所有同步内容,才会归还key。记住,那家伙只是干活干累了,去休息一下,他并没干完他要干的事。为了避免别人进入那个房间 把里面搞的一团糟,即使在睡觉的时候他也要把那唯一的钥匙戴在身上。

    最后,也许有人会问,为什么要一把钥匙通开,而不是一个钥匙一个门呢?我想这纯粹是因为复杂性问题。一个钥匙一个门当然更安全,但是会牵扯好多问题。钥匙 的产生,保管,获得,归还等等。其复杂性有可能随同步方法的增加呈几何级数增加,严重影响效率。这也算是一个权衡的问题吧。为了增加点点安全性,导致效 率大大降低,是多么不可取啊。

总的说来,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()――这样,调用端得到的就是对象副本的引用了


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值