谈谈对 java 中 synchronzied 的理解

Posted 小羊子说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了谈谈对 java 中 synchronzied 的理解相关的知识,希望对你有一定的参考价值。

synchronized在平时开发中和面试中常常会用到,深入了解并总结一下对synchronized的认识是有必要的,不同时期结合不同的运用场景的运用,往往会有不同角度的认识。本文总结了synchronized的三个常用经典用法。

文章目录


synchronize 英文单词翻译过来为:同步。

synchronized是Java中的关键字,是一种同步锁。

synchronized 可以修饰三个层面:

  • 修饰实例方法
  • 修饰静态方法
  • 修饰错码块

0. 为什么要线程同步?

当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各个线程之间要有先来后到,不能一窝蜂同时挤上去抢作一团。

线程同步其实就是相对于现实中的“排队”,几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

线程同步是为了防止多个线程同时访问同一个数据对象时,对数据造成破坏。

线程的同步是保证多线程安全访问资源的一种手段。

1.synchronized 修饰实例方法

public class SynDemo
	private int sum = 0;
  
  public synchronized void add()
    sum = sum + 1;
  

当前情况下的锁对象是当前实例对象,因此只有同一个实例对象调用此方法时才会产生互斥效果,不同实例不会产生互斥效果。

什么是互斥:只有某一个线程中的代码执行完成后,才会执行另一个线程中的代码。也就是说这两个线程是互斥的。

(锁可以理解为使需要互斥执行的代码块被一个标识保护,当一个线程执行此代码块时,其他线程只能等待)

实现Demo1如下:

public class SynDemo 

    public static void main(String[] args) 
        SynDemo demo1 = new SynDemo();
        SynDemo demo2 = new SynDemo();

        Thread thread1 = new Thread(new Runnable() 
            @Override
            public void run() 
                demo1.printLog(); // 1
            
        );

       Thread thread2 = new Thread(new Runnable() 
           @Override
           public void run() 
               demo2.printLog();// 2
           
       );

        thread1.start();
        thread2.start();

    

    public void printLog() 
        for (int i = 0; i < 5; i++) 
            try 
                System.out.println(Thread.currentThread().getName() + "  is printing " + i);
                Thread.sleep(300);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
    

可以看出,注释1和注释2为不同对象的方法执行,在不同的线程中调用的是不同对象的printLog方法,因此彼此之间是不会排斥的,两个线程之间依次交替执行。没有出现一个线程执行完成后才执行的情况,也不存在线程阻塞等待的情况。

如果此时将printLog中的方法改成 :

public synchronized void printLog()

虽然此时添加了synchronized,但是运行结果仍然不变。因为此时还是运行在不同的对象中,不会出现互斥。

如果我们此时将两个线程调用同一个对象的printLog方法:

实现Demo2如下:

public class SynDemo 

    public static void main(String[] args) 
        SynDemo demo1 = new SynDemo();

        Thread thread1 = new Thread(new Runnable() 
            @Override
            public void run() 
                demo1.printLog(); //1
            
        );

       Thread thread2 = new Thread(new Runnable() 
           @Override
           public void run() 
               demo1.printLog(); //2
           
       );

        thread1.start();
        thread2.start();

    

    public synchronized void printLog() 
        for (int i = 0; i < 5; i++) 
            try 
                System.out.println(Thread.currentThread().getName() + "  is printing " + i);
                Thread.sleep(300);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
    

注释1和注释2为同一对象调用printLog方法。此时运行结果如下:

由此可以看出:只有某一个线程执行完之后,才会调用另一个线程。也就是说这两个线程出现的互斥。
2022.3.18 新增使用心得:

根据上述的测试可以得出结论:如果是实例化不同的对象后,在不同的实例化对象中操作,不存在线程同步的问题,都是各做各的。如果是不同的线程操作同一个对象时,才会出现线程同步的问题。比如为什么要用单例模式等等,这里需要结合线程同步的使用场景多体会总结。

回到最开始的结论:

线程同步是为了防止多个线程同时访问同一个数据对象时,对数据造成破坏。 注意这里的“同一对象”。

2.synchronized 修饰静态方法

如果synchronized 修饰的是静待方法,则锁对象是当前的 Class 对象。因此即使在不同线程中调用不同实例对象,也会有互斥效果。

实现Demo3如下:

public class SynDemo 

    public static void main(String[] args) 
        SynDemo demo1 = new SynDemo();
        SynDemo demo2 = new SynDemo();

        Thread thread1 = new Thread(new Runnable() 
            @Override
            public void run() 
                demo1.printLog(); //1
            
        );

       Thread thread2 = new Thread(new Runnable() 
           @Override
           public void run() 
               demo2.printLog(); //2
           
       );

        thread1.start();
        thread2.start();
    

    public static synchronized void printLog()  //3
        for (int i = 0; i < 5; i++) 
            try 
                System.out.println(Thread.currentThread().getName() + "  is printing " + i);
                Thread.sleep(300);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
    

运行结果如下:

注释1和注释2 调用的是不同的实例对象,和demo1一样,不同之处在于注释3 此时添加了 static关键字,

此时产生了互斥效果。 因此,两个线程依次执行。

可对你会好奇,如果此时注释3的代码修改为:(去掉synchronzied,添加static)

public static void printLog()  

效果会怎样呢?

运行结果如下:

结果同demo1。

3.synchronized 修饰代码块

当synchronized作用于代码块时,锁对象就是跟在后面的括号中的对象。

public class SynDemo4 

    Object lock = new Object();

    public static void main(String[] args) 

        SynDemo4 demo1 = new SynDemo4();


        Thread thread1 = new Thread(new Runnable() 
            @Override
            public void run() 
                demo1.printLog(); //1
            
        );

        Thread thread2 = new Thread(new Runnable() 
            @Override
            public void run() 
                demo1.printLog(); //2
            
        );

        thread1.start();
        thread2.start();

    

    public void printLog() 
        synchronized (lock) 
            for (int i = 0; i < 5; i++) 
                try 
                    System.out.println(Thread.currentThread().getName() + "  is printing " + i);
                    Thread.sleep(300);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        
    

运行结果如下:

(其中: synchronized (lock) 改成 synchronized (this) 运行结果不变。)

上图可以看出任何 Object 对象 都可以当作锁对象。

其他知识点:

Java中实现的同步方法还有一个放在一起区分:ReentrantLock

synchronized的使用较简单,加锁和释放锁都是由虚拟机自动完成的。

而ReentrantLock需要开发者手动去完成,但是使用场景更多,公开锁、读写锁都可以在复杂场景中发挥重要的作用。关于这些的上体的介绍,不在此总结,以后遇到了 看心情再总结一波~ OVer~

4.总结

上面总结了synchronzized的常用用法。

接下来,从面试过程中考查的角度总结一下synchronized的修饰后的几种对象情况。

synchronized修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修饰一个类,其作用的范围是synchronized后面括号()括起来的部分,作用的对象是这个类的所有对象。

如有不对的地方,欢迎指出,欢迎从不同的角度解读,未完待续~

参考资料:
android工程师进阶》姜新星

以上是关于谈谈对 java 中 synchronzied 的理解的主要内容,如果未能解决你的问题,请参考以下文章

谈谈 Java 对函数式编程的支持。

谈谈对java 多态的理解

(更新中)谈谈个人对java并发编程中(管程模型,死锁,线程生命周期等问题) 见解

Java 同步容器和并发容器

谈谈对Java平台的理解

谈谈对java的理解