某公司面试题整理
1.Java 中 int、float、byte、short、char 各占几个字节。
2. 关键字 volatile 的作用?
保持内存可见性;
防止指令重排序
一.内存模型的相关概念(保持内存可见性)
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:
i=i+1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。
比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。
两种解决方式:
总线加锁;
缓存一致性协议;
缓存一致性协议:最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
正常情况下,系统操作并不会进行缓存一致性的校验,只有变量被volatile修饰了,该变量所在的缓存行才被赋予缓存一致性的校验功能。
其实这个高速缓存+pc寄存器+其他 组成了线程的工作内存(本地内存)
我们可以看到Java内存模型中每个线程的工作内存实际上就是寄存器以及高速缓存的抽象。
缓存一致性还是保证每个缓存里面共享变量的副本是一致的;
vlolitile保证的是线程共享数据的可见性,只是读取都保证从主存里面读的一致性,不能保证写,是非原子性的;i++,i的读取可以是一致的,但是写操作无法保证;
synchronied则是原子性,不仅保证可见性,还保证原子性;因为锁住了公共资源,相当于是单个线程执行,就不存在多个线程同时读取的操作了,因为下一个线程从内存中读取的值永远是最新的;
volatile的使用场景
状态标志(开关模式)内存屏障 双重检查锁定 需要利用顺序性
二:防止指令重排
一个指令重排的问题——被部分初始化的对象
懒加载单例模式和竞态条件
DCL和被部分初始化的对象
为了解决这个问题,可以使用synchronized关键字将getInstance方法改为同步方法;但这样串行化的单例是不能忍的。所以我猿族前辈设计了DCL(Double Check Lock,双重检查锁)机制,使得大部分请求都不会进入阻塞代码块
对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
synchronized里面其实也会发生指令重排,但是因为synchronized代码块内,相当于一个单线程环境,所以根据程序次序规则,执行的结果依然正确;
问题:原子性、可见性、有序性是什么?
原子性、可见性、有序性是多线程编程中最重要的几个知识点,由于多线程情况复杂,如何让每个线程能看到正确的结果,这是非常重要的,下面和千锋广州小编一起来看看吧!
原子性
原子性是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。在多线程情况下,每个线程的执行结果不受其他线程的干扰,比如说多个线程同时对同一个共享成员变量n++100次,如果n初始值为0,n最后的值应该是100,所以说它们是互不干扰的,这就是传说的中的原子性。但n++并不是原子性的操作,要使用AtomicInteger保证原子性。
可见性
可见性是指某个线程修改了某一个共享变量的值,而其他线程是否可以看见该共享变量修改后的值。在单线程中肯定不会有这种问题,单线程读到的肯定都是最新的值,而在多线程编程中就不一定了。
每个线程都有自己的工作内存,线程先把共享变量的值从主内存读到工作内存,形成一个副本,当计算完后再把副本的值刷回主内存,从读取到最后刷回主内存这是一个过程,当还没刷回主内存的时候这时候对其他线程是不可见的,所以其他线程从主内存读到的值是修改之前的旧值。
像CPU的缓存优化、硬件优化、指令重排及对JVM编译器的优化,都会出现可见性的问题。
有序性
我们都知道程序是按代码顺序执行的,对于单线程来说确实是如此,但在多线程情况下就不是如此了。为了优化程序执行和提高CPU的处理性能,JVM和操作系统都会对指令进行重排,也就说前面的代码并不一定都会在后面的代码前面执行,即后面的代码可能会插到前面的代码之前执行,只要不影响当前线程的执行结果。所以,指令重排只会保证当前线程执行结果一致,但指令重排后势必会影响多线程的执行结果。
虽然重排序优化了性能,但也是会遵守一些规则的,并不能随便乱排序,只是重排序会影响多线程执行的结果。
问题:为什么synchronized无法禁止指令重排,却能保证有序性?
首先我们要分析下这道题,不得不说这个面试官还是有一定的水平的,这简单的一个问题,其实里面还是包含了很多信息的,要想回答好这个问题,面试者至少要知道一下概念:
Java内存模型、并发编程有序性问题、指令重排、synchronized锁、可重入锁、排它锁、as-if-serial语义、单线程&多线程
(1).为了进一步提升计算机各方面能力,在硬件层面做了很多优化,如处理器优化和指令重排等,但是这些技术的引入就会导致有序性问题。
(2).我们也知道,最好的解决有序性问题的办法,就是禁止处理器优化和指令重排,就像volatile中使用内存屏障一样。
(3).但是,虽然很多硬件都会为了优化做一些重排,但是在Java中,不管怎么排序,都不能影响单线程程序的执行结果。这就是as-if-serial语义,所有硬件优化的前提都是必须遵守as-if-serial语义。
(4).再说下synchronized,他是Java提供的锁,可以通过他对Java中的对象加锁,并且他是一种排他的、可重入的锁。
(5).所以,当某个线程执行到一段被synchronized修饰的代码之前,会先进行加锁,执行完之后再进行解锁。在加锁之后,解锁之前,其他线程是无法再次获得锁的,只有这条加锁线程可以重复获得该锁。
(6).synchronized通过排他锁的方式就保证了同一时间内,被synchronized修饰的代码是单线程执行的。所以呢,这就满足了as-if-serial语义的一个关键前提,那就是单线程,因为有as-if-serial语义保证,单线程的有序性就天然存在了;
as-if-serial语义的意思是:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。所以编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。
在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果。
在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
volatile和synchronized的区别
使用上的区别
volatile只能修饰变量,synchronized只能修饰方法和语句块;
对原子性的保证
synchronized可以保证原子性,volatile不能保证原子性;
对可见性的保证
都可以保证可见性,但实现原理不同,volatile对变量加了lock,synchronized使用monitorEnter和monitorExit;
对有序性的保证
都可以保证有序性,但是synchronized并发退化到串行;
其他
synchronized引起阻塞;
volatile不会引起阻塞;
- 一个servlet 被请求 10 次,会创建几个实例?
一个,因为servlet是单例;
Java 有状态和无状态对象的区别
无状态就是对于一次操作,不能保存数据。无状态对象(Stateless Bean)是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。
也称为有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
简言之就是类里面有没有成员变量,或者有成员变量,成员变量也是没有状态的,就是没有可操作的成员变量,没有可以共享的。
Servlet的生命周期:
Servlet是运行在Servlet容器中的,常用的tomcat、jboss、weblogic都是Servlet容器,其生命周期是由容器来管理。Servlet的生命周期通过java.servlet.Servlet接口中的init()、service()、和destroy()方法表示。Servlet的生命周期有四个阶段:加载并实例化、初始化、请求处理、销毁。
加载并实例化
Servlet容器负责加载和实例化Servelt。当Servlet容器启动时,或者在容器检测到需要这个Servlet来响应第一个请求时,创建Servlet实例。当Servlet容器启动后,Servlet通过类加载器来加载Servlet类,加载完成后再new一个Servlet对象来完成实例化。
初始化
在Servlet实例化之后,容器将调用init()方法,并传递实现ServletConfig接口的对象。在init()方法中,Servlet可以部署描述符中读取配置参数,或者执行任何其他一次性活动。在Servlet的整个生命周期类,init()方法只被调用一次。
请求处理
当Servlet初始化后,容器就可以准备处理客户机请求了。当容器收到对这一Servlet的请求,就调用Servlet的service()方法,并把请求和响应对象作为参数传递。当并行的请求到来时,多个service()方法能够同时运行在独立的线程中。通过分析ServletRequest或者HttpServletRequest对象,service()方法处理用户的请求,并调用ServletResponse或者HttpServletResponse对象来响应。
销毁
一旦Servlet容器检测到一个Servlet要被卸载,这可能是因为要回收资源或者因为它正在被关闭,容器会在所有Servlet的service()线程之后,调用Servlet的destroy()方法。然后,Servlet就可以进行无用存储单元收集清理。这样Servlet对象就被销毁了。这四个阶段共同决定了Servlet的生命周期。
Servlet的调用过程
1.客户端通过发送请求给Tomcat,Tomcat发送客户端的请求页面给客户端。
2.用户对请求页面进行相关操作后将页面提交给Tomcat,Tomcat将其封装成一个HttpRequest对象,然后对请求进行处理,。
3.Tomcat截获请求,根据action属性值查询xml文件中对应的servlet-name,再根据servlet-name查询到对应的java类(如果是第一次,Tomcat则会将servlet编译成java类文件,所以如果servlet有很多的话第一次运行的时候程序会比较慢)。
4.Tomcat实例化查询到的java类,注意该类只实例化一次。
5.调用java类对象的service()方法(如果不对service()方法进行重写则根据提交的方式来决定执行doPost()方法还是doGet()方法)。
6.通过request对象取得客户端传过来的数据,对数据进行处理后通过response对象将处理结果写回客户端。
Servlet是线程安全的吗?
从上面Servlet的调用过程可以看出,当客户端第一次请求Servlet的时候,tomcat会根据web.xml配置文件实例化servlet,
当又有一个客户端访问该servlet的时候,不会再实例化该servlet,也就是多个线程在使用这个实例。
JSP/Servlet容器默认是采用单实例多线程(这是造成线程安全的主因)方式处理多个请求的,这种默认以多线程方式执行的设计可大大降低对系统的资源需求,提高系统的并发量及响应时间。
Servlet本身是无状态的,一个无状态的Servlet是绝对线程安全的,无状态对象设计也是解决线程安全问题的一种有效手段。
所以,servlet是否线程安全是由它的实现来决定的,如果它内部的属性或方***被多个线程改变,它就是线程不安全的,反之,就是线程安全的。
影响Servlet线程安全的因素
多线程下每个线程对局部变量都会有自己的一份copy,这样对局部变量的修改只会影响到自己的copy而不会对别的线程产生影响,所以这是线程安全的。
但是对于实例变量来说,由于servlet在Tomcat中是以单例模式存在的,所有的线程共享实例变量。多个线程对共享资源的访问就造成了线程不安全问题。
如何控制Servlet的线程安全性?
避免使用实例变量
避免使用非线程安全的集合
在多个Servlet中对某个外部对象(例如文件)的修改是务必加锁(Synchronized,或者ReentrantLock),互斥访问。
属性的线程安全:ServletContext、HttpSession是线程安全的;ServletRequest是非线程安全的。
问题:
public static int singleNumber(int[] nums) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; i++) {
if (map.get(nums[i]) != null) {
map.put(nums[i], 2);
} else {
map.put(nums[i], 1);
}
}
int num = 0;
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
int mapKey = entry.getKey();
int mapValue = entry.getValue();
if (mapValue == 1) {
num = mapKey;
break;
}
}
for(Map.Entry<Integer,Integer> entry:map.entrySet()){
int key = entry.getKey();
int value = entry.getValue();
}
return num;
}
安克创新 Anker公司福利 911人发布