深入解析Java的线程同步以及线程间通信

Java线程同步
当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用。达到此目的的过程叫做同步(synchronization)。像你所看到的,Java为此提供了独特的,语言水平上的支持。

同步的关键是管程(也叫信号量semaphore)的概念。管程是一个互斥独占锁定的对象,或称互斥体(mutex)。在给定的时间,仅有一个线程可以获得管程。当一个线程需要锁定,它必须进入管程。所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程。这些其他的线程被称为等待管程。一个拥有管程的线程如果愿意的话可以再次进入相同的管程。

如果你用其他语言例如C或C++时用到过同步,你会知道它用起来有一点诡异。这是因为很多语言它们自己不支持同步。相反,对同步线程,程序必须利用操作系统源语。幸运的是Java通过语言元素实现同步,大多数的与同步相关的复杂性都被消除。

你可以用两种方法同步化代码。两者都包括synchronized关键字的运用,下面分别说明这两种方法。
使用同步方法

Java中同步是简单的,因为所有对象都有它们与之对应的隐式管程。进入某一对象的管程,就是调用被synchronized关键字修饰的方法。当一个线程在一个同步方法内部,所有试图调用该方法(或其他同步方法)的同实例的其他线程必须等待。为了退出管程,并放弃对对象的控制权给其他等待的线程,拥有管程的线程仅需从同步方法中返回。

为理解同步的必要性,让我们从一个应该使用同步却没有用的简单例子开始。下面的程序有三个简单类。首先是Callme,它有一个简单的方法call( )。call( )方法有一个名为msg的String参数。该方法试图在方括号内打印msg 字符串。有趣的事是在调用call( ) 打印左括号和msg字符串后,调用Thread.sleep(1000),该方法使当前线程暂停1秒。

下一个类的构造函数Caller,引用了Callme的一个实例以及一个String,它们被分别存在target 和 msg 中。构造函数也创建了一个调用该对象的run( )方法的新线程。该线程立即启动。Caller类的run( )方法通过参数msg字符串调用Callme实例target的call( ) 方法。最后,Synch类由创建Callme的一个简单实例和Caller的三个具有不同消息字符串的实例开始。

Callme的同一实例传给每个Caller实例。

// This program is not synchronized.
class Callme {
  void call(String msg) {
    System.out.print("[" + msg);
    try {
      Thread.sleep(1000);
    } catch(InterruptedException e) {
      System.out.println("Interrupted");
    }
    System.out.println("]");
  }
}

class Caller implements Runnable {
  String msg;
  Callme target;
  Thread t;
  public Caller(Callme targ, String s) {
    target = targ;
    msg = s;
    t = new Thread(this);
    t.start();
  }
  public void run() {
    target.call(msg);
  }
}

class Synch {
  public static void main(String args[]) {
    Callme target = new Callme();
    Caller ob1 = new Caller(target, "Hello");
    Caller ob2 = new Caller(target, "Synchronized");
    Caller ob3 = new Caller(target, "World");
    // wait for threads to end
    try {
     ob1.t.join();
     ob2.t.join();
     ob3.t.join();
    } catch(InterruptedException e) {
     System.out.println("Interrupted");
    }
  }
}

该程序的输出如下:

Hello[Synchronized[World]
]
]

在本例中,通过调用sleep( ),call( )方法允许执行转换到另一个线程。该结果是三个消息字符串的混合输出。该程序中,没有阻止三个线程同时调用同一对象的同一方法的方法存在。这是一种竞争,因为三个线程争着完成方法。例题用sleep( )使该影响重复和明显。在大多数情况,竞争是更为复杂和不可预知的,因为你不能确定何时上下文转换会发生。这使程序时而运行正常时而出错。

为达到上例所想达到的目的,必须有权连续的使用call( )。也就是说,在某一时刻,必须限制只有一个线程可以支配它。为此,你只需在call( ) 定义前加上关键字synchronized,如下:

class Callme {
  synchronized void call(String msg) {
    ...

这防止了在一个线程使用call( )时其他线程进入call( )。在synchronized加到call( )前面以后,程序输出如下:

[Hello]
[Synchronized]
[World]

任何时候在多线程情况下,你有一个方法或多个方法操纵对象的内部状态,都必须用synchronized 关键字来防止状态出现竞争。记住,一旦线程进入实例的同步方法,没有其他线程可以进入相同实例的同步方法。然而,该实例的其他不同步方法却仍然可以被调用。
同步语句

尽管在创建的类的内部创建同步方法是获得同步的简单和有效的方法,但它并非在任何时候都有效。这其中的原因,请跟着思考。假设你想获得不为多线程访问设计的类对象的同步访问,也就是,该类没有用到synchronized方法。而且,该类不是你自己,而是第三方创建的,你不能获得它的源代码。这样,你不能在相关方法前加synchronized修饰符。怎样才能使该类的一个对象同步化呢?很幸运,解决方法很简单:你只需将对这个类定义的方法的调用放入一个synchronized块内就可以了。

下面是synchronized语句的普通形式:

synchronized(object) {
  // statements to be synchronized
}

其中,object是被同步对象的引用。如果你想要同步的只是一个语句,那么不需要花括号。一个同步块确保对object成员方法的调用仅在当前线程成功进入object管程后发生。

下面是前面程序的修改版本,在run( )方法内用了同步块:

// This program uses a synchronized block.
class Callme {
  void call(String msg) {
    System.out.print("[" + msg);
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      System.out.println("Interrupted");
    }
    System.out.println("]");
  }
}

class Caller implements Runnable {
  String msg;
  Callme target;
  Thread t;
  public Caller(Callme targ, String s) {
    target = targ;
    msg = s;
    t = new Thread(this);
    t.start();
  }

  // synchronize calls to call()
  public void run() {
    synchronized(target) { // synchronized block
      target.call(msg);
    }
  }
}

class Synch1 {
  public static void main(String args[]) {
    Callme target = new Callme();
    Caller ob1 = new Caller(target, "Hello");
    Caller ob2 = new Caller(target, "Synchronized");
    Caller ob3 = new Caller(target, "World");

    // wait for threads to end
    try {
      ob1.t.join();
      ob2.t.join();
      ob3.t.join();
    } catch(InterruptedException e) {
      System.out.println("Interrupted");
    }
  }
}

这里,call( )方法没有被synchronized修饰。而synchronized是在Caller类的run( )方法中声明的。这可以得到上例中同样正确的结果,因为每个线程运行前都等待先前的一个线程结束。

Java线程间通信
多线程通过把任务分成离散的和合乎逻辑的单元代替了事件循环程序。线程还有第二优点:它远离了轮询。轮询通常由重复监测条件的循环实现。一旦条件成立,就要采取适当的行动。这浪费了CPU时间。举例来说,考虑经典的序列问题,当一个线程正在产生数据而另一个程序正在消费它。为使问题变得更有趣,假设数据产生器必须等待消费者完成工作才能产生新的数据。在轮询系统,消费者在等待生产者产生数据时会浪费很多CPU周期。一旦生产者完成工作,它将启动轮询,浪费更多的CPU时间等待消费者的工作结束,如此下去。很明显,这种情形不受欢迎。

为避免轮询,Java包含了通过wait( ),notify( )和notifyAll( )方法实现的一个进程间通信机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来是很简单的:
wait( ) 告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify( )。
notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。
notifyAll( ) 恢复相同对象中所有调用 wait( ) 的线程。具有最高优先级的线程最先运行。

这些方法在Object中被声明,如下所示:

  final void wait( ) throws InterruptedException
  final void notify( )
  final void notifyAll( )

wait( )存在的另外的形式允许你定义等待时间。

下面的例子程序错误的实行了一个简单生产者/消费者的问题。它由四个类组成:Q,设法获得同步的序列;Producer,产生排队的线程对象;Consumer,消费序列的线程对象;以及PC,创建单个Q,Producer,和Consumer的小类。

// An incorrect implementation of a producer and consumer.
class Q {
  int n;
  synchronized int get() {
    System.out.println("Got: " + n);
    return n;
  }
  synchronized void put(int n) {
    this.n = n;
    System.out.println("Put: " + n);
  }
}
class Producer implements Runnable {
  Q q;
  Producer(Q q) {
    this.q = q;
    new Thread(this, "Producer").start();
  }
  public void run() {
    int i = 0;
    while(true) {
      q.put(i++);
    }
  }
}
class Consumer implements Runnable {
  Q q;
  Consumer(Q q) {
    this.q = q;
    new Thread(this, "Consumer").start();
  }
  public void run() {
    while(true) {
      q.get();
    }
  }
}
class PC {
  public static void main(String args[]) {
    Q q = new Q();
    new Producer(q);
    new Consumer(q);
    System.out.println("Press Control-C to stop.");
  }
}

尽管Q类中的put( )和get( )方法是同步的,没有东西阻止生产者超越消费者,也没有东西阻止消费者消费同样的序列两次。这样,你就得到下面的错误输出(输出将随处理器速度和装载的任务而改变):

Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7

生产者生成1后,消费者依次获得同样的1五次。生产者在继续生成2到7,消费者没有机会获得它们。

用Java正确的编写该程序是用wait( )和notify( )来对两个方向进行标志,如下所示:

// A correct implementation of a producer and consumer.
class Q {
  int n;
  boolean valueSet = false;
  synchronized int get() {
    if(!valueSet)
      try {
        wait();
      } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
      }
      System.out.println("Got: " + n);
      valueSet = false;
      notify();
      return n;
    }
    synchronized void put(int n) {
      if(valueSet)
      try {
        wait();
      } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
      }
      this.n = n;
      valueSet = true;
      System.out.println("Put: " + n);
      notify();
    }
  }
  class Producer implements Runnable {
    Q q;
    Producer(Q q) {
    this.q = q;
    new Thread(this, "Producer").start();
  }
  public void run() {
    int i = 0;
    while(true) {
      q.put(i++);
    }
  }
}
class Consumer implements Runnable {
  Q q;
  Consumer(Q q) {
    this.q = q;
    new Thread(this, "Consumer").start();
  }
  public void run() {
    while(true) {
      q.get();
    }
  }
}
class PCFixed {
  public static void main(String args[]) {
    Q q = new Q();
    new Producer(q);
    new Consumer(q);
    System.out.println("Press Control-C to stop.");
  }
}

内部get( ), wait( )被调用。这使执行挂起直到Producer 告知数据已经预备好。这时,内部get( ) 被恢复执行。获取数据后,get( )调用notify( )。这告诉Producer可以向序列中输入更多数据。在put( )内,wait( )挂起执行直到Consumer取走了序列中的项目。当执行再继续,下一个数据项目被放入序列,notify( )被调用,这通知Consumer它应该移走该数据。

下面是该程序的输出,它清楚的显示了同步行为:

Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5
(0)

相关推荐

  • JAVA生产者消费者(线程同步)代码学习示例

    一.问题描述 生产者消费者问题是一个典型的线程同步问题.生产者生产商品放到容器中,容器有一定的容量(只能顺序放,先放后拿),消费者消费商品,当容器满了后,生产者等待,当容器为空时,消费者等待.当生产者将商品放入容器后,通知消费者:当消费者拿走商品后,通知生产者. 二.解决方案 对容器资源加锁,当取得锁后,才能对互斥资源进行操作. 复制代码 代码如下: public class ProducerConsumerTest { public static void main(String []args

  • java 线程同步详细介绍及实例代码

    java 线程同步 概要: 为了加快代码的运行速度,我们采用了多线程的方法.并行的执行确实让代码变得更加高效,但随之而来的问题是,有很多个线程在程序中同时运行,如果它们同时的去修改一个对象,很可能会造成讹误的情况,这个时候我们需要用一种同步的机制来管理这些线程. (一)竞争条件 记得操作系统中,让我印象很深的有一张图.上面画的是一块块进程,在这些进程里面分了几个线程,所有这些线程齐刷刷统一的指向进程的资源.Java中也是如此,资源会在线程间共享而不是每个线程都有一份独立的资源.在这种共享的情况下

  • Java中多线程同步类 CountDownLatch

    在多线程开发中,常常遇到希望一组线程完成之后在执行之后的操作,java提供了一个多线程同步辅助类,可以完成此类需求: 类中常见的方法: 其中构造方法: CountDownLatch(int count) 参数count是计数器,一般用要执行线程的数量来赋值. long getCount():获得当前计数器的值. void countDown():当计数器的值大于零时,调用方法,计数器的数值减少1,当计数器等数零时,释放所有的线程. void await():调所该方法阻塞当前主线程,直到计数器减

  • Java多线程编程中synchronized线程同步的教程

    0.关于线程同步 (1)为什么需要同步多线程? 线程的同步是指让多个运行的线程在一起良好地协作,达到让多线程按要求合理地占用释放资源.我们采用Java中的同步代码块和同步方法达到这样的目的.比如这样的解决多线程无固定序执行的问题: public class TwoThreadTest { public static void main(String[] args) { Thread th1= new MyThread1(); Thread th2= new MyThread2(); th1.st

  • 详解Java编程中线程同步以及定时启动线程的方法

    使用wait()与notify()实现线程间协作 1. wait()与notify()/notifyAll() 调用sleep()和yield()的时候锁并没有被释放,而调用wait()将释放锁.这样另一个任务(线程)可以获得当前对象的锁,从而进入它的synchronized方法中.可以通过notify()/notifyAll(),或者时间到期,从wait()中恢复执行. 只能在同步控制方法或同步块中调用wait().notify()和notifyAll().如果在非同步的方法里调用这些方法,在

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

    在需要线程同步的时候如何选择合适的线程锁?例:选择可以存入到常量池当中的对象,String对象等 复制代码 代码如下: public class SyncTest{    private String name = "name";public void method(String flag)    {        synchronized (name)        {            System.out.println(flag + ", invoke metho

  • Java中的线程同步与ThreadLocal无锁化线程封闭实现

    Synchronized关键字 Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块. 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(

  • Java多线程 线程同步与死锁

     Java多线程 线程同步与死锁 1.线程同步 多线程引发的安全问题 一个非常经典的案例,银行取钱的问题.假如你有一张银行卡,里面有5000块钱,然后你去银行取款2000块钱.正在你取钱的时候,取款机正要从你的5000余额中减去2000的时候,你的老婆正巧也在用银行卡对应的存折取钱,由于取款机还没有把你的2000块钱扣除,银行查到存折里的余额还剩5000块钱,准备减去2000.这时,有趣的事情发生了,你和你的老婆从同一个账户共取走了4000元,但是账户最后还剩下3000元. 使用代码模拟下取款过

  • Java中CountDownLatch进行多线程同步详解及实例代码

    Java中CountDownLatch进行多线程同步详解 CountDownLatch介绍 在前面的Java学习笔记中,总结了Java中进行多线程同步的几个方法: 1.synchronized关键字进行同步. 2.Lock锁接口及其实现类ReentrantLock.ReadWriteLock锁实现同步. 3.信号量Semaphore实现同步. 其中,synchronized关键字和Lock锁解决的是多个线程对同一资源的并发访问问题.信号量Semaphore解决的是多副本资源的共享访问问题. 今天

  • java 多线程的同步几种方法

    java 多线程的同步几种方法 一.引言 前几天面试,被大师虐残了,好多基础知识必须得重新拿起来啊.闲话不多说,进入正题. 二.为什么要线程同步 因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常.举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块.假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个

  • Java线程同步机制_动力节点Java学院整理

    在之前,已经学习到了线程的创建和状态控制,但是每个线程之间几乎都没有什么太大的联系.可是有的时候,可能存在多个线程多同一个数据进行操作,这样,可能就会引用各种奇怪的问题.现在就来学习多线程对数据访问的控制吧. 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问. 一.多线程引起的数据访问安全问题 下面看一个经典的问题,银行取钱的问题: 1).你有一张银行卡,里面有5000

随机推荐