`
wenzongliang
  • 浏览: 450291 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

java同步锁

阅读更多

原子动作
    前文讲到,不同线程的操作在访问共享数据时,会因为交织进行而导致线程干扰和内存一致性错误。大多数Java语句在编译成伪代码后都由多条虚拟机指令组成,这使它们有可能被其他线程的语句所分割交织。不能分割交织的操作乘称作原子动作,这些动作一旦发生,便不能在中途停止,要么完全发生,要么根本不发生,直至动作结束。前文所提到的++操作不是一个原子动作。虽然大部分Java语句都不是原子动作,但是也有一些动作可以认定为是原子性的:
1.引用类型变量值的读和写。注意这儿是引用值的读写,而不是所引用对象内容的读和写。
2.除了long和double之外的简单类型的读和写。
3.所有声明为volatile的变量的读和写,包括long和double类型以及引用类型
    原子动作是不能被交织分割的,因此可以放心使用,不用担心线程干扰问题。但注意内存一致性错误对于原子动作仍然是存在的。使用volatile关键字能够减小内存一致性错误发生的风险,任何对volatile变量的写操作和之后进行的读操作都会自动建立“发生过”关系。这意味着任何对于volatile变量的改变都是对其他线程可见的。另外当某线程读一个volatile变量时,它看到的不仅仅是对该变量的最新改动,也能看到这一改变带来的副作用。

    使用原子变量访问要比使用互斥代码访问要高效得多,但是需要程序员人为地避免内存一致性错误发生。是否需要额外措施避免这些错误往往取决于程序的规模和复杂度。java.util.concurrent包中的类提供了不依赖于互斥原语的方法,在后面的文章我们将逐步介绍。

内部锁与互斥
    前面提到除少数原子动作能同时避免线程干扰和内存一致性错误外,其它操作都是需要互斥保护才能避免错误的发生。这些保护技术在Java语言中通过互斥方法和互斥代码实现。
    互斥访问机制是建立在内部锁的实体概念上的。API规范通常称这种实体为“管程(monitor)”。内部锁在这两个问题的解决上扮演着重要的角色,它为线程对对象的状态进行强制排他性访问,并建立对于可视性至关重要的“发生过”关系。
    每个对象都有一个内部锁与其对应。如果一个线程需要排他一致性访问对象的字段,它首先要在访问之前获得该对象的内部锁。当访问完成时需要释放该内部锁。线程在获得该锁和释放该锁期间称作拥有该锁。一旦线程拥有内部锁,其他任何线程都不能再获得该锁,它们在获得该锁时会被阻塞。
    当线程释放该内部锁时,“发生过”关系就在该动作和同把锁的后继动作之间建立起来。
互斥语句
    创建互斥性操作的方法是互斥语句。互斥语句的语法格式如下:
synchronized(lock){
  //critical code for accessing shared data.
  //...
}

    在Java中,实现互斥语句的关键字叫synchronized(同步),我认为这是一个不合适的术语。同步应该定义为按照固定顺序发生的动作序列。这儿的含义显然是互斥访问的含义。
    这儿lock是提供内部锁的对象。这个语句是互斥代码的一般写法。另外往往整个方法需要进行互斥,这时就有所谓互斥方法。互斥方法根据方法类型的不同分为实例互斥方法和静态互斥方法。实例互斥方法的例子如下:
public synchronized void addName(String name){
   //Adding name to a shared list.
}

    互斥实例方法实际获得的是当前实例对象的内部锁,前面的这个实例方法相当于下面写法的互斥语句:
public void addName(String name){
    synchronized(this){
        //Adding name to a shared list.
    }
}

    静态互斥方法的例子如下:
publi class ClassA{
    public static synchronized void addName(String name){
       //Adding to a static shared list.
    }
}

    静态互斥方法实际获得的是当前类Class对象的内部锁,前面这个静态方法的相当于下面写法的互斥语句:
public class ClassA{
    public static void addName(String name){
        synchronized(ClassA.class){
           //Adding to static shared list.
        }
    }
}

    互斥语句在互斥代码开始时获得对象的内部锁,在语句结束或互斥方法返回时释放锁。互斥语句块相对于互斥方法来说主要有两个作用:
1.避免不必要的死锁。有些被互斥代码块中如果包含其他互斥方法或者代码的调用,可能会造成死锁。
2.细化互斥的粒度。比如MsLunch有两个实例字段c1和c2从来不一起使用。所有对这些字段的更新必须互斥进行,但没理由防止c1和c2两个字段更新操作的交织,这样也会因不必要的阻塞减小两种操作之间的并发度。可以专门为每个字段定义一个对象锁,而没必要使用和this关联的互斥实例方法:

    public class MsLunch {
        private long c1 = 0;
        private long c2 = 0;
        private Object lock1 = new Object();
        private Object lock2 = new Object();

        public void inc1() {
            synchronized(lock1) {
                c1++;
            }
        }

        public void inc2() {
            synchronized(lock2) {
                c2++;
            }
        }
    }

互斥重入
    注意线程不能获得已经被另一线程所拥有的锁,但线程可以获取它已经拥有的锁。允许线程多次获取同一把锁使互斥方法可以重入,这样互斥代码就能直接或者间接调用另外有互斥代码的方法,而两处互斥代码可以使用同一把锁。如果没有互斥重入机制,我们需要非常小心的编码才能避免这种调用带来的死锁。
补充
    注意构造函数是不能互斥的。在构造函数前使用synchronized关键字是语法错误。互斥构造函数没有任何意义,因为在其构造时,只有创建该对象的线程可以访问它。在创建要被共享的对象时,一定要注意避免对象的引用提前“泄漏”。比方说想维护一个包含所有实例的静态列表,可能会有这样写代码:
public class A{
  public static ArrayList<A> instances=new ArrayList<A>();
  //...
  public A(){
     ...
     instances.add(this);
     ...
  }
}

    那么当线程通过new A()生成A的实例时,其他线程可以通过访问A.instances而获得该对象,而该对象目前还没有构建完毕,这时就会造成错误。

小结
    互斥方法和互斥语句为java提供了简单的防止线程干扰和内存一致性错误的办法,如果一个对象对多个线程可见,所有对该对象的读和写操作都应该通过互斥代码段或互斥方法来实现互斥性访问。

    当然final字段的访问是不需要互斥的。因为一旦初始化完毕,这些字段只能进行读操作,因此可被不同线程之间安全共享。
    这种互斥方式对于避免两种问题非常有效,但同时也带来了其他各种问题。其中最主要的问题就是对线程活性的影响,这些问题通常有死锁(deadlock)、饥饿(starvation)和活琐(livelock)。

    另外代码互斥如果使用不恰当,如互斥粒度掌握不好,就会造成并发度的降低,从而降低整个应用程序的性能。


并发的活性

 

死锁
    死锁是指多个线程为竞争某些共享资源而陷入无限等待状态。举个现实的例子。假如有条礼貌规则是,当你向朋友鞠躬时,你要一直弯着腰,直到朋友鞠躬还礼为止。这个礼貌规则没有规定同时鞠躬的情况下应该怎么做。A和B都是非常懂礼貌的朋友,那么他们之间在鞠躬时就有可能产生如下情况的死锁:
    public class Deadlock {
        static class Friend {
            private final String name;
            public Friend(String name) {
                this.name = name;
            }
            public String getName() {
                return this.name;
            }
            public synchronized void bow(Friend bower) {
                System.out.format("%s: %s has bowed to me!%n",
                        this.name, bower.getName());
                bower.bowBack(this);
            }
            public synchronized void bowBack(Friend bower) {
                System.out.format("%s: %s has bowed back to me!%n",
                        this.name, bower.getName());
            }
        }

        public static void main(String[] args) {
            final Friend a = new Friend("A");
            final Friend b = new Friend("B");
            new Thread(new Runnable() {
                public void run() { a.bow(b); }
            }).start();
            new Thread(new Runnable() {
                public void run() { b.bow(a); }
            }).start();
        }
    }
    这时两个朋友之间线程就可能产生死锁。两个线程有可能同时处于bow状态,分别等待另外一个人bowBack。两个线程永远不会终止,每个线程都在等待另外一个线程退出bow状态。这是一个典型死锁的例子。
饥饿

    饥饿是指线程长时间无法获得共享资源从而继续相继的处理。这种情况经常发生在当共享资源被“贪婪”线程长时间占据时。假设一个对象提供的互斥方法需要很长时间处理才能返回,然而如果某线程老是频繁激活这个方法,那么其他需要访问该对象的线程就会被长时间阻塞,而处于饥饿状态。
活锁
    一种常见的线程动作是响应另外线程的动作。然而如果另外线程的动作恰好也是该线程的响应,那么活锁现象就可能会产生。正如死锁一样,处于活锁状态的线程通常不能继续后续操作,但它们不是处于阻塞状态,而是简单不断地响应彼此的动作。举个例子,如果两朋友A和B在狭窄的走廊里碰面了,A想靠左以便让B通过,B想靠右以便A通过,结果他们仍然互相堵住对方的路,于是A便向右让以便让B通过,B同时也往左让,以便让A通过,于是就他们就如此让来让去,一直下去。这就是活锁。

 

同步实现 

 

线程除要对共享数据保证互斥性访问外,往往还需保证线程的操作按照特定顺序进行。解决多线程按照特定顺序访问共享数据的技术称作同步。同步技术最常见的编程范式是同步保护块。这种编程范式在操作前先检测某种条件是否成立,如成立则继续操作;如不成立则有两种选择,一种是简单的循环检测,直至此条件条件成立:
public void guardedOperation(){
  while(!condition_expression){
    System.out.println("Not ready yet, I have to wait again!");
  }
}

    这种方法非常消耗CPU资源,任何情况下都不应该使用这种方法。另种更好的方式是条件不成立时调用Object.wait方法挂起当前线程,使它一直等待,直至另一个线程发出激活事件。
当然该事件不一定是当前线程希望等待的事件。
public synchronized guardedOperation() {
    while(!condition_expression) {
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    System.out.println("Now, condition met and it is ready!");
}
    这儿有两点需要特别注意:
1.要在循环检测中等待条件满足,这是因为中断事件并不一定是当前线程所期望的事件。线程等待被中断后应该继续检测条件,以便决定是否进入下一轮等待。
2.当前线程在对wait方法调用时,必须是已经获得wait方法所属对象的内部锁。也就是说,wait方法必须在互斥块或者互斥方法体内调用,否则就会发生NotOwnerException错误
。这种限制和前面所说的同步前提是互斥的说法是一致的。
    上面代码更通用的写法是:
...
synchronized(lock){
   while(!condition_expression){
      try{
         lock.wait();
      }catch(InterruptedException ie){}
   }  
   System.out.println("Now, condition met and it is ready!");  
}
...
    线程在synchronized语句获取对象的内部锁之后,在synchronized代码块期间就拥有了内部锁。当判断条件不成立时,可以调用该对象的wait方法进入等待状态。
    注意持有锁的线程在调用wait方法进入等待状态之后,会自动释放持有的锁。这样做的目的是允许其他的线程进入临界区继续操作,以防止死锁的发生。

    举生产者和消费者的例子。如果消费者在检查时发现没有产品生成,则调用wait方法等待生产者生产。如果此时消费者不释放该锁,生产者就会因为获取不到该锁而处于阻塞状态。而此时消费者却在等待生产者生产出产品来,这样双方就进入死锁状态。因此wait方法需要在挂起线程后释放该线程所拥有的锁。
    当wait方法调用后,线程进入等待状态,直至未来某刻其他线程获得该锁并调用其invokeAll(或invoke)方法将其唤醒。该线程通过如下类似的代码激活等待在此锁上
的线程:
public synchronized notifyOperation(){
   condition_expression=true;
   notifyAll();
}

    假设线程C因检测到某种条件不满足而进入等待状态,激活C线程的P线程往往需要和C线程建立“发生过”关系。也就是说程序期望线程P和C之间按照先P后C的顺序执行。
对于生产者和消费者例子来说,P就是生产者,C就是消费者,它们之间存在从P到C的“发生过”关系。
    线程P在调用notify或者notifyAll方法时需要首先获得该对象的锁,因此这些代码也需要放在synchronized代码体内。上面的激活方法更通用的写法是:
  ...
  synchronized(lock){
     condition_expression=true;
     lock.notifyAll();
  }
  ...

    现举生产者和消费者之间同步的例子。为了简化,假设生产者和消费者之间只共享一个容器。生产者生产出对象后放在在该容器中,而消费者从该
容器中获取该对象进行消费。消费者和生者之间往往需要建立双向的“发生过”关系,即消费者只有在有东西才能消费,而生产者只有在有存放空间时才能生产。这儿为了简化,只假定保证消费者有东西可消费,生产者不管是否有空间可存放,只是将对象生产出来放在容器中。下面是这个例子的代码:
public class TankContainer{
   private Tank tank;
   public synchronized void putTank(Tank tank){
      //Dont bother to check whether it has room.
      this.tank=tank;
      notifyAll();
   }
   public synchronized Tank getTank(){
      //Check whether there's tank to consume
      while(tank==null){
         //No tank yet, let's wait.
         try{
             wait();
         }catch(InterruptedException e){}
      }
      Tank retValue=tank.
      tank=null; //Clear tank.
      return retValue;
   }
}
public ProducerThread extends Thread{
  //Shared TankContainer
  private TankContainer container;
  public ProducerThread(TankContainer container){
    this.container=container;
  }
  ...
  public void run(){
    while(true){
       Tank tank=produceTank();
       container.putTank(tank);   
    }
  }
  ...
}
public ConsumerThread extends Thread{
  //Shared TankContainer
  private TankContainer container;
  public ConsumerThread(TankContainer container){ 
    this.container=container;
  }
  ...
  public void run(){
    while(true){
      Tank tank=container.getTank();
      consumeTank(tank);     
    }
  }
  ...
}

public class ProducerConsumer{
  public static void main(String[]args){
    TankContainer container=new TankContainer();//Shared TankContainer
    new ProducerThread(container).start(); //Start to produce goods in its own thread.
    new ConsumerThread(container).start(); //Start to consume goods in its own thread.
  }
}

     总结一下,同步编程时应该要记住下面几条:
1.两个线程应该获取同一个对象的锁。这是获取同步的互斥性前提。
2.消费者线程应在循环体内检测条件是否成立。
3.消费者线程在条件没有满足时应调用锁对象的wait方法等待。
4.wait方法被中断后应进入下一轮条件检测循环。
5.生产者线程应该在其操作或结束返回之前调用锁对象的notify或notifyAll方法激活等待线程。
   补充一下notify和notifyAll方法的区别。notify激活等待队列上的下一个线程。而notifyAll则激活所有等待线程。在生产者释放锁之后,这些被激活线程竞争获取该
锁。获得该锁的线程只有一个,它从wait中返回,进入下一轮条件检测。没有获得锁的线程继续进入等待状态,等待下一次激活事件。

 


    Java中除了通过互斥和同步技术来获得代码线程安全共性以外,还通过所谓恒量对象(immutable objects)的模式获取线程安全性。其基本原理是恒量对象在创建完毕后就只能读取,就

像final对象一样。后面的文章将对immuable对象技术进行详细描述。

 

 sleep() 和 wait() 的区别

 

      sleep() 是 Thread 类的方法,wait() 是 Object 类的方法,由于所有类都是 Object 的子类,因此所有类都有 wait() 方法,从源代码看 public final native void wait() ,wait() 方法是 final ,不允许重载或是覆盖的,并且是 native ,是由本机接口实现的,与 JVM 的相关。

 

      sleep() 就是让线程空转,但是仍然占用资源。wait() 用在 synchronized 修饰的方法里,让线程暂停,并释放资源,直到有别的线程调用 notify() 或者 notifyAll() 方法唤醒。

 

      对某一个对象,wait() 让线程进入等待池,notify() 唤醒一个线程,notifyAll() 唤醒所有的线程。

我的话费充值店-各种面额

电信100元仅售98.60 
联通100仅售99.00
移动100仅售99.30

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics