java经典面试题
什么是线程安全?
又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
线程安全也可以划分为几个级别,java语言中各种擦偶哦共享的数据分为一下5类:不可变,绝对线程安全,相对线程安全,线程兼容,线程对立。
- 不可变:访问不可变对应,string或者final修饰的对象(不能引用逃逸),永远都是安全的。
- 绝对线程安全:不管运行环境如何,调用者不需要额外的同步措施。比如vector中的每个方法都是synchronized的但是这些方法组合起来用的时候,仍然不能保证线程安全。
- 相对线程安全:它能保证对象单独的操作是线程安全的,在调用的时候不需要额外的同步。但是如果特定顺序的连续调用,可能需要额外的手段来保证了。比如刚才的vector,hashtable都是相对安全的。
- 线程兼容:对象本身不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境的安全使用。
- 线程对立:无用是否采用了同步措施,都无法在多线程环境并发的使用代码。比如Thread的suspend和resume操作,可能产生死锁风险。
怎么实现线程安全
-
互斥同步
通过synchronized,Semaphore,Mutex等实现互斥。synchronized会在生成字节码时,加上monitorenter和monitorexit两个字节码指令,这个关键字是可重入的,同一个线程可多次通过synchronized获取对象的锁,每进入一次,锁的计数器加1,退出关键区时,锁的计数器减1.当计数器减为0时,锁释放,其他线程可获取锁。synchronized
是非公平锁。线程一旦因为synchronized进入阻塞状态,就需要从用户态转换为内核态。因为阻塞和唤醒线程需要操作系统帮忙。
reentrantlock是api层面的锁,比synchronized提供了一些高级功能:等待可中断,公平锁,绑定多个条件。
性能上,jdk6对synchronized做了很多优化,两者并未有多大区别,因此性能不是主要的考虑因素。 -
非阻塞同步
互斥同步相当于悲观锁,认为竞争总会发生,所以总是需要加锁。非阻塞同步使用乐观锁,如果没有竞争,会直接成功,
如果发生了竞争,则进行尝试。失败重视这个操作需要依赖硬件指令集,来保证cas是一个原子操作。如果不能cas是原子操作,则不能保证cas的正确性。cas不能解决aba的问题。 -
无同步方案
因为有些代码天生就是线程安全的,这类叫做可重入代码。这类代码有这样的特点:不会访问共享变量,不会调用非重入方法。
还有一种线程本地存储方案,threadlocal,变量存储在线程里,不存在共享问题。多用空间来解决共享问题。但是也很多场景不适用,因为有的场景就是要通过共享变量来进行线程间通信的效果。
volatile变量
普通变量在线程间的传递需要借助于主内存来完成。比如线程a修改一个变量的值,然后向主内存同步。线程b在线程a回写完成之后再从主内存进行读取,新变量才会对线程b可见。
volatile变量可以保证可见性,即对volatile变量的所有写操作都能立刻反应到其他线程中。因为volatile变量
在每次使用之前都必须从主内存刷新最新的值,执行引擎看不到不一致的情况。
volatile变量的另一个重要作用就是禁止指令重排序,如果指令之间有依赖关系,比如指令2依赖了指令1的运算结果,
重排序后,指令2肯定还是排在指令1后面。对于没有依赖关系的指令,执行顺序可能跟代码中的顺序不一样,这是机器级别的优化操作。当更改volatile变量的值时,会加入一个内存屏障,防止后面的指令重排序到这个指令前。写入操作相当于做了一次store和write操作,每次修改后,必须立刻同步回主内存。
java内存模型
java内存模型定义了各个对象变量的访问规则,不包括局部变量和方法参数。因为局部变量不会被共享,不存在竞争问题。
java内存模型规定了所有的变量都存储在主内存,每条线程还有自己的工作内存。线程的工作内存保存了该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存中的变量。
线程间变量的共享都要通过主内存来完成。理清线程、主内存、工作内存的关系。
对于上面说的线程工作内存有变量的拷贝,并不一定是拷贝整个对象变量,也可能是拷贝对象中的某个字段。
内存间的变量交互操作,load use store write
堆外内存
堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。
JAVA开发用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。对堆外内存的申请主要是通过成员变量unsafe来操作,
-
优点
1、减少了垃圾回收
因为垃圾回收会暂停其他的工作。
2、加快了复制的速度
堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。所以那些所谓的零拷贝,就是使用了堆外内存。 -
缺点
堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。 -
jvm可以通过 -XX:MaxDirectMemorySize参数配置对外内存的大小。堆外内存也可以通过system.gc主动回收,也可以通过ByteBuffer.cleaner.clean()方法回收。
先行发生原则(happen before原则)
意思就是规定了一些保证可见性的操作。操作A先行于操作b发生,那么操作A产生的结果一定能够被b看到。所谓的操作包括了修改变量的值,发送消息等。java内存模型保证了一些天然的先行发生关系:
- 一个线程内,按照代码顺序,书写在前面的操作先行发生于在书写在后面的操作。
- unlock操作先行发生于同一个锁的lock操作。
- 线程的start操作发生在终止操作之前
- 对象的初始化先行发生于finalize方法被调用前。
- 等等其他一些操作
先行发生具有传递性。
先行发生原则跟代码的执行时间先后顺序没有必然关系。
使用for(int j = 0; j < size; j ++)这种方式遍历list要比使用foreach快
因为foreach底层是使用iterator来做的。每次遍历前,都要调用一次hasNext()方法。
第一种方式是直接用地址来取元素。
get和post的区别
get参数挂在后面,只能url编码,可以保留查询参数,参数长度有限制。
post参数在body里,支持多种编码,长度无限制。
底层都是基于tcp没有本质区别。post请求也可以在链接后面挂参数。get也可以有body。
他们的区别只是浏览器和http协议以及大家的常识来约书的。
至于网上说的get发一次请求,post发两次请求,我一直没有证实。
link