`
wenzongliang
  • 浏览: 461367 次
  • 性别: 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

分享到:
评论

相关推荐

    Java 同步锁 wait notify 学习心得

    ### Java同步锁原理 在Java中,`synchronized`关键字用于实现线程同步,即确保同一时刻只有一个线程可以访问特定的代码块或方法。这种机制通过内部维护一个锁来实现,每个对象都有一个内置锁,这个锁可以被任何拥有...

    java-syn.zip_Java syn_Java syn锁_java同步锁syn_java锁 syn_syn同步事务锁

    Java 同步锁是Java多线程编程中的关键概念,用于确保多个线程在访问共享资源时能够有序、安全地进行。在这个主题中,我们将详细探讨Java中的同步机制,包括同步方法、同步代码块、synchronized关键字以及其背后的...

    Java 同步锁(synchronized)详解及实例

    Java中的同步锁,即`synchronized`关键字,是Java多线程编程中用于解决并发问题的重要机制。它确保了对共享资源的互斥访问,防止数据的不一致性。当我们有多线程环境并涉及到共享数据时,可能会出现竞态条件,就像...

    支持10000同步锁,Spring Boot,Java

    文件名`java_demo_synchronized`可能包含的是关于Java同步锁的示例代码,可以从中学习如何在实际项目中应用同步锁策略。通过深入理解同步锁的工作原理和优化技巧,我们可以构建出高效、高并发的Spring Boot应用程序...

    java同步锁的正确使用方法(必看篇)

    Java 同步锁的正确使用方法 Java 同步锁是 Java 编程语言中的一种机制,用于解决多线程环境下的线程安全问题。正确使用同步锁可以确保多线程环境下的数据安全和线程安全。 同步锁的分类 ------------ 同步锁有三...

    java编程的生产者消费者同步锁线程测试

    在Java编程中,多线程是...总之,掌握Java的线程同步锁,理解生产者消费者问题及其解决策略,是成为一名合格的Java开发者所必需的技能。通过实际的编程练习,你可以更好地领会这些概念,并运用到实际的并发应用程序中。

    基于Java synchronized同步锁实现线程交互.pdf

    "基于Java synchronized同步锁实现线程交互" Java多线程能够提高CPU利用效率,但也容易造成线程不安全、线程死锁等问题。Java synchronized同步锁可以保证同一时刻只有一个线程操作同一资源,使用wait()、notify()...

    spring实现集群同步锁

    Spring作为一款广泛使用的Java应用框架,提供了多种方式来实现集群同步锁。本篇文章将深入探讨如何利用Spring在集群环境中实现同步锁,并以Redis作为分布式锁的存储媒介进行实践。 首先,我们要理解什么是集群同步...

    java同步之如何写一个锁Lock

    "Java 同步锁实现原理详解" 在 Java 中,锁是实现同步的基础。 Lock 是 Java 中的一种同步机制,用于控制多个线程对共享资源的访问。在本篇文章中,我们将手动实现一个锁,探讨其实现原理,并了解 AQS 及各种同步器...

    层次业务模型的同步锁设计

    Java中常见的同步锁有synchronized关键字、ReentrantLock(可重入锁)、ReadWriteLock(读写锁)等。synchronized是内置的、隐式的锁,而ReentrantLock和ReadWriteLock属于显式锁,提供了更细粒度的控制和更丰富的...

    解析Java线程同步锁的选择方法

    本文将深入探讨Java线程同步锁的选择方法及其适用场景。 1. **synchronized关键字**: synchronized是Java内置的同步原语,它提供了两种使用方式:方法同步和代码块同步。在上述示例中,`method`方法通过`...

    java锁详解.pdf

    Java 锁详解 Java 锁是 Java 并发编程中的一种基本机制,用于确保线程安全和避免竞争条件。Java 锁可以分为两大类:synchronized 锁和 ReentrantLock 锁。 一、Synchronized 锁 1. 锁的原理:synchronized 锁是...

    java锁机制Synchronizedjava锁机制Synchronized

    Java 锁机制 Synchronized 是 Java 语言中的一种同步机制,用于解决多线程并发访问共享资源时可能出现的一些问题。 Java 锁机制 Synchronized 的概念 在 Java 中,每个对象都可以被看作是一个大房子,其中有多个...

    redis同步锁AOP实现

    Redis同步锁AOP实现是一种在分布式系统中控制资源访问的有效机制。Redis,作为一个高性能的键值数据存储系统,常被用作分布式锁的底层支持。本文将深入探讨如何结合注解和面向切面编程(AOP)来实现Redis锁。 首先...

    Java同步机制浅谈

    ### Java同步机制浅谈 #### synchronized关键字的作用及应用 在Java多线程环境中,`synchronized`关键字扮演着至关重要的角色。它可以帮助开发者确保多线程环境下的数据一致性,防止因并发访问导致的数据错误。本...

    Java线程同步Lock同步锁代码示例

    Java线程同步Lock同步锁代码示例 Java线程同步是指在多线程环境下,通过某种机制来确保线程之间的并发访问资源的安全性和一致性。Java提供了多种线程同步机制,其中Lock同步锁是一种强大的线程同步机制,通过显示...

    Java lock同步锁使用实例解析

    Java lock同步锁使用实例解析 Java lock同步锁是Java并发编程中的一种常见同步锁机制,主要用于解决多线程并发访问共享资源时的同步问题。在Java中,Lock是一个接口,而synchronized是Java中的关键字,两者都可以...

    java同步synchronized关键字用法示例

    Java中的`synchronized`关键字是多线程编程中的一个重要概念,用于控制并发访问共享资源时的同步机制。在Java中,当多个线程试图同时访问和修改同一块代码或数据时,可能会导致数据不一致的问题。为了解决这个问题,...

    彻底理解Java中的各种锁.pdf

    Java是一种广泛使用的编程语言,它在处理多线程环境下的数据同步和并发控制时,提供了多种锁机制来保证数据的一致性和线程的安全。本文将详细介绍Java中包括乐观锁、悲观锁、自旋锁、可重入锁、读写锁等多种锁机制的...

    大叔的大数据面试题.docx

    2. Lock 锁:使用 `java.util.concurrent.locks` 包中的锁来实现同步锁。 3. ReentrantLock 锁:使用 `java.util.concurrent.locks.ReentrantLock` 来实现可重入锁。 4. Semaphore 锁:使用 `java.util.concurrent....

Global site tag (gtag.js) - Google Analytics