Java 多线程 同步 Java编程思想读书笔记

来源:互联网 发布:淘宝招牌怎么装修 编辑:程序博客网 时间:2024/06/06 11:40

首先,我们构造 一个简单的银行家问题,定义一个银行:

public class Bank {      private int account;            public Bank(int account){          this.account = account;      }            public void transfer(int num){  //对总帐进行增减相同的数目          account += num;          account -= num;          System.out.println("银行总帐:"+account);      }        }  
为银行实现一个线程任务,对银行总帐增减相同数目:

public class BankRunnable implements Runnable {      Bank bank;      int num;            public BankRunnable(Bank bank,int num) {          this.bank = bank;          this.num = num;      }            public void run() {          while(true){              this.bank.transfer(this.num);   //对银行总帐进行增减操作              System.out.println(Thread.currentThread().getName() +            "   Account is " + bank.getAccount()); //输出相关信息              try {                  Thread.sleep(1000); //线程休眠一秒              } catch (InterruptedException e) {                  e.printStackTrace();              }          }      }  }

在测试方法中定义10个线程同时去增减同一个银行的总帐:

    public static void main(String[] args) {          Bank bank = new Bank(1000); //定义一个银行                    Thread[] threads = new Thread[10];  //定义10个线程,同时操作一个银行                    for (int i = 0; i < threads.length; i++) {              threads[i] = new Thread(new BankRunnable(bank, 100));          }                    for(int i = 0; i < threads.length; i++){              threads[i].start();          }      }  

过一个段时间后,可以看到程序出现了问题,银行总帐不再是1000了,变多或变少了。

当两个线程试图同时更新同一个帐户的时候,假定两个线程同时执行:account += num;稍微了解机算机底层的朋友都知道,这不是原子操作,该指令大概被处理为:

1).将account 加载到寄存器

2).在寄存器中对account数值 执行加法运算
3).将运算结果写入到内存中account 指向位置

假定account 为1000,而线程1执行了步骤1,2,计算结果为1100(注意,内存中account仍然为1000,计算结果1100还没有写回内存),但是,它被剥夺了运行权(抢占式调用系统),线程2被唤醒并完成了修改account 工作,则内存中account为1100,而线程1又被唤醒并完成其第3步,将之前结果1100写入内存,这一操作擦去了线程2所做的更新。于是总金额不再正确了(内存中account为1100,实际上应该为1200)

注意:删除线程run方法中的打印语句,程序出错的概率将减少。因为每个线程在再次睡眠之前所做的工作减少,那调度器在计算过程中剥夺线程的运行权就减少。


锁对象

可以用ReentrantLock保护代码块:

    myLock.lock();  //加锁    try{          critical section      }      finally{          myLock.unlock;    //解锁    }  
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他对象线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。

注意:把解锁操作放到finally子句是很重要。如果在临界区的代码抛出了异常,锁必须被释放,否则,其他线程将永远阻塞。
将上面例子银行修改为:

public class Bank {      private int account;      ReentrantLock lock; //锁对象      public Bank(int account){          this.account = account;          lock = new ReentrantLock();      }            public void transfer(int num){  //对总帐进行增减相同的数目          try{              lock.lock();    //加锁              account += num;              account -= num;          }          finally{              lock.unlock();  //解锁          }      }   }  

而不会出现总帐出错的问题了

注意:如果有多个Bank对象,那每个Bank对象都有一个ReentrantLock对象。如果两个对象试图访问两一个Bank对象,那锁以串行方式提供服务。但如果两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,则两个线程都不会发生阻塞。因为线程在操作不同的Bank实例时,线程之前不会相互影响。

锁是可重入的。因为线程可以重复地获得已经持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套调用。线程在每一次调用lock都 要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。

注意:要留心临界区中的代码,不要因为异常的抛出而跳出了临界区。如果在临界区代码结束前抛出了异常,finally子句将释放锁,但会使对象可能处于一种受损状态。如上例子中,语句account += num;account -= num;代码被加锁,如果执行完account += num;出现异常了而解锁跳出临界区,则银行总帐也是不正确的。

注意:可以通过ReentrantLock(boolean fair) 构造一个带公平策略的锁,但使用公平锁比使用常规锁要慢很多。只有当你确实了解你自己要做什么并且对于你要解决的问题有一个特定的理由必须使用公平锁的时候,才可以使用公平锁。即使使用公平锁,也无法确保线程调用器是公平的。如果线程调度器选择忽略一个线程,而该线程为了这个锁已经等待了很长时间了,那么就没有机会公平处理这个锁了。


条件对象 

我们修改一下银行对象,使之有存款,取款业务,同时对其存款,取款业务加锁

public class Bank {      private int account;      private ReentrantLock lock; //锁对象      public Bank(int account){          lock = new ReentrantLock();          this.account = account;      }            public void deposit(int num){   //存款          try{              lock.lock();              int accoutBefore = account;              account += num;              System.out.println("银行总帐:" + accoutBefore + " 存入: " + num + "  ,银行总帐:" + this.account); //输出信息,以便调试          }          finally{              lock.unlock();  //解锁          }      }            public void withdrawal(int num){    //取款          try{              lock.lock();              while(num > account){    //银行没有足够金额,则等待其他线程存款使银行有足够金额再取款                  try {                      System.out.println("金额不足,等待其他线程存款");                      Thread.sleep(1000);                  } catch (InterruptedException e) {                      e.printStackTrace();                  }              }              int accoutBefore = account;              account -= num;              System.out.println("银行总帐:" + accoutBefore + " 取出: " + num + "  ,银行总帐:" + this.account);          }          finally{              lock.unlock();  //解锁          }      }  }  

然后我们定义一个线程进行取款工作,一个线程进行存款工作 :

public class DepositRunnable implements Runnable{      private Bank bank;      private int num;            public DepositRunnable(Bank bank,int num){          this.bank = bank;          this.num = num;      }            public void run() {          while(true){              bank.deposit(num);  //存款              try {                  Thread.sleep(1000); //线程休眠一秒              } catch (InterruptedException e) {                  e.printStackTrace();              }          }      }  } 

public class WithdrawalRunnalbe implements Runnable {      private Bank bank;      private int num;        public WithdrawalRunnalbe(Bank bank,int num){          this.bank = bank;          this.num = num;      }            public void run() {          while(true){              bank.withdrawal(num);   //取款              try {                  Thread.sleep(1000); //线程休眠一秒              } catch (InterruptedException e) {                  e.printStackTrace();              }          }      }  }

主测试方法如下 :

public static void main(String[] args) {      Bank bank = new Bank(1000); //定义一个银行            Thread depositThread = new Thread(new DepositRunnable(bank, 500));  //定义存款线程,每秒存500      Thread withdrawalThread = new Thread(new WithdrawalRunnalbe(bank, 1000));   //定义取款线程,每秒取1000                depositThread.start();      withdrawalThread.start();  }  

但程序很快就进入了等待死循环,如下:


当银行中没有足够金额时,它会等待另一线程向银行注入资金。但这一线程刚刚获得了对lock排它性访问,因此别的线程没有进行存款操作的机会。所以程序进入了等待的死循环。这也就是为什么 我们需要条件对象的原因。

可以通过newCondition方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。如:sufficientFunds = lock.newCondition;当银行发现余额不足时,可以调用sufficientFunds.await()方法,使当前线程被阻塞,并放弃锁。这样另一个线程就可以进行存款操作了。一个锁对象可以有一个可多个相关的条件对象。

一旦一个线程调用了await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它仍处于阻塞状态,直到另一个线程调用同一条件对象上的signalAll方法为止。所以,当另一线程存款时,它应该调用sufficientFund.ssignalAll(),这一调用重新激活因为这一条件而等待的所有线程。它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。

一旦一个线程调用了await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它仍处于阻塞状态,直到另一个线程调用同一条件对象上的signalAll方法为止。所以,当另一线程存款时,它应该调用sufficientFund.ssignalAll(),这一调用重新激活因为这一条件而等待的所有线程。它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。

我们使用条件对象修改银行对象:

public class Bank {      private int account;      private ReentrantLock lock; //锁对象      private Condition sufficientFunds;  //条件对象    public Bank(int account){          lock = new ReentrantLock();          this.account = account;          sufficientFunds  = lock.newCondition();      }            public void deposit(int num){   //存款          try{              lock.lock();  //加锁            int accoutBefore = account;              account += num;              sufficientFunds.signalAll();  //存款后激活后的取款线程            System.out.println("银行总帐:" + accoutBefore + " 存入: " + num + "  ,银行总帐:" + this.account); //输出信息,以便调试          }          finally{              lock.unlock();  //解锁          }      }            public void withdrawal(int num){    //取款          try{              lock.lock();  //加锁            while(num > account){    //银行没有足够金额,则等待其他线程存款使银行有足够金额再取款                  try {                      System.out.println("金额不足,等待其他线程存款");                      sufficientFunds.await();  //余款不够,进入等待                } catch (InterruptedException e) {                      e.printStackTrace();                  }              }                            int accoutBefore = account;              account -= num;              System.out.println("银行总帐:" + accoutBefore + " 取出: " + num + "  ,银行总帐:" + this.account);          }          finally{              lock.unlock();  //解锁          }      }  }
一旦一个线程调用了await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它仍处于阻塞状态,直到另一个线程调用同一条件对象上的signalAll方法为止。所以,当另一线程存款时,它应该调用sufficientFund.signalAll(),这一调用重新激活因为这一条件而等待的所有线程。它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。

锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。

锁可以管理试图进入被保护代码段的线程。

锁可以拥有一个或多个相关的条件对象。

每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。


synchronized

如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用这个方法,线程必须获得内部的对象锁。

public synchronized void method(){      method body  } 
等价于

public void method(){      this.intrinsicLock.lock();      try{          method body      }      finally{this.intrinsicLock.unlock();}  }  

synchronized内部对象锁也可以使用条件对象,但只能有一个条件对象。wait方法添加一个线程到等待集中,notiryAll/notify方法解除等待线程的阻塞状态。调用wait或notifyAll等价上例中:

intrinsicCondition.await();

intrinsicCondition.signalAll();

注意:wait,notifyAll或notify方法是Obect类的final方法。所以Condition方法必须被命名为await,signalAll和signal以便它们不会与那些方法发生冲突。

可将上面例子中银行对象修改为:

public class Bank {      private int account;            public Bank(int account){          this.account = account;      }            public synchronized void deposit(int num){  //存款          int accoutBefore = account;          account += num;          notifyAll();  //存款后激活后的取款线程        System.out.println("银行总帐:" + accoutBefore + " 存入: " + num + "  ,银行总帐:" + this.account); //输出信息,以便调试      }            public synchronized void withdrawal(int num){   //取款          try{              while(num > account){    //银行没有足够金额,则等待其他线程存款使银行有足够金额再取款                  System.out.println("金额不足,等待其他线程存款");                  wait();  //添加线程到等待集中            }          }          catch(InterruptedException e){              e.printStackTrace();          }                    int accoutBefore = account;          account -= num;          System.out.println("银行总帐:" + accoutBefore + " 取出: " + num + "  ,银行总帐:" + this.account);                }  } 

使用synchronized关键字来编写代码更简洁。

内部锁synchronized和内部条件局限:

1.不能中断一个正在试图获得锁的线程。

2.试图获得锁时不能设定超时。

3.每个锁仅有单一的条件,可能是不够的。

使用Lock和Condition或同步方法的建议:

最好既不使用Lock/Condition也不使用synchronized关键字。在许多情况下可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁。

如果synchronized关键字适合你的程序,那么请尽量使用它,这样可以减少编写的代码数量,减少出错的机率。

如果特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition。

可以使用:

synchronized(this){

code

来锁定对象。

注意:

 一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

     二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

     三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对该object中所有其它synchronized(this)同步代码块的访问将被阻塞。


将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。例如,如果Bank类有一个静态同步的方法,那么当该方法被调用时,Bank.class对象的锁被锁住。因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。

实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

0 0
原创粉丝点击