java之synchronized关键字用法(隐式锁)

一、一套清晰的规则

无论是加了锁的方法还是加了锁的代码块,无论加的是对象锁还是加的是类锁,只要上的是同一把锁,那么它们的访问规则就应该是相同的。也就是上了同一把锁的东西,一个线程进来要么一起允许被访问,要么一起禁止访问。因此,如果想搞清楚访问的规则,我们首先要搞清楚锁的类型。然后判断,只要上的是同一把锁,访问的规则就应该相同。那么java中的锁有那些类型呢。可以简单的总结为两种类型:

一)java中锁的类型:

1.类锁:只有synchronized修饰静态方法或者修饰一个类的chass对象时,才是类锁。

2.对象锁:除了类锁,所有其他上锁方式都认为是对象锁,比如:synchronized修饰普通方法或者synchronized(this)给代码块上锁。

应该注意的是,因为一个类只有一个.class对象,因此所有的访问者在访问被加了类锁的方法或者代码块时候,都是共用同一把锁,而类的实例却可以有很多个,因此不同对象访问加了对象锁的方法或者代码块,它们的访问互不干扰。

二)知道了锁的类型,那么我们就可以总结出一个通用且清晰的规则了。如下:

1.加了相同锁的方法或者代码块,他们的访问规则是相同的,即当否个访问者获得该锁的时候,他们(上锁的方法或者代码块)一起向访问者开放(当前访问者可以调用这些方法)同时其他访问者如果访问(上相同锁的这些方法访问)只能等待。

2.加了不同锁的代码或者代码块,多线程访问互不影响

3.没有加锁的方法或者代码块可以随意访问不受限制

三)然后再来看怎么判断什么情况下是相同的锁。如下:

1.不同类型的锁不是同一把锁

2.加的是对象锁,那么必须是同一对象实例才是同一把锁

3.加的是类锁,那必须是同一类才是同一把锁

四)我们判断访问的规则,就是基于个步骤:

1.首先判断是不是同一把锁

2.然后判断各自的访问规则

五)锁的重入

锁重入:比如2个方法one、two都加了synchronized,one里调用了two,那么可以执行two方法(即使one没有执行完)。继承也可以用锁重入。

二、使用这个规则

1.例子1

//对象锁

   private synchronized void test1(){

      for (int i = 0; i < 3; i++) {

         System.out.println(i+":我们是test1方法。"); 

      }

   }

   private  void test2(){

      synchronized(this){

         for (int i = 0; i < 3; i++) {

               System.out.println(i+":我们是test2方法。"); 

            }

      }

   }

   private void  name() {

      System.out.println("我没有锁,随便访问。");

   }

   public static void main(String arg[]) {

      final SyncSameObjectTest1 sync=new SyncSameObjectTest1();

     

      Thread t1=new Thread(new Runnable() {

         public void run() {

            sync.test1();

         }

      });

      Thread t2=new Thread(new Runnable() {

         public void run() {

            sync.name();

            sync.test2();

         }

      });

      t1.start();

      t2.start();

   }

在代码中,可以看到我们给test1方法上了对象锁,加锁的方式是给方法加锁,加的是当前对象锁;而给test2方法上也上了对象锁,加锁的方式给代码块加锁,注意上的是当前对象锁,你也可以将this替换成任意其他对象。然后我们在main方法中看到,新建了两个线程,都是用同一个对象来调用这两个方法。因此,可以判定线程t1和t2使用的是同一把锁,因此访问规则就是t1获得了这把锁后,tes1方法和test2中被加锁的代码块都允许t1访问,都拒绝t2访问,等t1运行完,t2才会获得该锁,进行访问。因此输出的结果很明显了,t1执行完,再执行t2。而sync.name()不受限制,我们运行下程序,看看我们按照规则来推理的是否正确,运行结果如下:

0:我们是test1方法。

我没有锁,随便访问。

1:我们是test1方法。

2:我们是test1方法。

0:我们是test2方法。

1:我们是test2方法。

2:我们是test2方法。

因为t1访问test1后上个对象锁,t2这时候也去访问test2,发现test2也被上了同一个对象的锁,没有钥匙,进不去,只能等t1将对象锁释放后才能进去

然后我们再做个试验,比如将main方法中的代码改成下面的:

private synchronized void test1(){

      for (int i = 0; i < 3; i++) {

         System.out.println(i+":我们是test1方法。"); 

      }

   }

   private  void test2(){

      synchronized(this){

         for (int i = 0; i < 3; i++) {

               System.out.println(i+":我们是test2方法。"); 

            }

      }

   }

public static void main(String arg[]) {

      final SyncSameObjectTest1 st=new SyncSameObjectTest1();

      final SyncSameObjectTest1 st2=new SyncSameObjectTest1();

      Thread t1=new Thread(new Runnable() {

         public void run() {

            st.test1();

         }

      });

      Thread t2=new Thread(new Runnable() {

         public void run() {

            st2.test2();

            //或者

            //st2.test1();

         }

      });

      t1.start();

      t2.start();

   }

只是多出一个实例而已,然后线程t2通过st2来调用test2。那么就test1和test2加的都是当前对象的锁,显然它们的当前对象不同吧。所以它们不是同一把锁,互相不干扰。那么我们运行程序,效果如下:它们各自运行各自的,所以没什么顺序。

关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当作锁,所以示例代码中的那个线程先执行synchronized关键字的方法,那个线程就持有该方法所属对象的锁(Lock),两个对象,线程获得的就是两个不同的锁,他们互不影响。

0:我们是test1方法。

0:我们是test2方法。

1:我们是test2方法。

2:我们是test2方法。

1:我们是test1方法。

2:我们是test1方法。

例子2:

//类锁

   private static synchronized void test1(){

      for (int i = 0; i < 3; i++) {

         System.out.println(i+":我们是test1方法。"); 

      }

   }

   //类锁

   private  void test2(){

      synchronized(SyncSameObjectTest1.class){

         for (int i = 0; i < 3; i++) {

               System.out.println(i+":我们是test2方法。"); 

            }

      }

   }

   private void  name() {

      System.out.println("我没有锁,随便访问。");

   }

   public static void main(String arg[]) {

      final SyncSameObjectTest1 sync=new SyncSameObjectTest1();

      Thread t1=new Thread(new Runnable() {

         public void run() {

            SyncSameObjectTest1.test1();

         }

      });

      Thread t2=new Thread(new Runnable() {

         public void run() {

            sync.name();

            sync.test2();

         }

      });

      t1.start();

      t2.start();

   }

很简单,test1是一个静态方法,所以它加的锁是类锁, test2也加了一个类锁,是加在了一个代码块中,因此他们加的是相同的锁。一个类只有一个.class对象,因此所有的访问者在访问被加了类锁的方法或者代码块时候,都是共用同一把锁,即t1访问完,t2再访问。

有一种情况是所有对象都是相同的锁, 即在静态方法上加synchronized关键字,static方法是类独有,表示锁定.class类,类一级别的锁(独占.class类)。而不是对象,这样就是synchronized的.class类,所有对象都拥有共同的锁。

运行程序,结果如下:

0:我们是test1方法。

我没有锁,随便访问。

1:我们是test1方法。

2:我们是test1方法。

0:我们是test2方法。

1:我们是test2方法。

2:我们是test2方法。

例3://类锁

   private static synchronized void test1(){

      for (int i = 0; i < 3; i++) {

         System.out.println(i+":我们是test1方法。"); 

      }

   }

   //对象锁

   private synchronized void test2(){

         for (int i = 0; i < 3; i++) {

               System.out.println(i+":我们是test2方法。"); 

            }

   }

   private void  name() {

      System.out.println("我没有锁,随便访问。");

   }

   public static void main(String arg[]) {

      final SyncSameObjectTest1 sync=new SyncSameObjectTest1();

      Thread t1=new Thread(new Runnable() {

         public void run() {

            SyncSameObjectTest1.test1();

         }

      });

      Thread t2=new Thread(new Runnable() {

         public void run() {

            sync.name();

            sync.test2();

         }

      });

      t1.start();

      t2.start();

   }

代码中很显然了,test1是静态方法,它上的是类锁。而test2是普通方法,它上的对象锁。这是不同的锁。所以t1访问test1时,test2方法是不受干扰的,t2肯定可以同时访问test2.因此打印顺序为任意顺序。如下:

0:我们是test1方法。

我没有锁,随便访问。

1:我们是test1方法。

0:我们是test2方法。

2:我们是test1方法。

1:我们是test2方法。

2:我们是test2方法。

为乱序。

此时如果你的打印结果为先打印出t1的结果再是t2的结果,也不必惊讶,因为程序简单,循环次数少,CPU性能高,所以很可能t2刚启动就瞬间运行完了t1。

例子3:

//对象锁

   private  synchronized void test1(){

      for (int i = 0; i < 3; i++) {

         System.out.println(i+":我们是test1方法。"); 

      }

   }

   //类锁

   private  void test2(){

      synchronized(SyncSameObjectTest1.class){

         for (int i = 0; i < 3; i++) {

               System.out.println(i+":我们是test2方法。"); 

            }

      }

   }

   private void  name() {

      System.out.println("我没有锁,随便访问。");

   }

   public static void main(String arg[]) {

      final SyncSameObjectTest1 sync=new SyncSameObjectTest1();

      Thread t1=new Thread(new Runnable() {

         public void run() {

            sync.test1();

         }

      });

      Thread t2=new Thread(new Runnable() {

         public void run() {

            sync.name();

            sync.test2();

         }

      });

      t1.start();

      t2.start();

   }

很明显一个类锁,一个对象锁,访问规则互不干扰。即,t1访问test1方法时,并不影响t2访问test2(但是会禁止t2访问test1,因为t2用的也是sync对象,此时t1已给sync对象上锁了)。所以打印顺序可能为任意顺序,还有如果sync同时访问test2方法,由于test2是类锁,所以所有对象共有一把锁,会按照顺序打印。好了,运行程序,结果如下:

0:我们是test1方法。

我没有锁,随便访问。

0:我们是test2方法。

1:我们是test2方法。

2:我们是test2方法。

1:我们是test1方法。

2:我们是test1方法。

还有很多的实验我们就不再做了,都是根据上面总结的规则来做的。相信这下,你对上锁和上锁后的访问规则都清楚了吧。

三、锁非this对象  如:(锁String对象)

在Java中是有常量池缓存的功能的,就是说如果我先声明了一个String str1 = “a”; 再声明一个一样的字符串的时候,取值是从原地址去取的,也就是说是同一个对象。这也就导致了在锁字符串对象的时候,可以会取得意料之外的结果(字符串一样会取得相同锁),下面看一个例子介绍。都是将“xc”字符串传给上面的测试方法。下面看下测试结果。

//对象锁

   private   void lord(String str){

      try {

         synchronized(str){

            while (true) {

                System.out.println(Thread.currentThread().getName());

                Thread.sleep(100);

               

            }

         }

      } catch (Exception e) {

      }

   }

   public static void main(String arg[]) {

      final SyncSameObjectTest1 sync=new SyncSameObjectTest1();

      Thread t1=new Thread(new Runnable() {

         public void run() {

            sync.lord("xc");

         }

      });

      Thread t2=new Thread(new Runnable() {

         public void run() {

            sync.lord("xc");

         }

      });

      t1.start();

      t2.start();

   }

Thread-0

Thread-0

Thread-0

Thread-0

Thread-0

Thread-0

Thread-0

Thread-0

Thread-0

Thread-0

Thread-0

Thread-0

Thread-0

从结果可以就看到,线程B并没有进来,也就说明两个线程持有的是同一个锁,线程1一直循环没释放锁,其他线程也进不来,即字符串对象是同一个。这就是String常量池会带来的问题。所以在大多数情况下,同步代码块synchronized代码块不使用String作为锁对象,而采用其他。稍加改造,每次都创建一个新的字符串对象,不同对象的锁:

final SyncSameObjectTest1 sync=new SyncSameObjectTest1();

      Thread t1=new Thread(new Runnable() {

         public void run() {

            sync.lord(new String("xc"));

         }

      });

      Thread t2=new Thread(new Runnable() {

         public void run() {

            sync.lord(new String("xc"));

         }

      });

      t1.start();

      t2.start();

   }

Thread-0

Thread-1

Thread-1

Thread-0

Thread-1

Thread-0

四、如果你还不理解的话

如果你还不理解的话,这是某位大牛做的一个很好的比喻。你可以看看。摘抄如下:

猜你喜欢

转载自my.oschina.net/u/1054538/blog/1605499
今日推荐