关于同步机制的一些见解

首先先提一下线程安全问题,我们判断一个程序是否有线程安全的问题的标准是:

   a.是否是处于多线程环境

   b.是否有共享数据

   c.是否有多条语句操作共享数据

通过以上的参考标准我们可以清晰的知道,关于一二两点,我们是无法改变的,因此我们只能尝试去改变第三条:

思想:就是将多条语句操作共享的语句包成一个整体,即让这段代码具有原子性,让某个线程在执行的时候,别人的线程无法执行,因此Java提供可同步机制

正文开始:

    因为如果没有同步机制的话,便会由于线程的不确定先后执行顺序,可能导致数据的讹误,因此Java中有两种机制可以防止代码块受并发访问的干扰,第一种就是synchronized关键字,第二种就是JavaSE 5.0引入的ReentranLock类。

 一、Lock接口(显示锁)

首先说说Lock的实现类ReentranLock:

 1 public class Bank{
 2     
 3         private Lock bankLock = new ReentrantLock();
 4         
 5         public void transfer(int from,int to,int amount){
 6             bankLock.lock();
 7             try
 8             {
 9                     System.out.print(Thread.currentThread());
10                     ...
//getTotalBalance()方法中也有一个bankLock锁;
11 System.out.print(getTotalBalance()); 12 } 13 finally 14 { 15 bankLock.unlock(); 16 } 17 } 18 19 }

1、 假定一个线程调用 tranfer ,在执行结束前被剥夺了运行权。假定第二个线程也调用了 tranfer ,但是由于第二个线程不能获得锁,将在调用lock方法时 被阻塞。它必须等待第一个线程完成 tranfer 方法的执行之后才能再度被激活。当第一个线程释放时,那么第二个线程才能开始开始运行 ,因此这个类上锁的代码不会出现数据的讹误。

注意每一个bank对象都有自己的ReentranLock对象,如果两个线程访问同一个Bank对象,那么锁将以串行的方式提供服务,但是,如果两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞,这也是应该的,因为线程在操作不同的Bank对象的时候,线程之间时互不影响的。

2、锁是可重入的,线程可以重复地获取自己所持有的锁,锁保持了一个持有计数(holdcount)来跟踪对lock方法的嵌套使用。线程每一次调用lock都要调用unlock来释放锁,由于这个特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法:蓝色字什么意思呢?我举个例子:

例如,上图的代码,仔细看,tranfer 方法调用 getTotalBalance 方法,这也会封装bankLock 对象,此时bankLock 对象的持有计数为2,当getTotalBalance 方法退出的时候,持有计数变回1。当tranfer 方法也退出的时候,持有计数变为 0 ,线程锁释放。

注意:要留意临界区种的代码,不要因为异常的抛出而跳出了临界区。如果在临界区代码结束之前抛出了异常,finally子句将释放锁,但会使对象可能处于一种受损状态。

 

二、条件对象(条件变量)

    通常,线程进入临界区,却发现其在某一个条件满足后它才能执行,因此我们要使用一个条件对象来管理这些已经获得了锁但却不能做有用工作的线程,举个例子:

假如我们有一个银行账户,有一个线程隔三岔五的取钱(这个过程加锁,假设这个锁为bankLock),直到有一天线程取钱的金额大于账户余额了,线程应该怎么做?自然是等待另一个线程向账户中存入资金(这个过程加锁,但是问题来了取钱的线程刚刚获得了对bankLock的排他性访问,因此别的线程没有进行存款操作的机会,这就是我们为什么我们需要条件对象的原因。

一个锁对象可以有一个或者多个相关的条件对象,你可以使用newCondition方法获得一个条件对象,习惯上给每一个条件对象命名为可以反映它所表达的条件的名字,我们就用sufficientFunds表达”余额充足“条件:


 1 public class Bank{
 2     
 3         private Lock bankLock = new ReentrantLock();
 4         //代表余额充足这个条件
 5         private Condition sufficientFunds;
 6         public void transfer(int from,int to,int amount){
 7             bankLock.lock();
 8             try
 9             {
10                 //accounts[]银行账户
11                    while(accounts[from] < amount)
12                        sufficientFunds.await();
13                    //别人往accounts[]银行账户存入多少钱
14                    System.out.print(Thread.currentThread());
15                    accounts[from] -= amount;
16                    System.out.print("%10.2f from %d to %d",amount,from,to);
17                    //账户存款后变成多少钱
18                    accounts[to] += amount;
19                    System.out.print("Total :%10.2f%n",getTotalBalance());
20                    
21                     //唤醒等待线程
22                    sufficientFunds.signalAll();
23             }
24             finally
25             {
26                 bankLock.unlock();
27             }
28         }
29 }



这个时候有了条件变量,如果线程发现余额不足,就会调用sufficientFunds.await(),即当前线程被阻塞,并放弃了锁,期待另一个线程进行存款操作。

等待获得锁的线程和调await方法进行自我阻塞的线程存在着本质上的区别,一旦一个线程调用了await方法,它就进入该条件(sufficientFunds)的等待集中,当锁可用时,该线程不是马上解除阻塞,而是处于阻塞中,直到另一个线程调用同一个条件上的sufficientFunds.signalAll()方法为止,即当一个线程转账时,调用sufficientFunds.signalAll()方法,这一调用重新激活因为这一条件而阻塞的所有线程,这些阻塞的线程试图重新从await方法调用中返回,获得锁并从当初阻塞的地方继续执行,此时这些线程应该再次测试该条件是否满足,是就执行,否则继续阻塞,因为signalAll()方法仅仅只是通知阻塞等待的线程:此时有可能满足条件了,值得再次检测该条件。

三、synchronized关键字(隐式锁)

Lock和Condition接口为程序设计人员提供了高度的锁定控制,然而,大多数情况下,并不需要那样的控制。从Java 1.0开始,Java中每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法,也就是说,要调用该方法,线程必须获得内部的对象锁。

换句话说,

public synchronized void method(){
method body;
}

等价于

private Lock bankLock = new ReentrantLock();
public void method(){
bankLock.lock();
try{
method body;
}
finally{
bankLock.unlock();
}
}
我们使用synchronized关键字来修改之前的代码:
 1 public class Bank{
 2     
 3         private Lock bankLock = new ReentrantLock();
 4         //代表余额充足这个条件
 5         private Condition sufficientFunds;
 6         public synchronized void transfer(int from,int to,int amount) throws Exception{
 7             bankLock.lock();
 8             try
 9             {
10                 //accounts[]银行账户
11                    while(accounts[from] < amount)
12                        this.wait();
13                    //别人往accounts[]银行账户存入多少钱
14                    System.out.print(Thread.currentThread());
15                    accounts[from] -= amount;
16                    System.out.print("%10.2f from %d to %d",amount,from,to);
17                    //账户存款后变成多少钱
18                    accounts[to] += amount;
19                    System.out.print("Total :%10.2f%n",getTotalBalance());
20                    
21                     //唤醒等待线程
22                    this.notifyAll();
23             }
24             finally
25             {
26                 bankLock.unlock();
27             }
28         }
29 }

由代码可以看出synchronized关键字使用wait方法添加一个线程到等待集中,再通过notifyAll方法唤醒全部再次检测。也可以看出使用synchronized关键字来编写代码简洁的多,要理解这个代码,首先你要理解每一个对象有一个内部锁,并且该锁有一个内部条件。

特殊情况:将静态方法声明为synchronized也是合法的,如果这么做的话,该方法就会获得相关的类对象的内部锁,例如,如果Bank类有一个静态同步方法,方法被调用的时候,Bank.class对象被锁住,因此没有任何其他线程可以调用同一个类的这个或任何其他同步静态方法,但非静态方法就没有这么严格。

 1 public class Bank{
 2 public synchronized static void transfer(int from,int to,int amount) throwsException{
 3             
 4                 //accounts[]银行账户
 5                    while(accounts[from] < amount)
 6                        Bank.Class.wait();
 7                 ...
 8                    accounts[from] -= amount;
 9                    //账户存款后变成多少钱
10                    accounts[to] += amount;
11                                 ...
12                    
13                     //唤醒等待线程
14                    Bank.Class.notifyAll();
15             }
16 }

我们再谈谈 同步代码块、静态同步方法、非静态同步方法的锁:

  • 同步代码块可以使用自定义的Object对象,也可以使用this或者当前类的字节码文件(类名.class);
  • 静态同步方法的锁是当前类的字节码文件(类名.Class);
  • 非静态同步方法的锁是this;

写个简单的例子:

 1 public class Demo{
 2     public synchronized void showA(){
 3         System.out.println("showA..");
 4         try {
 5             Thread.sleep(3000);
 6         } catch (InterruptedException e) {
 7             e.printStackTrace();
 8         }
 9     }
10     
11     public void showB(){
12         synchronized (this) {
13             System.out.println("showB..");
14         }
15     }
16     
17     public void showC(){
18         String s="1";
19         synchronized (s) {
20             System.out.println("showC..");
21         }
22     }
23 }

测试输出:

 1 public class Test {
 2     public static void main(String[] args) {
 3         final Demo do=new Demo();
 4         new Thread(new Runnable() {
 5             
 6             @Override
 7             public void run() {
 8                 do.showA();
 9             }
10         }).start();
11         new Thread(new Runnable() {
12             
13             @Override
14             public void run() {
15                 do.showB();
16             }
17         }).start();
18         new Thread(new Runnable() {
19             
20             @Override
21             public void run() {
22                 do.showC();
23             }
24         }).start();
25     }
26 }

结果:

这段代码的打印结果是,showA…..showC…..会很快打印出来,showB…..会隔一段时间才打印出来,那么showB为什么不能像showC那样很快被调用呢?

  在启动线程1调用方法A后,接着会让线程1休眠3秒钟,这时会调用方法C,注意到方法C这里用synchronized进行加锁,这里锁的对象是s这个字符串对象。但是方法B则不同,是用当前对象this进行加锁,注意到方法A直接在方法上加synchronized,这个加锁的对象是什么呢?显然,这两个方法用的是一把锁。

  由这样的结果,我们就知道这样同步方法是用什么加锁的了,由于线程1在休眠,这时锁还没释放,导致线程2只有在3秒之后才能调用方法B,由此,可知两种加锁机制用的是同一个锁对象,即当前对象。 
  另外,同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好。

     1、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。 
     2、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。 
     3、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞,因为它们用的是同一个锁对象。

 

四、小结

在代码中应该选择哪一种 Lock和Condition对象还是同步方法或者是同步代码块?

 1、最好使用同步代码块,范围小,性能好,值得拥有!

 

未完待续。。。。。

原文地址:https://www.cnblogs.com/afei1013/p/12442326.html