JAVA面试题 简谈你对synchronized关键字的理解
面试官: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支持锁的重入;
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。