43.字符流和字节流的区别
45.stringbuffer内部的数据结构在字符串变化时怎么操作
String 类不可变,内部维护的char[] 数组长度不可变,为final修饰,String类也是final修饰,不存在扩 容。字符串拼接,截取,都会生成一个新的对象。频繁操作字符串效率低下,因为每次都会生成新的对象。 StringBuilder 类内部维护可变长度char[] , 初始化数组容量为16,存在扩容, 其append拼接字符串方法内 部调用System的native方法,进行数组的拷贝,不会重新生成新的StringBuilder对象。非线程安全的字符串操 作类, 其每次调用 toString方法而重新生成的String对象,不会共享StringBuilder对象内部的char[],会进 行一次char[]的copy操作。 StringBuffer 类内部维护可变长度char[], 基本上与StringBuilder一致,但其为线程安全的字符串操作类, 大部分方法都采用了Synchronized关键字修改,以此来实现在多线程下的操作字符串的安全性。其toString方法 而重新生成的String对象,会共享StringBuffer对象中的toStringCache属性(char[]),但是每次的 StringBuffer对象修改,都会置null该属性值
如果JDK64位,8字节是引用,16字节是堆内存(对象头大小),总共是8+16=24字节,所以new一个Object对象占 用8+16=24字节。 如果JDK是32位,按如上分析方法可知new一个Object对象占用4+8=12字节
【转】Java new一个Object对象占用多少内存? - MERRU - 博客园
jvm
引用计数算法(已废弃)
可达性分析
引用计数算法简单高效,早期的Java虚拟机中使用这个方式,但是正如上面提到的不能解决“引用闭环”的问题,后来的Java虚拟机中普遍采用根集算法。从GCRoot(比如一个静态变量)开始遍历引用关系,能遍历到的,叫做引用可达,遍历不到的叫做不可达。不可达的对象就被判“死刑了”,GC的时候将被枪毙掉。
可以作为GCRoots的对象包括下面几种:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
2.JVM崩溃原因查找?(jvm内存镜像文件分析,常用jvm性能分析命令)
1、内存泄漏是指分配出去的内存无法回收了
2、内存溢出是指程序要求的内存,超出了系统所能分配的范围,从而发生溢出。
OOM是内存泄漏的常见指示
Java heap space 不一定意味着内存泄漏
PermGen space 此错误消息表明永久代已满。
Requested array size exceeds VM limit 此错误表示应用程序(或该应用程序使用的API)尝试分配大于堆大小的数组
Request bytes for . Out of swap space 当本机堆的分配失败并且本机堆可能将被耗尽时,HotSpot VM会抛出此异常
Native method 在JNI或本机方法中检测到Java内存分配失败,而不是在Java VM代码中检测到
Application Crash Without OOM 应用程序可能会在从本机堆分配失败后很快崩溃
Java VisualVM执行内存泄漏检测的工具
jstack
线程共享:堆,非堆,本地方法栈
线程独享:栈,程序计数器,
Java.lang.OutOfMemeoryError:GC overhead limit exceeded
如上异常,即程序在垃圾回收上花费了98%的时间,却收集不回2%的空间,通常这样的异常伴随着CPU的冲高。定位方法同上(内存泄漏)
Java.lang.OutOfMemoryError: PermGen space(JAVA8引入了Metaspace区域)
永久代内存被耗尽(内存溢出)
Java.lang.OutOfMemeoryError:Unable to create new native thread
Java应用已经达到它可以运行的线程数的上限.
解决方法
1、检查是否存在大对象的分配,最有可能的是大数组分配
2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
12.常见JVM参数列表
13.JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor?
15.你知道哪几种垃圾收集器,各自的优缺点,重点讲下 cms,包括原理,流程,优缺点
16.JVM为什么需要GC
多线程
3.什么是线程安全
在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态
9.解释以下名词:重排序,自旋锁,偏向锁,轻量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁
重量级锁( Mutex Lock)
Synchronized 是通过对象内部的一个叫做监视器锁( monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。因此, 这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁” 。 JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
轻量级锁
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。锁升级随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。“ 轻量级” 是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前, 先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
偏向锁
Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。 偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。 引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令, 而偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过, 轻量级锁是为了在线程交替执行同步块时提高性能, 而偏向锁则是在只有一个线程执行同步块时进一步提高性能
自旋锁
自旋锁原理非常简单, 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁
的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来
说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁;
公平锁( Fair)
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
非公平锁( Nonfair)
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
1. 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
2. Java 中的 synchronized 是非公平锁, ReentrantLock 默认的 lock()方法采用的是非公锁。
可重入锁(递归锁)
本文里面讲的是广义上的可重入锁,而不是单指 JAVA 下的 ReentrantLock。 可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过 CAS 操作实现的, CAS 是一种更新的原子操作, 比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是 Synchronized,AQS框架下的锁则是先尝试 cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
10.重排序
12.线程池原理,并说说newCache和newFixed有什么区别,构造函数的各个参数的含义是什么
shutdown()
调用后,不可以再 submit 新的 task,已经 submit 的将继续执行shutdownNow()
调用后,试图停止当前正在执行的 task,并返回尚未执行的 task 的 list
14.假如有一个第三方接口,有很多个线程去调用获取数据,现在规定每秒钟最多有10个线程同时调用它,如何做到
Semaphore类是一个计数信号量,必须由获取它的线程释放, 通常用于限制可以访问某些资源(物理或逻辑的)线程数目。
15.spring的controller是单例还是多例,怎么保证并发的安全
16.用三个线程按顺序循环打印 abc 三个字母,比如 abcabcabc
18.AQS同步器的实现原理(AbstractQueuedSynchronizer)
19.countdowlatch内部原理和用法(比如countdownlatch的await方法是怎么实现的)
CountDownLatch是基于AQS的共享锁来实现的,由于内部类继承了AQS,所以它内部也是FIFO队列,同时也一样是
前驱节点唤醒后继节点,不能像CyclicBarrier那样使用完毕后还可以复用;
1任务分为N个任子线程去执行,state也初始化为N(注意N要要与线程个数一致)
2这N个线程也是并行执行的,每个子线程执行完后后countDown()一次,state会CAS减1
3等到所有子线程都执行完成之后即(state==0),会调用LockSupport.unpark(s.thread)
4然后主调用会从await()函数返回(之前是通过LockSupport.park(this);阻塞),继续后余动作
CyclicBarrier实现主要基于ReentrantLock 默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告 诉CyclicBarrier已经到达屏障位置,线程被阻塞。 另外一个构造方法CyclicBarrier(int parties, Runnable barrierAction),其中barrierAction任务会在 所有线程到达屏障后执行。
21.Lock锁的实现原理
需要实现锁的功能,两个必备元素: 一个是表示(锁)状态的变量(我们假设0表示没有线程获取锁,1表示已有线程占有锁),该变量必须声明为 voaltile类型; 另一个是队列,队列中的节点表示因未能获取锁而阻塞的线程。
24.进程和线程的区别
27.简述ConcurrentLinkedQueue和LinkedBlockingQueue 的用处和不同之处
28.Queue添加数据方法add()put()offer()不同之处
add方法在添加元素的时候,若超出了度列的长度会直接抛出异常
offer方法在添加元素时,如果发现队列已满无法添加的话,会直接返回false。
put方法,若向队尾添加元素的时候发现队列已经满了会发生阻塞一直等待空间,以加入元素。
30编写一段死锁代码
//首先我们先定义两个final的对象锁.可以看做是共有的资源. final Object lockA = new Object(); final Object lockB = new Object(); //生产者A class ProductThreadA implements Runnable{ @Override public void run() { //这里一定要让线程睡一会儿来模拟处理数据 ,要不然的话死锁的现象不会那么的明显.这里就是同步语句块里面,首先获得对象锁lockA,然后执行一些代码,随后我们需要对象锁lockB去执行另外一些代码. synchronized (lockA){ //这里一个log日志 Log.e("CHAO","ThreadA lock lockA"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB){ //这里一个log日志 Log.e("CHAO","ThreadA lock lockB"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } //生产者B class ProductThreadB implements Runnable{ //我们生产的顺序真好好生产者A相反,我们首先需要对象锁lockB,然后需要对象锁lockA. @Override public void run() { synchronized (lockB){ //这里一个log日志 Log.e("CHAO","ThreadB lock lockB"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockA){ //这里一个log日志 Log.e("CHAO","ThreadB lock lockA"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } //这里运行线程 ProductThreadA productThreadA = new ProductThreadA(); ProductThreadB productThreadB = new ProductThreadB(); Thread threadA = new Thread(productThreadA); Thread threadB = new Thread(productThreadB); threadA.start(); threadB.start();
当threadA开始执行run方法的时候,它会先持有对象锁localA,然后睡眠2秒,这时候threadB也开始执行run方法,它持有的是localB对象锁.当threadA运行到第二个同步方法的时候,发现localB的对象锁不能使用(threadB未释放localB锁),threadA就停在这里等待localB锁.随后threadB也执行到第二个同步方法,去访问localA对象锁的时候发现localA还没有被释放(threadA未释放localA锁),threadB也停在这里等待localA锁释放.就这样两个线程都没办法继续执行下去,进入死锁的状态.
ReentrantReadWriteLock是一个解决单线程写和多线程读的理想方法。它采用类似于读写分离的思路设定了读锁和写锁。对于这两个锁的访问保证尽可能大的读并行和写互斥。另外,在一定的条件下写锁可以转换成读锁,而读锁却不能转换成写锁。
ReentrantReadWriteLock却可以有锁的机制保证互斥。它同时也尽可能保证了足够大的并行性。