JAVA面试题 请谈谈你对Sychronized关键字的理解?
面试官: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支持锁的重入;
作者:Java蚂蚁
出处:https://www.cnblogs.com/marsitman/p/11235552.html
版权:转载请在文章明显位置注明作者及出处。
JAVA面试题 请谈谈你对Sychronized关键字的理解?的更多相关文章
- Java面试题之谈谈你对Struts的理解
1. struts是一个按MVC模式设计的Web层框架,其实它就是一个大大的servlet,这个Servlet名为ActionServlet,或是ActionServlet的子类.我们可以在web.x ...
- Java面试题之谈谈reactor模型
reactor是什么? 事件驱动 可以处理一个或多个输入源 通过Service Handle同步的将输入事件采用多路复用分发给相应的Request Handler(一个或多个)处理 具体可参考:htt ...
- java面试题之谈谈你对java的理解
平台无关性:一处编译到处运行 GC:不用像c++那样手动释放堆内容 语言特性:泛型.反射.lamda表达式 面向对象:封装.继承.多态 类库:集合.并发库.网络库.IO库 异常处理
- Java 面试题 MD
Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...
- Java面试官最常问的volatile关键字
在Java相关的职位面试中,很多Java面试官都喜欢考察应聘者对Java并发的了解程度,以volatile关键字为切入点,往往会问到底,Java内存模型(JMM)和Java并发编程的一些特点都会被牵扯 ...
- java面试题(杨晓峰)---第三讲谈谈final、finally、finalize有什么不同?
java语言有很多看起来相似,但用途却完全不相同的语言要素,这些内容往往容易成为面试官考察你知识掌握程度的切入点. 今天我要问你一个基础的java经典题目,谈谈final.finally.finali ...
- 2019 Java面试题
马上又是一个金九银十的招聘旺季,小编在这里给大家整理了一套各大互联网公司面试都喜欢问的一些问题或者一些出场率很高的Java面试题,给在校招或者社招路上的你一臂之力. 首先我们需要明白一个事实,招聘的一 ...
- java面试题(目录版)
在https://www.cnblogs.com/marsitman/p/9539369.html 根据自己以往的面试经验,在该基础上做了补充和删减,均链接到相应的地址(链接失效请留言评论). 一. ...
- 大公司的Java面试题集
找工作要面试,有面试就有对付面试的办法.以下一些题目来自我和我朋友痛苦的面试经历,提这些问题的公司包括IBM, E*Trade, Siebel, Motorola, SUN, 以及其它大小公司. 面试 ...
随机推荐
- scrapy爬虫框架研究!
最近由于项目需要,开始研究scrapy爬虫,走过不少弯路,准备写个记录,记下踩过的各种坑.
- 第一式、单例模式-Singleton模式(创建型)
一.简介 单例模式主要用的作用是用于保证程序运行中某个类只有一个实例,并提供一个全局入口点.单例模式(Singleton)为GOF阐述的标准24种设计模式中最简单的一个.但随着时间推移,GOF所阐述的 ...
- epoll模型的探索与实践
我们知道nginx的效率非常高,能处理上万级的并发,其之所以高效离不开epoll的支持, epoll是什么呢?,epoll是IO模型中的一种,属于多路复用IO模型; 到这里你应该想到了,select, ...
- 【协议】TCP与UDP
转载地址:https://blog.csdn.net/qq_34988624/article/details/85856848 1.为什么会有TCP/IP协议 在世界上各地,各种各样的电脑运行着各自不 ...
- 字节跳动Java研发面试99题(含答案):JVM+Spring+MySQL+线程池+锁
JVM的内存结构 根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部分. 1. Java虚拟机栈:线程私有:每个方法在执行的时候会创建一个栈帧,存储了局部变量表, ...
- 从无到有构建vue实战项目(二)
二.vue项目的初步搭建 该项目我采用了当下最流行的vue ui框架---element-ui,首先用vue-cli构建一个vue项目: vue create education 然后会出现一系列配置 ...
- python方法和函数集锦
方法的使用: 变量.方法名(参数) 函数的使用: 函数名(参数) 字符串 1.删除空白 rstrip(): 返回去掉尾部的空格后的字符串.(不改变原字符串) lstrip(): 去掉首部空格 stri ...
- .Net之Layui多图片上传
前言: 多图上传在一些特殊的需求中我们经常会遇到,其实多图上传的原理大家都有各自的见解.对于Layui多图上传和我之前所说的通过js获取文本框中的文件数组遍历提交的原理一样,只不过是Layui中的up ...
- 利用LDAP操作AD域
LDAP操作代码样例 初始化LDAP 目录服务上下文 该例子中,我们使用uid=linly,ou=People,dc=jsoso,dc=net这个账号,链接位于本机8389端口的LDAP服务器(ld ...
- HTML标签--入门
最近开始学习前端的知识,分享自己学的一点东西 <!DOCTYPE html> <!--HTML标识,,,用于告诉浏览器,这是一个HTML文档--> <html> & ...