JAVA面试题 简谈你对synchronized关键字的理解

作者:Java蚂蚁 时间:2022-09-17 17:06:05 

面试官:sychronized关键字有哪些特性?

应聘者:

  • 可以用来修饰方法;

  • 可以用来修饰代码块;

  • 可以用来修饰静态方法;

  • 可以保证线程安全;

  • 支持锁的重入;

  • sychronized使用不当导致死锁;

了解sychronized之前,我们先来看一下几个常见的概念:内置锁、互斥锁、对象锁和类锁。

内置锁

在Java中每一个对象都可以作为同步的锁,那么这些锁就被称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。

互斥锁

内置锁同时也是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B抛出异常或者正常执行完毕释放这个锁;如果B线程不释放这个锁,那么A线程将永远等待下去。

对象锁和类锁

对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的。

  • 对象锁是用于对象实例方法;

  • 类锁是用于类的静态方法或者一个类的class对象上的

一个对象无论有多少个同步方法区,它们共用一把锁,某一时刻某个线程已经进入到某个synchronzed方法,那么在该方法没有执行完毕前,其他线程无法访问该对象的任何synchronzied 方法的,但可以访问非synchronzied方法。

如果synchronized方法是static的,那么当线程访问该方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在对象的对应的Class对象,

因为java中无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的static,synchronized方法时,他们的执行也是按顺序来的,也就是说一个线程先执行,一个线程后执行。

synchronized的用法:修饰方法和修饰代码块,下面分别分析这两种用法在对象锁和类锁上的效果。

对象锁的synchronized修饰方法和代码块


public class TestSynchronized {
 public void test1() {
   synchronized (this) {
     int i = 5;
     while (i-- > 0) {
       System.out.println(Thread.currentThread().getName() + " : " + i);
       try {
         Thread.sleep(500);
       } catch (InterruptedException ie) {
       }
     }
   }
 }

public synchronized void test2() {
   int i = 5;
   while (i-- > 0) {
     System.out.println(Thread.currentThread().getName() + " : " + i);
     try {
       Thread.sleep(500);
     } catch (InterruptedException ie) {
     }
   }
 }

public static void main(String[] args) {
   final TestSynchronized myt2 = new TestSynchronized();
   Thread test1 = new Thread(new Runnable() {
     public void run() {
       myt2.test1();
     }
   }, "test1");
   Thread test2 = new Thread(new Runnable() {
     public void run() {
       myt2.test2();
     }
   }, "test2");
   test1.start();
   test2.start();
 }
}

打印结果如下:


test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0
test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0

上述的代码,第一个方法用了同步代码块的方式进行同步,传入的对象实例是this,表明是当前对象;第二个方法是修饰方法的方式进行同步

。因为第一个同步代码块传入的this,所以两个同步代码所需要获得的对象锁都是同一个对象锁,下面main方法时分别开启两个线程,分别调用test1和test2方法,那么两个线程都需要获得该对象锁,另一个线程必须等待。

上面也给出了运行的结果可以看到:直到test2线程执行完毕,释放掉锁,test1线程才开始执行。这里test2方法先抢到CPU资源,故它先执行,它获得了锁,它执行完毕后,test1才开始执行。

如果我们把test2方法的synchronized关键字去掉,执行结果会如何呢?


test1 : 4
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0
test1 : 3
test1 : 2
test1 : 1
test1 : 0

我们可以看到,结果输出是交替着进行输出的,这是因为,某个线程得到了对象锁,但是另一个线程还是可以访问没有进行同步的方法或者代码。进行了同步的方法(加锁方法)和没有进行同步的方法(普通方法)是互不影响的,一个线程进入了同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法)。

类锁的修饰(静态)方法和代码块


public class TestSynchronized {
 public void test1() {
   synchronized (TestSynchronized.class) {
     int i = 5;
     while (i-- > 0) {
       System.out.println(Thread.currentThread().getName() + " : " + i);
       try {
         Thread.sleep(500);
       } catch (InterruptedException ie) {
       }
     }
   }
 }

public static synchronized void test2() {
   int i = 5;
   while (i-- > 0) {
     System.out.println(Thread.currentThread().getName() + " : " + i);
     try {
       Thread.sleep(500);
     } catch (InterruptedException ie) {
     }
   }
 }

public static void main(String[] args) {
   final TestSynchronized myt2 = new TestSynchronized();
   Thread test1 = new Thread(new Runnable() {
     public void run() {
       myt2.test1();
     }
   }, "test1");
   Thread test2 = new Thread(new Runnable() {
     public void run() {
       TestSynchronized.test2();
     }
   }, "test2");
   test1.start();
   test2.start();
 }
}

输出结果如下:


test1 : 4
test1 : 3
test1 : 2
test1 : 1
test1 : 0
test2 : 4
test2 : 3
test2 : 2
test2 : 1
test2 : 0

类锁修饰方法和代码块的效果和对象锁是一样的,因为类锁只是一个抽象出来的概念,只是为了区别静态方法的特点,因为静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁。其实这里的重点在下面这块代码,synchronized同时修饰静态和非静态方法


public class TestSynchronized {
 public synchronized void test1() {
   int i = 5;
   while (i-- > 0) {
     System.out.println(Thread.currentThread().getName() + " : " + i);
     try {
       Thread.sleep(500);
     } catch (InterruptedException ie) {
     }
   }
 }

public static synchronized void test2() {
   int i = 5;
   while (i-- > 0) {
     System.out.println(Thread.currentThread().getName() + " : " + i);
     try {
       Thread.sleep(500);
     } catch (InterruptedException ie) {
     }
   }
 }

public static void main(String[] args) {
   final TestSynchronized myt2 = new TestSynchronized();
   Thread test1 = new Thread(new Runnable() {
     public void run() {
       myt2.test1();
     }
   }, "test1");
   Thread test2 = new Thread(new Runnable() {
     public void run() {
       TestSynchronized.test2();
     }
   }, "test2");
   test1.start();
   test2.start();
 }
}

输出结果如下:


test1 : 4
test2 : 4
test1 : 3
test2 : 3
test2 : 2
test1 : 2
test2 : 1
test1 : 1
test1 : 0
test2 : 0

上面代码synchronized同时修饰静态方法和实例方法,但是运行结果是交替进行的,这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。

synchronized是如何保证线程安全的

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题:

我们来模拟一下火车站卖票过程,总共有100张票,总共有三个窗口卖票。


public class SellTicket {
 public static void main(String[] args) {
   // 创建票对象
   Ticket ticket = new Ticket();
   // 创建3个窗口
   Thread t1 = new Thread(ticket, "窗口1");
   Thread t2 = new Thread(ticket, "窗口2");
   Thread t3 = new Thread(ticket, "窗口3");
   t1.start();
   t2.start();
   t3.start();
 }
}

// 模拟票
class Ticket implements Runnable {
 // 共100票
 int ticket = 100;

@Override
 public void run() {
   // 模拟卖票
   while (true) {
     if (ticket > 0) {
       // 模拟选坐的操作
       try {
         Thread.sleep(1);
       } catch (InterruptedException e) {
         e.printStackTrace();
       }
       System.out.println(Thread.currentThread().getName() + "正在卖票:"
           + ticket--);
     }
   }
 }
}

运行结果发现:上面程序出现了问题

  • 票出现了重复的票

  • 错误的票 0、-1

其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

那么出现了上述问题,我们应该如何解决呢?

线程同步(线程安全处理Synchronized)

java中提供了线程同步机制,它能够解决上述的线程安全问题。

线程同步的方式有两种:

  • 方式1:同步代码块

  • 方式2:同步方法

同步代码块

同步代码块: 在代码块声明上 加上synchronized


synchronized (锁对象) {
 可能会产生线程安全问题的代码
}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

使用同步代码块,对火车站卖票案例中Ticket类进行如下代码修改:


public class SellTicket {
 public static void main(String[] args) {
   // 创建票对象
   Ticket ticket = new Ticket();
   // 创建3个窗口
   Thread t1 = new Thread(ticket, "窗口1");
   Thread t2 = new Thread(ticket, "窗口2");
   Thread t3 = new Thread(ticket, "窗口3");
   t1.start();
   t2.start();
   t3.start();
 }
}

// 模拟票
class Ticket implements Runnable {
 // 共100票
 int ticket = 100;

Object lock = new Object();

@Override
 public void run() {
   // 模拟卖票
   while (true) {
     // 同步代码块
     synchronized (lock) {
       if (ticket > 0) {
         // 模拟选坐的操作
         try {
           Thread.sleep(1);
         } catch (InterruptedException e) {
           e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName()
             + "正在卖票:" + ticket--);
       }
     }
   }
 }
}

当使用了同步代码块后,上述的线程的安全问题,解决了。

同步方法

同步方法:在方法声明上加上synchronized


public synchronized void method(){
   可能会产生线程安全问题的代码
}

同步方法中的锁对象是 this

使用同步方法,对火车站卖票案例中Ticket类进行如下代码修改:


public class SellTicket {
 public static void main(String[] args) {
   // 创建票对象
   Ticket ticket = new Ticket();
   // 创建3个窗口
   Thread t1 = new Thread(ticket, "窗口1");
   Thread t2 = new Thread(ticket, "窗口2");
   Thread t3 = new Thread(ticket, "窗口3");
   t1.start();
   t2.start();
   t3.start();
 }
}

// 模拟票
class Ticket implements Runnable {
 // 共100票
 int ticket = 100;

Object lock = new Object();

@Override
 public void run() {
   // 模拟卖票
   while (true) {
     // 同步方法
     method();
   }
 }

// 同步方法,锁对象this
 public synchronized void method() {
   if (ticket > 0) {
     // 模拟选坐的操作
     try {
       Thread.sleep(10);
     } catch (InterruptedException e) {
       e.printStackTrace();
     }
     System.out.println(Thread.currentThread().getName() + "正在卖票:"
         + ticket--);
   }
 }
}

synchronized支持锁的重入吗?

我们先来看下面一段代码:


public class ReentrantLockDemo {
 public synchronized void a() {
   System.out.println("a");
   b();
 }

private synchronized void b() {
   System.out.println("b");
 }

public static void main(String[] args) {
   new Thread(new Runnable() {
     @Override
     public void run() {
       ReentrantLockDemo d = new ReentrantLockDemo();
       d.a();
     }
   }).start();
 }
}

上述的代码,我们分析一下,两个方法,方法a和方法b都被synchronized关键字修饰,锁对象是当前对象实例,按照上文我们对synchronized的了解,如果调用方法a,在方法a还没有执行完之前,我们是不能执行方法b的,方法a必须先释放锁,方法b才能执行,方法b处于等待状态,那样不就形成死锁了吗?那么事实真的如分析一致吗?

运行结果发现:


a
b

代码很快就执行完了,实验结果与分析不一致,这就引入了另外一个概念:重入锁。在 java 内部,同一线程在调用自己类中其他 synchronized 方法/块或调用父类的 synchronized 方法/块都不会阻碍该线程的执行。就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。在JDK1.5后对synchronized关键字做了相关优化。

synchronized死锁问题

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。


synchronzied(A锁){
 synchronized(B锁){
 }
}

我们进行下死锁情况的代码演示:


public class DeadLock {
 Object obj1 = new Object();
 Object obj2 = new Object();

public void a() {
   synchronized (obj1) {
     synchronized (obj2) {
       System.out.println("a");
     }
   }
 }

public void b() {
   synchronized (obj2) {
     synchronized (obj1) {
       System.out.println("b");
     }
   }
 }

public static void main(String[] args) {
   DeadLock d = new DeadLock();
   new Thread(new Runnable() {
     @Override
     public void run() {
       d.a();
     }
   }).start();

new Thread(new Runnable() {
     @Override
     public void run() {
       d.b();
     }
   }).start();
 }
}

上述的代码,我们分析一下,两个方法,我们假设两个线程T1,T2,T1运行到方法a了,拿到了obj1这把锁,此时T2运行到方法b了,拿到了obj2这把锁,T1要往下执行,就必须等待T2释放了obj2这把锁,线程T2要往下面执行,就必须等待T1释放了持有的obj1这把锁,他们两个互相等待,就形成了死锁。

为了演示的更明白,需要让两个方法执行过程中睡眠10ms,要不然很难看到现象,因为计算机执行速度贼快


public class DeadLock {
 Object obj1 = new Object();
 Object obj2 = new Object();

public void a() {
   synchronized (obj1) {
     try {
       Thread.sleep(10);
     } catch (InterruptedException e) {
       e.printStackTrace();
     }
     synchronized (obj2) {
       System.out.println("a");
     }
   }
 }

public void b() {
   synchronized (obj2) {
     try {
       Thread.sleep(10);
     } catch (InterruptedException e) {
       e.printStackTrace();
     }
     synchronized (obj1) {
       System.out.println("b");
     }
   }
 }

public static void main(String[] args) {
   DeadLock d = new DeadLock();
   new Thread(new Runnable() {
     @Override
     public void run() {
       d.a();
     }
   }).start();

new Thread(new Runnable() {
     @Override
     public void run() {
       d.b();
     }
   }).start();
 }

}

感兴趣的童鞋,下去可以试一下,程序执行不完,永远处于等待状态。

总结

  • sychronized是隐式锁,是JVM底层支持的关键字,由JVM来维护;

  • 单体应用下,多线程并发操作时,使用sychronized关键字可以保证线程安全;

  • sychronized可以用来修饰方法和代码块,此时锁是当前对象实例,修饰静态方法时,锁是对象的class字节码文件;

  • 一个线程进入了sychronized修饰的同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法);

  • sychronized支持锁的重入;

来源:https://www.cnblogs.com/marsitman/p/11235552.html

标签:java,synchronized,关键字
0
投稿

猜你喜欢

  • 详解Java关键字final

    2023-11-29 09:10:27
  • Java 分割字符串详解及实例代码

    2023-11-29 13:18:42
  • Java 守护线程_动力节点Java学院整理

    2023-11-28 07:51:14
  • mybatis-plus与JPA混合的使用方式

    2022-06-24 02:10:25
  • 详解Spring/Spring boot异步任务编程WebAsyncTask

    2022-12-18 00:40:23
  • Android ActionBar使用教程

    2023-10-24 19:09:08
  • Android组件之服务的详解

    2021-10-14 00:05:30
  • RecyclerView中监听EditText变化的BUG的解决方法

    2022-08-07 20:44:25
  • Java8 CompletableFuture 异步多线程的实现

    2023-07-21 08:07:15
  • 解决nacos升级spring cloud 2020.0无法使用bootstrap.yml的问题

    2021-12-02 19:44:29
  • BufferedInputStream(缓冲输入流)详解_动力节点Java学院整理

    2022-01-08 07:02:41
  • Java doGet, doPost方法和文件上传实例代码

    2023-01-19 03:24:19
  • springmvc参数为对象,数组的操作

    2022-04-20 07:38:03
  • Android编程实现播放视频的方法示例

    2023-04-20 06:34:17
  • Java特性队列和栈的堵塞原理解析

    2023-10-13 14:15:55
  • java实现简单的小超市程序

    2023-05-17 00:04:29
  • android Watchdog 实现剖析

    2022-07-12 06:39:21
  • Java 中的弱引用是什么

    2021-08-21 08:10:46
  • Mybatis配置返回为修改影响条数方式

    2021-10-26 12:05:05
  • Android 蓝牙开发实例解析

    2021-06-04 03:34:37
  • asp之家 软件编程 m.aspxhome.com