zoukankan      html  css  js  c++  java
  • 线程同步(使用了synchronized)和线程通讯(使用了wait,notify)

    线程同步


    什么是线程同步?

    当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题。

    实现同步机制有两个方法:
    1。同步代码块:
    synchronized(同一个数据){} 同一个数据:就是N条线程同时访问一个数据。

    2。

    同步方法:
    public synchronized 数据返回类型 方法名(){}
    就是使用 synchronized 来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无需显示指定同步监视器,同步方法的同步监视器是 this 也就是该对象的本身(这里指的对象本身有点含糊,其实就是调用该同步方法的对象)通过使用同步方法,可非常方便的将某类变成线程安全的类,具有如下特征:
    1,该类的对象可以被多个线程安全的访问。
    2,每个线程调用该对象的任意方法之后,都将得到正确的结果。
    3,每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
    注:synchronized关键字可以修饰方法,也可以修饰代码块,但不能修饰构造器,属性等。

    实现同步机制注意以下几点:   安全性高,性能低,在多线程用。性能高,安全性低,在单线程用。
    1,不要对线程安全类的所有方法都进行同步,只对那些会改变共享资源方法的进行同步。
    2,如果可变类有两种运行环境,当线程环境和多线程环境则应该为该可变类提供两种版本:线程安全版本和线程不安全版本(没有同步方法和同步块)。在单线程中环境中,使用线程不安全版本以保证性能,在多线程中使用线程安全版本.

    线程通讯:

    为什么要使用线程通讯?

    当使用synchronized 来修饰某个共享资源时(分同步代码块和同步方法两种情况),当某个线程获得共享资源的锁后就可以执行相应的代码段,直到该线程运行完该代码段后才释放对该共享资源的锁,让其他线程有机会执行对该共享资源的修改。当某个线程占有某个共享资源的锁时,如果另外一个线程也想获得这把锁运行就需要使用wait() 和notify()/notifyAll()方法来进行线程通讯了。
    Java.lang.object 里的三个方法wait() notify()  notifyAll()
    wait方法导致当前线程等待,直到其他线程调用同步监视器的notify方法或notifyAll方法来唤醒该线程。
    wait(mills)方法
    都是等待指定时间后自动苏醒,调用wait方法的当前线程会释放该同步监视器的锁定,可以不用notify或notifyAll方法把它唤醒。

    notify()
    唤醒在同步监视器上等待的单个线程,如果所有线程都在同步监视器上等待,则会选择唤醒其中一个线程,选择是任意性的,只有当前线程放弃对该同步监视器的锁定后,也就是使用wait方法后,才可以执行被唤醒的线程。

    notifyAll()方法
    唤醒在同步监视器上等待的所有的线程。只用当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

    -------------------------------------------------另外的总结2--------------------------------------------

    线程的同步
    原子操作:根据Java规范,对于基本类型的赋值或者返回值操作,是原子操作。但这里的基本数据类型不包括long和double, 因为JVM看到的基本存储单位是32位,而long 和double都要用64位来表示。所以无法在一个时钟周期内完成。

    自增操作(++)不是原子操作,因为它涉及到一次读和一次写。

    原子操作:由一组相关的操作完成,这些操作可能会操纵与其它的线程共享的资源,为了保证得到正确的运算结果,一个线程在执行原子操作其间,应该采取其他的措施使得其他的线程不能操纵共享资源。

    同步代码块:为了保证每个线程能够正常执行原子操作,Java引入了同步机制,具体的做法是在代表原子操作的程序代码前加上synchronized标记,这样的代码被称为同步代码块。

    同步锁:每个JAVA对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。

    当一个线程试图访问带有synchronized(this)标记的代码块时,必须获得 this关键字引用的对象的锁,在以下的两种情况下,本线程有着不同的命运。
    1、 假如这个锁已经被其它的线程占用,JVM就会把这个线程放到本对象的锁池中。本线程进入阻塞状态。锁池中可能有很多的线程,等到其他的线程释放了锁,JVM就会从锁池中随机取出一个线程,使这个线程拥有锁,并且转到就绪状态。
    2、 假如这个锁没有被其他线程占用,本线程会获得这把锁,开始执行同步代码块。 (一般情况下在执行同步代码块时不会释放同步锁,但也有特殊情况会释放对象锁 如在执行同步代码块时,遇到异常而导致线程终止,锁会被释放;在执行代码块时,执行了锁所属对象的wait()方法,这个线程会释放对象锁,进入对象的等待池中)

    线程同步的特征:
    1、 如果一个同步代码块和非同步代码块同时操作共享资源,仍然会造成对共享资源的竞争。因为当一个线程执行一个对象的同步代码块时,其他的线程仍然可以执行对象的非同步代码块。(所谓的线程之间保持同步,是指不同的线程在执行同一个对象的同步代码块时,因为要获得对象的同步锁而互相牵制)
    2、 每个对象都有唯一的同步锁
    3、 在静态方法前面可以使用synchronized修饰符。
    4、 当一个线程开始执行同步代码块时,并不意味着必须以不间断的方式运行,进入同步代码块的线程可以执行Thread.sleep()或执行Thread.yield()方法,此时它并不释放对象锁,只是把运行的机会让给其他的线程。
    5、 Synchronized声明不会被继承,如果一个用synchronized修饰的方法被子类覆盖,那么子类中这个方法不在保持同步,除非用synchronized修饰。

    线程安全的类:
    1、 这个类的对象可以同时被多个线程安全的访问。
    2、 每个线程都能正常的执行原子操作,得到正确的结果。
    3、 在每个线程的原子操作都完成后,对象处于逻辑上合理的状态。

    释放对象的锁:
    1、 执行完同步代码块就会释放对象的锁
    2、 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放
    3、 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放对象锁,进入对象的等待池。

    死锁
    当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。JVM不监测也不试图避免这种情况,因此保证不发生死锁就成了程序员的责任。

    如何避免死锁
    一个通用的经验法则是:当几个线程都要访问共享资源A、B、C 时,保证每个线程都按照同样的顺序去访问他们。

    线程通信
    Java.lang.Object类中提供了两个用于线程通信的方法
    1、 wait():执行了该方法的线程释放对象的锁,JVM会把该线程放到对象的等待池中。该线程等待其它线程唤醒
    2、 notify():执行该方法的线程唤醒在对象的等待池中等待的一个线程,JVM从对象的等待池中随机选择一个线程,把它转到对象的锁池中。

    ----------------------------------------------------线程同步的总结3---------------------------------------

    我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能包括多个独立运行的线程(Thread)。
    线程(Thread)是一份独立运行的程序,有自己专用的运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。
    当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。
    同步这个词是从英文synchronize(使同时发生)翻译过来的。我也不明白为什么要用这个很容易引起误解的词。既然大家都这么用,咱们也就只好这么将就。
    线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

    因此,关于线程同步,需要牢牢记住的第一点是:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。
    关于线程同步,需要牢牢记住的第二点是 “共享”这两个字。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。
    关于线程同步,需要牢牢记住的第三点是,只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。
    关于线程同步,需要牢牢记住的第四点是:多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。

    为了加深理解,下面举几个例子。
    有两个采购员,他们的工作内容是相同的,都是遵循如下的步骤:
    (1)到市场上去,寻找并购买有潜力的样品。
    (2)回到公司,写报告。
    这两个人的工作内容虽然一样,他们都需要购买样品,他们可能买到同样种类的样品,但是他们绝对不会购买到同一件样品,他们之间没有任何共享资源。所以,他们可以各自进行自己的工作,互不干扰。
    这两个采购员就相当于两个线程;两个采购员遵循相同的工作步骤,相当于这两个线程执行同一段代码。

    下面给这两个采购员增加一个工作步骤。采购员需要根据公司的“布告栏”上面公布的信息,安排自己的工作计划。
    这两个采购员有可能同时走到布告栏的前面,同时观看布告栏上的信息。这一点问题都没有。因为布告栏是只读的,这两个采购员谁都不会去修改布告栏上写的信息。

    下面增加一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。
    如果行政人员先到达布告栏,并且正在修改布告栏的内容。两个采购员这个时候,恰好也到了。这两个采购员就必须等待行政人员完成修改之后,才能观看修改后的信息。
    如果行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员需要等待两个采购员把当前信息记录下来之后,才能够写上新的信息。
    上述这两种情况,行政人员和采购员对布告栏的访问就需要进行同步。因为其中一个线程(行政人员)修改了共享资源(布告栏)。而且我们可以看到,行政人员的工作流程和采购员的工作流程(执行代码)完全不同,但是由于他们访问了同一份可变共享资源(布告栏),所以他们之间需要同步。

    同步锁

    前面讲了为什么要线程同步,下面我们就来看如何才能线程同步。
    线程同步的基本实现思路还是比较容易理解的。我们可以给共享资源加一把锁,这把锁只有一把钥匙。哪个线程获取了这把钥匙,才有权利访问该共享资源。
    生活中,我们也可能会遇到这样的例子。一些超市的外面提供了一些自动储物箱。每个储物箱都有一把锁,一把钥匙。人们可以使用那些带有钥匙的储物箱,把东西放到储物箱里面,把储物箱锁上,然后把钥匙拿走。这样,该储物箱就被锁住了,其他人不能再访问这个储物箱。(当然,真实的储物箱钥匙是可以被人拿走复制的,所以不要把贵重物品放在超市的储物箱里面。于是很多超市都采用了电子密码锁。)
    线程同步锁这个模型看起来很直观。但是,还有一个严峻的问题没有解决,这个同步锁应该加在哪里?
    当然是加在共享资源上了。反应快的读者一定会抢先回答。
    没错,如果可能,我们当然尽量把同步锁加在共享资源上。一些比较完善的共享资源,比如,文件系统,数据库系统等,自身都提供了比较完善的同步锁机制。我们不用另外给这些资源加锁,这些资源自己就有锁。
    但是,大部分情况下,我们在代码中访问的共享资源都是比较简单的共享对象。这些对象里面没有地方让我们加锁。
    读者可能会提出建议:为什么不在每一个对象内部都增加一个新的区域,专门用来加锁呢?这种设计理论上当然也是可行的。问题在于,线程同步的情况并不是很普遍。如果因为这小概率事件,在所有对象内部都开辟一块锁空间,将会带来极大的空间浪费。得不偿失。
    于是,现代的编程语言的设计思路都是把同步锁加在代码段上。确切的说,是把同步锁加在“访问共享资源的代码段”上。这一点一定要记住,同步锁是加在代码段上的。
    同步锁加在代码段上,就很好地解决了上述的空间浪费问题。但是却增加了模型的复杂度,也增加了我们的理解难度。
    现在我们就来仔细分析“同步锁加在代码段上”的线程同步模型。
    首先,我们已经解决了同步锁加在哪里的问题。我们已经确定,同步锁不是加在共享资源上,而是加在访问共享资源的代码段上。
    其次,我们要解决的问题是,我们应该在代码段上加什么样的锁。这个问题是重点中的重点。这是我们尤其要注意的问题:访问同一份共享资源的不同代码段,应该加上同一个同步锁;如果加的是不同的同步锁,那么根本就起不到同步的作用,没有任何意义。
    这就是说,同步锁本身也一定是多个线程之间的共享对象。

    Java语言的synchronized关键字

    为了加深理解,举几个代码段同步的例子。
    不同语言的同步锁模型都是一样的。只是表达方式有些不同。这里我们以当前最流行的Java语言为例。Java语言里面用synchronized关键字给代码段加锁。整个语法形式表现为
    synchronized(同步锁) {
      // 访问共享资源,需要同步的代码段
    }

    这里尤其要注意的就是,同步锁本身一定要是共享的对象。

    … f1() {

    Object lock1 = new Object(); // 产生一个同步锁

    synchronized(lock1){
      // 代码段 A
    // 访问共享资源 resource1
    // 需要同步
    }
    }

    上面这段代码没有任何意义。因为那个同步锁是在函数体内部产生的。每个线程调用这段代码的时候,都会产生一个新的同步锁。那么多个线程之间,使用的是不同的同步锁。根本达不到同步的目的。
    同步代码一定要写成如下的形式,才有意义。

    public static final Object lock1 = new Object();

    … f1() {

    synchronized(lock1){ // lock1 是公用同步锁
      // 代码段 A
    // 访问共享资源 resource1
    // 需要同步
    }

    你不一定要把同步锁声明为static或者public,但是你一定要保证相关的同步代码之间,一定要使用同一个同步锁。
    讲到这里,你一定会好奇,这个同步锁到底是个什么东西。为什么随便声明一个Object对象,就可以作为同步锁?
    在Java里面,同步锁的概念就是这样的。任何一个Object Reference都可以作为同步锁。我们可以把Object Reference理解为对象在内存分配系统中的内存地址。因此,要保证同步代码段之间使用的是同一个同步锁,我们就要保证这些同步代码段的synchronized关键字使用的是同一个Object Reference,同一个内存地址。这也是为什么我在前面的代码中声明lock1的时候,使用了final关键字,这就是为了保证lock1的Object Reference在整个系统运行过程中都保持不变。
    一些求知欲强的读者可能想要继续深入了解synchronzied(同步锁)的实际运行机制。Java虚拟机规范中(你可以在google用“JVM Spec”等关键字进行搜索),有对synchronized关键字的详细解释。synchronized会编译成 monitor enter, … monitor exit之类的指令对。Monitor就是实际上的同步锁。每一个Object Reference在概念上都对应一个monitor。
    这些实现细节问题,并不是理解同步锁模型的关键。我们继续看几个例子,加深对同步锁模型的理解。

    public static final Object lock1 = new Object();

    … f1() {

    synchronized(lock1){ // lock1 是公用同步锁
      // 代码段 A
    // 访问共享资源 resource1
    // 需要同步
    }
    }

    … f2() {

    synchronized(lock1){ // lock1 是公用同步锁
      // 代码段 B
    // 访问共享资源 resource1
    // 需要同步
    }
    }

    上述的代码中,代码段A和代码段B就是同步的。因为它们使用的是同一个同步锁lock1。
    如果有10个线程同时执行代码段A,同时还有20个线程同时执行代码段B,那么这30个线程之间都是要进行同步的。
    这30个线程都要竞争一个同步锁lock1。同一时刻,只有一个线程能够获得lock1的所有权,只有一个线程可以执行代码段A或者代码段B。其他竞争失败的线程只能暂停运行,进入到该同步锁的就绪(Ready)队列。
    每一个同步锁下面都挂了几个线程队列,包括就绪(Ready)队列,待召(Waiting)队列等。比如,lock1对应的就绪队列就可以叫做lock1 - ready queue。每个队列里面都可能有多个暂停运行的线程。
    注意,竞争同步锁失败的线程进入的是该同步锁的就绪(Ready)队列,而不是后面要讲述的待召队列(Waiting Queue,也可以翻译为等待队列)。就绪队列里面的线程总是时刻准备着竞争同步锁,时刻准备着运行。而待召队列里面的线程则只能一直等待,直到等到某个信号的通知之后,才能够转移到就绪队列中,准备运行。
    成功获取同步锁的线程,执行完同步代码段之后,会释放同步锁。该同步锁的就绪队列中的其他线程就继续下一轮同步锁的竞争。成功者就可以继续运行,失败者还是要乖乖地待在就绪队列中。
    因此,线程同步是非常耗费资源的一种操作。我们要尽量控制线程同步的代码段范围。同步的代码段范围越小越好。我们用一个名词“同步粒度”来表示同步代码段的范围。
    同步粒度
    在Java语言里面,我们可以直接把synchronized关键字直接加在函数的定义上。
    比如。
    … synchronized … f1() {
      // f1 代码段
    }

    这段代码就等价于
    … f1() {
      synchronized(this){ // 同步锁就是对象本身
        // f1 代码段
      }
    }

    同样的原则适用于静态(static)函数
    比如。
    … static synchronized … f1() {
      // f1 代码段
    }

    这段代码就等价于
    …static … f1() {
      synchronized(Class.forName(…)){ // 同步锁是类定义本身
        // f1 代码段
      }
    }

    但是,我们要尽量避免这种直接把synchronized加在函数定义上的偷懒做法。因为我们要控制同步粒度。同步的代码段越小越好。synchronized控制的范围越小越好。
    我们不仅要在缩小同步代码段的长度上下功夫,我们同时还要注意细分同步锁。
    比如,下面的代码

    public static final Object lock1 = new Object();

    … f1() {

    synchronized(lock1){ // lock1 是公用同步锁
      // 代码段 A
    // 访问共享资源 resource1
    // 需要同步
    }
    }

    … f2() {

    synchronized(lock1){ // lock1 是公用同步锁
      // 代码段 B
    // 访问共享资源 resource1
    // 需要同步
    }
    }

    … f3() {

    synchronized(lock1){ // lock1 是公用同步锁
      // 代码段 C
    // 访问共享资源 resource2
    // 需要同步
    }
    }

    … f4() {

    synchronized(lock1){ // lock1 是公用同步锁
      // 代码段 D
    // 访问共享资源 resource2
    // 需要同步
    }
    }

    上述的4段同步代码,使用同一个同步锁lock1。所有调用4段代码中任何一段代码的线程,都需要竞争同一个同步锁lock1。
    我们仔细分析一下,发现这是没有必要的。
    因为f1()的代码段A和f2()的代码段B访问的共享资源是resource1,f3()的代码段C和f4()的代码段D访问的共享资源是resource2,它们没有必要都竞争同一个同步锁lock1。我们可以增加一个同步锁lock2。f3()和f4()的代码可以修改为:
    public static final Object lock2 = new Object();

    … f3() {

    synchronized(lock2){ // lock2 是公用同步锁
      // 代码段 C
    // 访问共享资源 resource2
    // 需要同步
    }
    }

    … f4() {

    synchronized(lock2){ // lock2 是公用同步锁
      // 代码段 D
    // 访问共享资源 resource2
    // 需要同步
    }
    }

    这样,f1()和f2()就会竞争lock1,而f3()和f4()就会竞争lock2。这样,分开来分别竞争两个锁,就可以大大较少同步锁竞争的概率,从而减少系统的开销。

    信号量

    同步锁模型只是最简单的同步模型。同一时刻,只有一个线程能够运行同步代码。
    有的时候,我们希望处理更加复杂的同步模型,比如生产者/消费者模型、读写同步模型等。这种情况下,同步锁模型就不够用了。我们需要一个新的模型。这就是我们要讲述的信号量模型。
    信号量模型的工作方式如下:线程在运行的过程中,可以主动停下来,等待某个信号量的通知;这时候,该线程就进入到该信号量的待召(Waiting)队列当中;等到通知之后,再继续运行。
    很多语言里面,同步锁都由专门的对象表示,对象名通常叫Monitor。
    同样,在很多语言中,信号量通常也有专门的对象名来表示,比如,Mutex,Semphore。
    信号量模型要比同步锁模型复杂许多。一些系统中,信号量甚至可以跨进程进行同步。另外一些信号量甚至还有计数功能,能够控制同时运行的线程数。
    我们没有必要考虑那么复杂的模型。所有那些复杂的模型,都是最基本的模型衍生出来的。只要掌握了最基本的信号量模型——“等待/通知”模型,复杂模型也就迎刃而解了。
    我们还是以Java语言为例。Java语言里面的同步锁和信号量概念都非常模糊,没有专门的对象名词来表示同步锁和信号量,只有两个同步锁相关的关键字——volatile和synchronized。
    这种模糊虽然导致概念不清,但同时也避免了Monitor、Mutex、Semphore等名词带来的种种误解。我们不必执着于名词之争,可以专注于理解实际的运行原理。
    在Java语言里面,任何一个Object Reference都可以作为同步锁。同样的道理,任何一个Object Reference也可以作为信号量。
    Object对象的wait()方法就是等待通知,Object对象的notify()方法就是发出通知。
    具体调用方法为
    (1)等待某个信号量的通知
    public static final Object signal = new Object();

    … f1() {
    synchronized(singal) { // 首先我们要获取这个信号量。这个信号量同时也是一个同步锁

        // 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码
        signal.wait(); // 这里要放弃信号量。本线程要进入signal信号量的待召(Waiting)队列

    // 可怜。辛辛苦苦争取到手的信号量,就这么被放弃了

        // 等到通知之后,从待召(Waiting)队列转到就绪(Ready)队列里面
    // 转到了就绪队列中,离CPU核心近了一步,就有机会继续执行下面的代码了。
    // 仍然需要把signal同步锁竞争到手,才能够真正继续执行下面的代码。命苦啊。
        …
    }
    }

    需要注意的是,上述代码中的signal.wait()的意思。signal.wait()很容易导致误解。signal.wait()的意思并不是说,signal开始wait,而是说,运行这段代码的当前线程开始wait这个signal对象,即进入signal对象的待召(Waiting)队列。

    (2)发出某个信号量的通知
    … f2() {
    synchronized(singal) { // 首先,我们同样要获取这个信号量。同时也是一个同步锁。

        // 只有成功获取了signal这个信号量兼同步锁之后,我们才可能进入这段代码
    signal.notify(); // 这里,我们通知signal的待召队列中的某个线程。

    // 如果某个线程等到了这个通知,那个线程就会转到就绪队列中
    // 但是本线程仍然继续拥有signal这个同步锁,本线程仍然继续执行
    // 嘿嘿,虽然本线程好心通知其他线程,
    // 但是,本线程可没有那么高风亮节,放弃到手的同步锁
    // 本线程继续执行下面的代码
        …
    }
    }

    需要注意的是,signal.notify()的意思。signal.notify()并不是通知signal这个对象本身。而是通知正在等待signal信号量的其他线程。

    以上就是Object的wait()和notify()的基本用法。
    实际上,wait()还可以定义等待时间,当线程在某信号量的待召队列中,等到足够长的时间,就会等无可等,无需再等,自己就从待召队列转移到就绪队列中了。
    另外,还有一个notifyAll()方法,表示通知待召队列里面的所有线程。
    这些细节问题,并不对大局产生影响。

    ----------------------------------------------------------------------------------------线程同步的总结4-------------------------------------------------------------------------

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

  • 相关阅读:
    javaScript对象
    javaScript基础
    使用javaScript和JQuery制作经典面试题:光棒效果
    Animate自定义动画
    使用jQuery快速高效制作网页交互特效
    jQuery中绑定事件bind() on() live() one()的异同
    小笔记1(Get请求)

    this关键字
    Eclipse断点调试(下)
  • 原文地址:https://www.cnblogs.com/hudingbo/p/3541087.html
Copyright © 2011-2022 走看看