同步方法和同步块

在之前例子的基础上,我们增加新的功能:根据正确与不正确的响应来显示玩家的分数。

public class ScoreLabel extends JLabel implements CharacterListener {
    private volatile int score = 0;
    private int char2type = -1;
    private CharacterSource generator = null, typist = null;

    public ScoreLabel(CharacterSource generator, CharacterSource typist) {
        this.generator = generator;
        this.typist = typist;
        if (generator != null) {
            generator.addCharacterListener(this);
        }
        if (typist != null) {
            typist.addCharacterListener(this);
        }
    }

    public ScoreLabel() {
        this(null, null);
    }

    public synchronized void resetGenerator(CharacterSource newCharactor) {
        if (generator != null) {
            generator.removeCharacterListener(this);
        }
        generator = newCharactor;
        if (generator != null) {
            generator.addCharacterListener(this);
        }
    }

    public synchronized void resetTypist(CharacterSource newTypist) {
        if (typist != null) {
            typist.removeCharacterListener(this);
            typist = newTypist;
        }
        if (typist != null) {
            typist.addCharacterListener(this);
        }
    }

    public synchronized void resetScore() {
        score = 0;
        char2type = -1;
        setScore();
    }

    private synchronized void setScore() {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                setText(Integer.toString(score));
            }
        });
    }

    @Override
    public synchronized void newCharacter(CharacterEvent ce) {
        if (ce.source == generator) {
            if (char2type != -1) {
                score--;
                setScore();
            }
            char2type = ce.character;
        } else {
            if (char2type != ce.character) {
                score--;
            } else {
                score++;
                char2type = -1;
            }
            setScore();
        }
    }
}

这里我们将newCharacter()方法用synchronized进行同步,是因为这个方法会被多个线程调用,而我们根本就不知道哪个线程会在什么时候调用这个方法。这就是race condition。
     变量的volatile无法解决上面的多线程调度问题,因为这里的问题是方法调度的问题,而且更加可怕的是,需要共享的变量不少,其中有些变量是作为条件判断,这就会导致在这些条件变量没有正确的设置前,有些线程已经开始启动了。

这并不是简单的将这些变量设置为volatile就能解决的问题,因为就算这些变量的状态不对,其他线程依然能够启动。

这里有几个方法的同步是需要引起我们注意的:resetScore(),resetGenerator()和resetTypist()这几个方法是在重新启动时才会被调用,似乎我们不需要为此同步它们:其他线程这时根本就没有开始启动!!

但是我们还是需要同步这些方法,这是一种防卫性的设计,保证整个Class所有相关的方法都是线程安全的。遗憾的是,我们必须这样考虑,因为多线程编程的最大问题就是我们永远也不知道我们的程序会出现什么问题,所以,任何可能会引起线程不安全的因素我们都要尽量避免。

这也就引出我们的问题:如何能够对两个不同的方法同步化以防止多个线程在调用这些方法的时候影响对方呢?

对方法做同步化,能够控制方法执行的顺序,因为某个线程上已经运行的方法无法被其他线程调用。这个机制的实现是由指定对象本身的lock来完成的,因为方法需要访问的对象的lock被一个线程占有,但值得注意的是吗,所谓的对象锁其实并不是绑定在对象上,而是对象实例上,如果两个线程拥有对象的两个实例,它们都可以同时访问该对象,

同步的方法如何和没有同步的方法共同执行呢?

所有的同步方法都会执行获取对象锁的步骤,但是没有同步的方法,也就是异步方法并不会这样,所以它们能够在任意的时间点被任意的线程执行,而不管到底是否有同步方法在执行。

关于对象锁的话题自然就会引出一个疑问:静态的同步方法呢?静态的同步方法是无法获取对象锁的,因为它没有this引用,对于它的调用是不存在对象的。但静态的同步方法的确是存在的,那么它又是怎样运作的呢?

这需要另一个锁:类锁。

我们可以从对象实例上获得锁,也能从class(因为class对象的存在)上获得锁,即使这东西实际上是不存在的,因为它无法实现,只是帮助我们理解的概念。值得注意的是,因为一个class只有一个class对象,所以一个class只有一个线程可以执行同步的静态方法,而且与对象的锁毫无相关,类锁可以再对象锁外被独立的获得和释放,一个非静态的同步方法如果调用同步的静态方法,那么它可以同时获得这两个锁。

提供synchronized关键字的目的是为了让对象中的方法能够循序的进入,大部分数据保护的需求都可以由这个关键字实现,但在更加复杂的同步化情况中还是太简单了。

在java这个对象王国里,难道真的是没有Lock这个对象的容身之处吗?答案当然是不可能的,J2SE 5.0开始提供Lock这个接口:

private Lock scoreLock = new ReentrantLock();

public void newCharacter(CharacterEvent ce){
    if(ce.source == generator){
         try{
             scoreLock.lock();
             if(char2type != -1){
                  score--;
                  setScore();
             }
             char2type = ce.character;
         }finally{
             scoreLock.unlock();
         }
     }
     else{
         try{
             scoreLock.lock();
             if(char2type != ce.character){
                  score--;
             }
             else{
                 score++;
                 char2type = -1;
             }
             setScore();
         }finally{
              scoreLock.unlock();
         }
     }

Lock这个接口有两个方法:lock()和unlock(),我们可以在开始的时候调用lock(),然后在结束的时候调用unlock(),这样就能有效的同步化这个方法。
      我们可以看到,其实使用Lock接口只是为了让Lock更加容易被管理:我们可以存储,传递,甚至是抛弃,其余和使用synchronized是一样的,但更加灵活:我们可以在有需要的时候才获取和释放锁,因为lock不再依附于任何调用方法的对象,我们甚至可以让两个对象共享同一个lock!也可以让一个对象占有多个lock!!

使用Lock接口,是一种明确的加锁机制,之前我们的加锁是我们无法掌握的,我们无法知道是哪个线程的哪个方法获得锁,但能确保同一时间只有一个线程的一个方法获得锁,现在我们可以明确得的把握这个过程,灵活的设置lock scope,将一些耗时和具有线程安全性的代码移出lock scope,这样我们就可以写出高效而且线程安全的程序代码,不用像之前一样,为了防止未知错误必须对所有相关方法进行同步。

使用lock接口,可以方便的利用它里面提供的一些便利的方法,像是tryLock(),它可以尝试取得锁,如果无法获取,我们就可以执行其他操作,而不是浪费时间在等待锁的释放。tryLock()还可以指定等待锁的时间。

synchronized不仅可以同步方法,它还可以同步一个程序块:

public void newCharacter(CharacterEvent ce){
     if(ce.source == generator){
           synchronized(this)[
                  if(char2type != -1){
                        score--;
                        setScore();
                  }
                  char2type = ce.character;
            }
      }
      else{
            synchronized(this){
                  if(char2type != ce.character){
                        score--;
                  }
                  else{
                        score--;
                        char2type = -1;
                   }
                   setScore();
            }
      }
}

如果是为了缩小lock的范围,我们依然还是可以使用synchronized而不是使用lock接口,而且这种方式才是更加常见的,因为使用lock接口时我们需要创建新的对象,需要异常管理。我们可以lock住其他对象,如被共享的数据对象。
      选择synchronized整个方法还是代码块,都没有什么问题,但lock scope还是尽可能的越小越好。

考虑到newCharacter()这个方法里面出现了策略选择,我们可以对它进行重构:

private synchronized void newGeneratorCharacter(int c){
      if(char2type != -1){
            score--;
            setScore();
      }
      char2type = c;
}

private synchronized void newTpistCharacter(int c){
      if(char2type != c){
             score--;
      }
      else{
           score++;
           char2type = -1;
      }
      setScore();
}

public synchronized void newCharacter(CharacterEvent ce){
       if(ce.source == generator){
              newGeneratorCharacter(ce.character);
       }
       else{
            newTypistCharacter(ce.character);
       }
}

我们会注意到,两种策略方法都要用synchronized锁住,但真的有必要吗?因为它们是private,只会在该对象中使用,没有理由要让这些方法获取锁,因为它们也只会被对象内的synchronized方法调用,而这时已经获得锁了。但是我们还是要这样做,考虑到以后的开发者可能不知道调用这些方法之前需要获取锁的情况。
     由此可见,java的锁机制远比我们想象中要聪明:它并不是盲目的在进入synchronized程序代码块时就开始获取锁,如果当前的线程已经获得锁,根本就没有必要等到锁被释放还是去获取,只要让synchronized程序段运行就可以。如果没有获取锁,也就不会将它释放掉。这种机制之所以能够运行是因为系统会保持追踪递归取得lock的数目,最后会在第一个取得lock的方法或者代码块退出的时候释放锁。

这就是所谓的nested lock。

之前我们使用的ReentrantLock同样支持nested lock:如果lock的请求是由当前占有lock的线程发出,内部的nested lock就会要求计数递增,调用unlock()就会递减,直到计数为0就会释放该锁。但这个是ReentrantLock才具有的特性,其他实现了Lock这个接口的类并不具有。

nested lock是非常重要的,因为它有利于避免死锁的发生。死锁的发生远比我们想象中要更常见,像是方法间的相互调用,更加常见的情况就是回调,像是Swing编程中依赖事件处理程序与监听者的窗口系统,考虑一下监听者经常变动的情况,同步简直就是一个恶梦!!

Synchronized无法知道lock被递归调用的次数,但是使用ReentrantLock可以做到这点。我们可以通过getHoldCount()方法来获得当前线程对lock所要求的数量,如果数量为0,代表当前线程并未持有锁,但是还不能知道锁是自由的,我们必须通过isLocked()来判断。我们还可以通过isHeldByCurrentThread()来判断lock是否由当前的线程所持有,getQueueLength()可以用来取得有多少个线程在等待取得该锁,但这个只是预估值。

在多线程编程中经常讲到死锁,但是即使没有涉及到同步也有可能会产生死锁。死锁之所以是个问题,是因为它会让程序无法正确的执行,更加可怕的是,死锁是很难被检测的,特别是多线程编程往往都会是一个复杂的程序,它可能永远也不会被发现!!

更加悲哀的是,系统无法解决死锁这种情况!

最后一个问题是关于公平的授予锁。

我们知道,锁是要被授予线程的,但是应该按照什么依据来授予呢?是按照先到先得吗?还是服务请求最多?或者是对系统最有利的形式来授予?java的同步行为最接近第三种,因为同步并不是用来对特殊情况授予锁,它是通用的,所以没有理由让锁按照到达的顺序来授予,应该是由各实现所定义在底层线程系统的行为所决定,但ReentrantLock提供了一种选项可以按照先进先出的顺序获取锁:new ReentrantLock(true),这是为了防止发生锁饥饿的现象。

我们可以根据自己的具体实现来决定这种公平。

最后,我们来总结一下:

1.对于同时涉及到静态和非静态方法的同步情况,使用lock对象更加容易,因为lock对象无关于使用它的对象。

2.将整个方法同步化是最简单的,但是这样范围会变大,让确实没有必要的程序段无效率的持有锁。

3.如果涉及到太多的对象,使用同步块机制也是有问题的,同步块无法解决跨方法的锁范围。

时间: 2024-08-11 05:43:40

同步方法和同步块的相关文章

java synchronized静态同步方法与非静态同步方法,同步语句块

摘自:http://topmanopensource.iteye.com/blog/1738178 进行多线程编程,同步控制是非常重要的,而同步控制就涉及到了锁. 对代码进行同步控制我们可以选择同步方法,也可以选择同步块,这两种方式各有优缺点,至于具体选择什么方式,就见仁见智了,同步块不仅可以更加精确的控制对象锁,也就是控制锁的作用域,何谓锁的作用域?锁的作用域就是从锁被获取到其被释放的时间.而且可以选择要获取哪个对象的对象锁.但是如果在使用同步块机制时,如果使用过多的锁也会容易引起死锁问题,同

为什么wait(),notify()和notifyAll()必须在同步块或同步方法中调

我们常用wait(),notify()和notifyAll()方法来进行线程间通信.线程检查一个条件后就行进入等待状态,例如,在"生产者-消费者"模型中,生产者线程发现缓冲区满了就等待,消费者线程通过消费一个产品使得缓冲区有空闲并通知生产者线程.notify()或notifyAll()的调用给一个或多个线程发出通知,告诉它(它们)条件已经发生改变,并且,一旦通知线程离开同步块,所有等待这个对象锁的线程将竞争这个对象锁,幸运的线程获得锁后就从wait()方法返回并继续执行.让我们把这整个

同步方法、同步代码块、volidate变量的使用

当多个线程涉及到共享数据的时候,就会设计到线程安全的问题.非线程安全其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是"脏读".发生脏读,就是取到的数据已经被其他的线程改过了.什么是线程安全呢?用并发编程实战里面的一段话解释说: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的 这里需要注意的是多个线程,如果一个线程肯定是线程安全的,而

从头认识多线程-2.17 同步方法与同步静态代码块持有的是不同的锁

这一章节我们来讨论一下同步方法与同步静态代码块持有的是不同的锁. 代码清单: package com.ray.deepintothread.ch02.topic_18; /** * * @author RayLee * */ public class SynchClass { public static void main(String[] args) throws InterruptedException { MyService myService = new MyService(); Thr

(转)为什么wait(),notify()和notifyAll()必须在同步块或同步方法中调用

我们常用wait(),notify()和notifyAll()方法来进行线程间通信.线程检查一个条件后就行进入等待状态,例如,在"生产者-消费者"模型中,生产者线程发现缓冲区满了就等待,消费者线程通过消费一个产品使得缓冲区有空闲并通知生产者线程.notify()或notifyAll()的调用给一个或多个线程发出通知,告诉它(它们)条件已经发生改变,并且,一旦通知线程离开同步块,所有等待这个对象锁的线程将竞争这个对象锁,幸运的线程获得锁后就从wait()方法返回并继续执行.让我们把这整个

同步代码块、同步方法以及同步锁的语法

1.同步代码块 在Thread子类run()方法代码块之外套一个下面的代码 synchronized(obj) { ... //此处就是原有的run()方法代码块 } 这里的obj就是需要锁定的对象. 2.同步方法 只要在可变类中修改方法上,加上syschronized修饰即可. 注:同步方法的同步监视器是this. 3.同步锁 先在类中定义锁对象,然后在需要保证线程安全的方法中加锁(锁变量.lock()),最后再在finally块中保证释放锁(锁变量.unlock()) class abc{

java的同步方法和同步代码块,对象锁,类锁区别

/** * @author admin * @date 2018/1/12 9:48 * 作用在同一个实例对象上讨论 * synchronized同步方法的测试 * 两个线程,一个线程调用synchronized修饰方法,另一个线程可以调用非synchronized修饰的方法,互不影响 */ public class SynchronizedTest { public synchronized void methodA() { try { for (int i = 0; i < 5; i++)

synchronized同步块和volatile同步变量

阅读目录 synchronized同步块 volatile同步变量 Java语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量.这两种机制的提出都是为了实现代码线程的安全性.其中 Volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错. 回到顶部 synchronized同步块 Java中的同步块用synchronized标记.同步块在Java中是同步在某个对象上.所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作.所有其他等待进

Java多线程-Java同步块

以下内容转自http://ifeve.com/synchronized-blocks/: Java 同步块(synchronized block)用来标记方法或者代码块是同步的.Java同步块用来避免竞争.本文介绍以下内容: Java同步关键字(synchronzied) 实例方法同步 静态方法同步 实例方法中同步块 静态方法中同步块 Java同步示例 Java 同步关键字(synchronized) Java中的同步块用synchronized标记.同步块在Java中是同步在某个对象上.所有同