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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容