Java编程-多线程同步
同步的需求
例如你写了一个金融类程序,使用取钱/存钱这一对操作来表示金融交易。在这个程序里,一个线程执行取钱操作,另一个线程负责存钱操作。每一个线程操作着一对代表着金融交易的名字和金额的共享变量、类和实例域变量。对于一个合法的交易,每一个线程都必须在下一个线程操作之前完成对变量name和mount的分配。下面的例子展示了为什么需要同步。
NeedForSynchronizationDemo.java
// NeedForSynchronizationDemo.java
public class NeedForSynchronizationDemo
{
public static void main(String[] args)
{
FinTrans ft = new FinTrans();
FinTransThread depositThread = new FinTransThread(ft, "Deposit Thread");
FinTransThread withdrawalThread = new FinTransThread(ft, "Withdrawal Thread");
depositThread.start();
withdrawalThread.start();
}
}
class FinTrans
{
public static String transName;
public static int transAmount;
}
class FinTransThread extends Thread
{
private FinTrans ft;
public FinTransThread(FinTrans ft, String threadName)
{
super(threadName);
this.ft = ft;
}
@Override
public void run()
{
if (getName().equals("Deposit Thread"))
{
ft.transName = "Deposit";
try
{
Thread.sleep((int)(Math.random() * 1000));
}
catch (InterruptedException e)
{
}
ft.transAmount = 2000;
System.out.println(ft.transName + " " + ft.transAmount);
}
else
{
ft.transName = "Withdrawal";
try
{
Thread.sleep((int)(Math.random() * 1000));
}
catch (InterruptedException e)
{
}
ft.transAmount = 250;
System.out.println(ft.transName + " " + ft.transAmount);
}
}
}
我们可能期望输出结果为:
Deposit 2000
Withdrawal 250
但是结果可能是以下的几种组合
Withdrawal 250.0
Withdrawal 2000.0
Deposit 2000.0
Deposit 250.0
Java同步机制
Java的同步机制可以避免多于一个线程在同一时间执行同一段关键代码。Java的同步机制基于监听(monitor)和锁(lock)的概念。将monitor想象成一个保护装置,它保护着一段关键代码,而lock是通过保护装置的一个途径。意思是:当一个线程想要访问受保护装置保护的代码时,这个线程必须取得一个与这个monitor相关联的锁(每一个对象都有它自己的锁)。如果其他线程持有这个锁,那么JVM强制让请求的线程等待直到锁被释放。JVM提供了monitorenter
和monitorexit
指令,但是我们不必使用这种低等级的方法。我们可以使用synchronized
关键字和对应的synchronized
语句。
Synchronized语句
synchronized
语句以synchronized
关键字开始。sync object可以看做锁,而下面的代码块可以看做monitor保护的关键代码。
synchronized ("sync object")
{
// Access shared variables and other shared resources
}
SynchronizationDemo.java
// SynchronizationDemo.java
public class SynchronizationDemo
{
public static void main(String[] args)
{
FinTrans ft = new FinTrans();
FinTransThread depositThread = new FinTransThread(ft, "Deposit Thread");
FinTransThread withdrawalThread = new FinTransThread(ft, "Withdrawal Thread");
depositThread.start();
withdrawalThread.start();
}
}
class FinTrans
{
public static String transName;
public static int transAmount;
}
class FinTransThread extends Thread
{
private FinTrans ft;
public FinTransThread(FinTrans ft, String threadName)
{
super(threadName);
this.ft = ft;
}
@Override
public void run()
{
for (int i = 0; i < 100; i++)
{
if (getName().equals("Deposit Thread"))
{
synchronized(ft)
{
ft.transName = "Deposit";
try
{
Thread.sleep((int)(Math.random() * 1000));
}
catch (InterruptedException e)
{
}
ft.transAmount = 2000;
System.out.println(ft.transName + " " + ft.transAmount);
}
}
else
{
synchronized(ft)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep((int)(Math.random() * 1000));
}
catch (InterruptedException e)
{
}
ft.transAmount = 250;
System.out.println(ft.transName + " " + ft.transAmount);
}
}
}
}
}
Tips:如果想要知道线程是否获得了给定对象的锁,可以调用Thread类的holdsLock(Object o)
方法。
让方法同步
过度的使用synchronized
会导致代码运行的极为低效。例如,你的程序的一个方法里面存在连续的两个synchronized
语段,每个synchronized
代码段都尝试获取同一个对象的关联锁。由于获取和释放资源都需要消耗时间,重复地调用这个方法会降低程序的性能。
当一个实例或类的方法被冠以synchronized
关键字时,这个方法称为同步的方法。例如:synchronized void print(String s)
。当你对一个实例的方法进行同步时,每次调用这个方法都需要获得与该实例相关联的锁。
SynchronizationDemo2.java
//SynchronizationDemo2.java
public class SynchronizationDemo2
{
public static void main(String[] args)
{
FinTrans ft = new FinTrans();
FinTransThread depositThread = new FinTransThread(ft, "Deposit Thread");
FinTransThread withdrawalThread = new FinTransThread(ft, "Withdrawal Thread");
depositThread.start();
withdrawalThread.start();
}
}
class FinTrans
{
public static String transName;
public static int transAmount;
synchronized public void update(String transName, int transAmount)
{
this.transName = transName;
this.transAmount = transAmount;
System.out.println(transName + " " + transAmount);
}
}
class FinTransThread extends Thread
{
private FinTrans ft;
public FinTransThread(FinTrans ft, String threadName)
{
super(threadName);
this.ft = ft;
}
@Override
public void run()
{
for (int i = 0; i < 100; i++)
{
if (getName().equals("Deposit Thread"))
{
ft.update("Deposit Thread", 2000);
}
else
{
ft.update("Withdrawal Thread", 250);
}
}
}
}
类的方法也能被同步,一些程序混淆了同步的实例方法和类方法。以下两点需要注意:
- 对象锁和类锁之间并不关联。他们是不同的实体。获取和释放每个锁都是相互独立的。一个同步的实例方法调用一个同步的类方法两种锁都需要。首先,同步的实例方法需要对应实例的锁,同时,实例方法需要类方法的锁。
- 同步的类方法可以调用一个对象的同步方法。同步的类方法调用一个对象的同步方法也需要两个锁。
LockTypes.java
// LockTypes.java
class LockTypes
{
// Object lock acquired just before execution passes into instanceMethod()
synchronized void instanceMethod ()
{
// Object lock released as thread exits instanceMethod()
}
// Class lock acquired just before execution passes into classMethod()
synchronized static void classMethod (LockTypes lt)
{
lt.instanceMethod ();
// Object lock acquired just before critical code section executes
synchronized (lt)
{
// Critical code section
// Object lock released as thread exits critical code section
}
// Class lock released as thread exits classMethod()
}
}
同步失效
当一个线程自愿或非自愿地离开了临界代码,它会释放锁以便其他线程能获得。假设两个线程想要进入同一段临界区。为了避免线程同时进入同一个临界区,每一个线程必须尝试去取得同一个锁。如果每一个线程尝试去获得不同的锁而且成功了,两个线程都会进入临界区,一个线程无须等待另一个线程结束,因为他们拥有的是两个不同的锁。
NoSynchronizationDemo.java
// NoSynchronizationDemo.java
class NoSynchronizationDemo
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); // Save thread's name
this.ft = ft; // Save reference to financial transaction object
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
synchronized (this)
{
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
else
{
synchronized (this)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
}
}
由于this是对当前线程对象的引用,所以上述代码会导致同步失效。
死锁
在一些程序里面,下面的场景可能会发生。线程A获得了一个锁,线程B也需要这个锁来进入自己的临界区。相同的,线程B也获得一个锁,线程A需要这个锁来进入自己的临界区。由于没有一个线程获得它们需要的锁,每个线程必须等待以获得锁,此外,由于没有线程能够继续执行以释放对方所需要的锁,程序就会进去死锁状态。
DeadlockDemo.java
// DeadlockDemo.java
class DeadlockDemo
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
private static String anotherSharedLock = "";
TransThread (FinTrans ft, String name)
{
super (name); // Save thread's name
this.ft = ft; // Save reference to financial transaction object
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
synchronized (ft)
{
synchronized (anotherSharedLock)
{
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
else
{
synchronized (anotherSharedLock)
{
synchronized (ft)
{
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
}
}
}
}
}
}
Tip:为了避免死锁,我们必须仔细分析代码当中是否存在线程之间的锁依赖问题。