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提供了monitorentermonitorexit指令,但是我们不必使用这种低等级的方法。我们可以使用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);
            }
        }

    }
}

类的方法也能被同步,一些程序混淆了同步的实例方法和类方法。以下两点需要注意:

  1. 对象锁和类锁之间并不关联。他们是不同的实体。获取和释放每个锁都是相互独立的。一个同步的实例方法调用一个同步的类方法两种锁都需要。首先,同步的实例方法需要对应实例的锁,同时,实例方法需要类方法的锁。
  2. 同步的类方法可以调用一个对象的同步方法。同步的类方法调用一个对象的同步方法也需要两个锁。

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:为了避免死锁,我们必须仔细分析代码当中是否存在线程之间的锁依赖问题。

你可能感兴趣的:(Java编程-多线程同步)