并发编程之剖析synchronized的用法及使用场景

The purpose of life is to grow, the essence of life is to change, the challenge of life is to conquer
生活的目的是成长,生活的本质是变化,生活的挑战是征服


synchronized关键字几乎是每次面试的必考题,今天就来聊聊synchronized的用法以及它的使用场景

众所周知,Java 内存模型中有三大特性,原子性,可见性,有序性

synchronized:保证可见性和原子性

在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存-->在主内存中拷贝最新变量的副本到工作内存-->执行完代码-->将更改后的共享变量的值刷新到主内存中-->释放互斥锁

有序性:java程序中天然的有序性可以总结为一句话,如果在本线程内观察,所有操作都是天然有序的,如果在一个线程内观察另一个线程,所有操作都是无序的

synchronized 使变量或代码块在某一时刻只能被一个线程访问,减少了内存的消耗,锁住的是对象的实例,存在堆中的实例,不是代码块,其他线程若想使用此对象必须等这个对象释放才能获取到,所以也可以把synchronized的思想理解为悲观锁

且看下面采用双重校验锁单例模式

     private volatile static Singleton singleton;

     public static Singleton getSingleton(){    
        if(null == singleton){        
           synchronized (Singleton.class) {            
              if(null == singleton){                
                   singleton = new Singleton();            
              }        
           }    
         }
         return singleton;
     }

     public static void main(String[] args) {    
       for(int i = 0;i<10;i++){        
          new Thread(new Runnable() {            
          @Override            
          public void run() {                
              System.out.println(Thread.currentThread().getName()+":"+Singleton.getSingleton().hashCode());            
          }}).start();    
     }复制代码


大家可以看到,对象Singleton采用了volatile修饰,又进行了两次判空,采用volatile修饰实现了变量的修改可见性(volatile也是一个关键字),两次判空是因为第一次判空是单例模式只需要创建一次实例,如果后面再次调用,则直接返回之前创建的实例, 因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。第二次判空是假设线程1执行第一次校验后,此时线程2也获取了cpu执行权,接下来线程2获得锁,创建实例 这时线程1,获得执行权,也创建实例,结果就会导致创建多个实例,所以需要在同步代码里面进行第二次校验, 如果实例为空,则再进行创建,这样保证只创建一个对象

synchronized 可加在方法上也可加在代码块上

class Sync{
     public synchronized void test() {          
         System.out.println("test开始..");          
         
         try {              
             Thread.sleep(1000);          
         } catch (InterruptedException e) {             
             e.printStackTrace();          
         }          
             System.out.println("test结束..");      
      }  
}    

class MyThread extends Thread {        

     public void run() {          
        Sync sync = new Sync();          
        sync.test();      
     }  
}   
 
public class Main {        
    
      public static void main(String[] args) {          
          for (int i = 0; i < 3; i++) {              
             Thread thread = new MyThread();              
             thread.start();          
          }      
      }  
} 
} 复制代码

运行结果:

test开始.. test开始.. test开始.. test结束.. test结束.. test结束..

并没有什么变化,并没有看到synchronized起到作用

那我们试试把synchronized加到代码块上,减小锁的粒度

class Sync {        

      public void test() {          
         synchronized (Sync.class) {              
              System.out.println("test开始..");              
         
         try {                  
              Thread.sleep(1000);              
         } catch (InterruptedException e) {                 
              e.printStackTrace();              
         }              
         System.out.println("test结束..");          
      }      
}  
}    


class MyThread extends Thread {        
      public void run() {          
          Sync sync = new Sync();          
          sync.test();      
      }  
}   


public class Main {        
     
        public static void main(String[] args) {         
        
           for (int i = 0; i < 3; i++) {              
               Thread thread = new MyThread();              
               thread.start();          
           }      
        }  
}  复制代码

运行结果: 
test开始.. test结束.. test开始.. test结束.. test开始.. test结束..复制代码

由此可看出,synchronized锁的不是this,而是类的Class对象

Synchronized 的可重入性

syncronized 是基于原子性的内部锁机制,是可重入的
一个线程调用syncronized方法的同时在其方法体内部调用该对象的另一个synchronized方法
当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法

总结

synchronized 锁住的要么是该类的实例,要么是传入的对象,还是那句话,synchronized使变量或代码块在某一时刻只能被一个线程访问


猜你喜欢

转载自juejin.im/post/5e8ecce9f265da48027a2763
今日推荐