专业的JAVA编程教程与资源

网站首页 > java教程 正文

大厂面试系列-面试中被问到如何Synchronized关键字?

temp10 2025-02-18 13:27:11 java教程 9 ℃ 0 评论

Synchroinzed 中文意思是同步的意思,也被称为是同步锁,其作用是保证在同一时刻,被Synchroinzed关键字修饰的方法或者是代码块只会有一个线程执行,从而达到并发安全的效果。Synchroinzed关键字是在Java中解决并发问题的最常用的方法,当然也是最简单的方式。下面我们就来看看在面试中被问及Synchroinzed关键字应该如何回答?

Synchroinzed 的使用

Synchroinzed有三种使用方式。

大厂面试系列-面试中被问到如何Synchronized关键字?

  • 修饰实例方法:用于对当前实例进行加锁操作
  • 修饰静态方法:用于对当前类对象进行加锁
  • 修饰方法代码块:用于指定加锁对象,对给定对象加锁

修饰方法

Synchroinzed 非常简单,直接在方法前面加上synchronized关键字即可。synchronized修饰方法和修饰代码块是类似的,只是其对应的作用范围是不一样的。修饰代码块作用范围是整个被大括号括起来的范围,修饰方法则是标识整个的方法函数。

方法一,用来修饰一个方法

public synchronized void method()
{
   // todo
}

方法二,用来修饰一个代码块

public void method()
{
   synchronized(this) {
      // todo
   }
}

synchronized关键字是不能被继承的,虽然我们使用了synchronized关键字修饰了方法,但是synchronized并不是属于方法的一部分,所以,synchronized关键字是不能被继承的。如果在父类方法中使用了synchronized关键字修饰了方法,那么如果我们在其子类代码中对该方法进行了覆盖,那么如果要让子类方法实现同步的效果,那么就必须在子类方法上也加上synchronized关键字。

public class Parent {
   public synchronized void method() { }
}
public class Child extends Parent {
   public synchronized void method() { }
}

当然如果我们在子类的方法中调用了父类中的同步方法,那么虽然子类方法是不同步的,但是由于它调用的是父类的同步方法,这样一来,子类方法也相当于同步的。

public class Parent {
   public synchronized void method() {   }
}
public class Child extends Parent {
   public void method() { super.method();   }
} 

这里需要有两点注意

  • 第一、在定义接口方法的时候不能使用synchronized关键字进行修饰
  • 第二、在构造方法上不能使用synchronized进行修饰,但是可以在构造方法内部使用synchronized来修饰代码块。

修饰代码块

一个线程访问对象中的同步代码块的情况

在上面的代码中中我们编写了一段如下的代码,并且我们强调,当一个线程访问一个对象中的代码块的时候,如果这个时候有其他的对象线程也需要访问那么就会被阻塞。

public void method()
{
   synchronized(this) {
      // todo
   }
}

下面我们来看一下如下的一段代码,通过实现Runnbale接口来实现一段多线程代码,并且在run()方法中引入了同步代码块。在代码块中对外部静态变量进行自增操作。

public class SyncThread implements Runnable {
       private static int count;
       public SyncThread() {
          count = 0;
       }
       public  void run() {
          synchronized(this) {
             for (int i = 0; i < 5; i++) {
                try {
                   System.out.println(Thread.currentThread().getName() + ":" + (count++));
                   Thread.sleep(100);
                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
             }
          }
       }
       public int getCount() {
          return count;
       }
}

我们知道,在调用这个多线程对象的时候有如下的两种实现方式。如下所示

public class Test {
    public static void main(String args[]){
  //调用方式一
  SyncThread s1 = new SyncThread();
  SyncThread s2 = new SyncThread();
  Thread t1 = new Thread(s1);
  Thread t2 = new Thread(s2);
  //调用方式二        
       SyncThread s = new SyncThread();
       Thread t1 = new Thread(s);
       Thread t2 = new Thread(s);
        
       t1.start();
       t2.start();
    }
}

调用方式一中,两个线程是同时执行的,这个是因为synchronized关键字只是锁定了对象,并且每一个对象只有一个关联的锁。而在调用方式一种,创建了两个对象,并且两个对由两个线程分别执行,所以不会出现阻塞。

调用方式二中,让两个线程并发访问同一个SyncThread对象,并且在SyncThread代码中还有一块同步代码块,也就是说在同一时刻只能由一个线程执行同步代码块中的内容,另一个线程则会阻塞等待当前线程执行完成之后再去尝试获取锁,也就是说两个线程t1和t2互斥的。

同步代码块与非同步代码块的访问

如下代码所示。

public class Counter implements Runnable{
   private int count;
 
   public Counter() {
      count = 0;
   }
 
   public void countAdd() {
      synchronized(this) {
         for (int i = 0; i < 5; i ++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }
 
   //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
   public void printCount() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + " count:" + count);
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
 
   public void run() {
      String threadName = Thread.currentThread().getName();
      if (threadName.equals("A")) {
         countAdd();
      } else if (threadName.equals("B")) {
         printCount();
      }
   }
}
 
public class Test{
    public static void main(String args[]){
        Counter counter = new Counter();
        Thread thread1 = new Thread(counter, "A");
        Thread thread2 = new Thread(counter, "B");
        thread1.start();
        thread2.start();
    }
}

可以看到在代码中,B线程的是非同步代码,并且没有影响到A线程对于同步代码的调用。从上面的运行结果来看,如果一个线程访问一个对象的synchronized 代码块的时候,别的线程如果访问了非synchronized代码块,这个是可以被允许的,也就是说不会受到阻塞。

对象加锁

代码如下,如果在某个方法调用的过程中,我们指定了一个加锁的对象,那么当一个线程访问这个加锁对象的时候,如果其他线程想要尝试访问该对象则会产生阻塞,一直到当前线程对该对象的访问结束。也就是说谁获取的了这个对象锁,谁就可以操作对应的代码。

public void method3(SomeObject obj)
{
   //obj 锁定的对象
   synchronized(obj)
   {
      // todo
   }
}

在有些情况下如果我们没有制定明确的对象锁,我们也可以创建一个特殊类型的对象来充当对象锁。

class Test implements Runnable
{
   private byte[] lock = new byte[0];  // 特殊的instance变量
   public void method()
   {
      synchronized(lock) {
         // todo 同步代码块
      }
   }
 
   public void run() {
 
   }
}

修饰静态方法

如下所示synchronized也可以用来修饰一个静态方法。

public synchronized static void method() {
   // todo
}

在之前的时候,我们知道如果一个方法被static修饰那么这个静态方法就是属于整个类的,而不是属于对象的。也就是说如果一个静态方法被synchronized修饰那么其锁定的就是这个类对应的所有对象。

改造上面的测试代码

/**
 * 同步线程
 */
class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public synchronized static void method() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + ":" + (count++));
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
 
   public synchronized void run() {
      method();
   }
}
 
public class Test{
    
    public static void main(String args[]){
        SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "A");
        Thread thread2 = new Thread(syncThread2, "B");
        thread1.start();
        thread2.start();
    }
}

Sync的两个对象,由t1和t2并发进行调用,这是由于在run()方法中调用了静态方法method。而我们之前提到过静态方法是属于类的,这个时候Sync的两个对象相当于获取的是同一把锁。所以上面这种调用中,两个对象是属于同步的。

修饰类

前面我们提到synchronized关键字还可以修饰一个类。当然这里的修饰一个类并不是说在public class 前面去加上一个修饰,而是通过如下的方式来实现。

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}

通过上面这种方式实现对于类的加锁,其实根据上面我们分析的内容来讲,给类加锁,与给类中的静态方法加锁是一样的。所使用的都是同一把类锁。

总结

到这里,关于synchronized锁的使用就介绍完了,对于synchronized底层原理的实现。我们会在后续的分享中来给大家分享,这里大家先来吸收一下如果在面试中被问到关于synchronized的实现或者是出现了上面的实现之后运行的结果是什么,并且如何进行分析回答。关于synchronized底层实现原理。我们在后面的分享中给大家详细说明,敬请期待。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表