Home >Java >javaTutorial >In-depth analysis of Java's thread synchronization and inter-thread communication

In-depth analysis of Java's thread synchronization and inter-thread communication

高洛峰
高洛峰Original
2017-01-05 15:18:311288browse

Java Thread Synchronization
When two or more threads need to share resources, they need some way to determine that the resource is occupied by only one thread at a certain moment. The process to achieve this is called synchronization. As you can see, Java provides unique, language-level support for this.

The key to synchronization is the concept of a monitor (also called a semaphore). A monitor is a mutually exclusive locked object, or mutex. Only one thread can obtain the monitor at a given time. When a thread needs a lock, it must enter the monitor. All other threads trying to enter a locked tube must hang until the first thread exits the tube. These other threads are called wait monitors. A thread that owns a monitor can enter the same monitor again if it wishes.

If you have used synchronization in other languages ​​such as C or C++, you will know that it is a bit weird to use. This is because many languages ​​do not support synchronization themselves. In contrast, for synchronized threads, the program must make use of operating system source language. Fortunately, Java implements synchronization through language elements, and most of the complexity associated with synchronization is eliminated.

You can synchronize code in two ways. Both include the use of the synchronized keyword. The two methods are explained below.
Using synchronization methods

Synchronization in Java is simple because all objects have their corresponding implicit monitors. Entering the monitor of an object is to call the method modified by the synchronized keyword. When a thread is inside a synchronized method, all other threads of the same instance that attempt to call that method (or other synchronized methods) must wait. In order to exit the monitor and relinquish control of the object to other waiting threads, the thread owning the monitor simply returns from the synchronized method.

To understand the necessity of synchronization, let's start with a simple example where synchronization should be used but is not. The following program has three simple classes. The first is Callme, which has a simple method call(). The call() method has a String parameter named msg. This method attempts to print the msg string within square brackets. The interesting thing is that after calling call() to print the left bracket and msg string, Thread.sleep(1000) is called, which pauses the current thread for 1 second.

The constructor Caller of the next class refers to an instance of Callme and a String, which are stored in target and msg respectively. The constructor also creates a new thread that calls the object's run() method. The thread starts immediately. The run() method of the Caller class calls the call() method of the Callme instance target through the parameter msg string. Finally, the Synch class starts by creating a simple instance of Callme and three instances of Caller with different message strings.

The same instance of Callme is passed to each Caller instance.

// 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");
    }
  }
}

The output of this program is as follows:

Hello[Synchronized[World]
]
]

In this example, by Calling sleep(), the call() method allows execution to transition to another thread. The result is a mixed output of the three message strings. In this program, there is no way to prevent three threads from calling the same method of the same object at the same time. This is a race condition because three threads compete to complete the method. The example uses sleep( ) to make this effect repeatable and obvious. In most cases, contention is more complex and unpredictable because you can't be sure when a context switch will occur. This causes the program to sometimes run fine and sometimes fail.

In order to achieve the purpose of the above example, you must have the right to use call() continuously. That is, at a certain moment, it must be restricted to only one thread that can dominate it. To do this, you only need to add the keyword synchronized before the definition of call(), as follows:

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

This prevents the use of call() in a thread Other threads enter call(). After synchronized is added to the front of call(), the program output is as follows:

[Hello]
[Synchronized]
[World]

Any time in a multi-threaded situation, you have one method or multiple methods to manipulate The internal state of the object must use the synchronized keyword to prevent state competition. Remember, once a thread enters a synchronized method of an instance, no other thread can enter a synchronized method of the same instance. However, other asynchronous methods of the instance can still be called.
Synchronization Statement

Although creating a synchronization method inside the created class is a simple and effective way to obtain synchronization, it does not work all the time. Please think about the reasons for this. Suppose you want to obtain synchronized access to a class object that is not designed for multi-threaded access, that is, the class does not use the synchronized method. Moreover, the class was not created by you but by a third party, and you cannot obtain its source code. In this way, you cannot add the synchronized modifier before the relevant method. How can an object of this class be synchronized? Fortunately, the solution is simple: you just put the calls to the methods defined by this class inside a synchronized block.

The following is the common form of the synchronized statement:

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

    

更多深入解析Java的线程同步以及线程间通信相关文章请关注PHP中文网!


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn