forked from klovien/klovien.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
Java常见问题
Yebin Yu edited this page Jul 29, 2023
·
1 revision
使用JVM(Java Virtual Machine)。
.java 编译后生产.class文件,也就是字节码文件。jvm负责将字节码文件翻译成不同平台下的机器码然后运行。也就是说,在不同的平台上安装相应的jvm,就可以运行字节码文件,执行我们写的程序。
garbage collection 垃圾收集
内存处理是开发人员容易出现问题的地方,忘记或者错误地内存回收会导致程序或者系统的不稳定甚至崩溃,Java提供的垃圾回收机制可以自动检测对象是否超过作用域从而达到自动回收的目的。
在Java开发中,程序员并不需要显式去释放一个对象的内存的,而是由虚拟机自动进行管理。在JVM中,有一个低优先级的垃圾回收线程,在正常情况下这个线程是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的对象集合中,然后进行回收操作。
判断一个对象是否存活有两种方法:引用计数法和可达性分析法
引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加1,引用失效时,计数器就减1。当一个对象的引用计数器为0时,说明此对象没有被其他对象引用,也就是死对象,将会被GC回收。
缺陷:无法解决循环引用问题,也就是说对象A引用对象B,对象B反过来引用对象A,那么此时A、B对象的引用计数器都不为0,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
可达性分析法
从一个被称为GC Roots的对象开始往下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
可以作为GC Roots的对象有以下几种:
虚拟机栈中引用的对象
方法区类静态属性引用的对象
方法区常量池引用的对象
方法栈JNI引用的对象
当一个对象不可达GC Roots时,这个对象并不会立马被回收,而是处于一个死缓的阶段,如果要真正的回收需要经历两次标记。如果对象在可达性分析中没有与GC Roots的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已被虚拟机调用过,那么就认为是没有必要的。如果该对象有必要执行finalize()方法,那么这个对象就会放在一个称为F-Queue的队列中,虚拟机会触发一个Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize()方法执行缓慢或者发生了死锁,那么就是造成F-Queue队列一直等待,造成了内存回收系统的崩溃。GC对处于 F-Queue队列中的对象进行第二次标记,这时,该对象将会被移出“即将回收”集合,等待回收。
https://www.huaweicloud.com/articles/e3cdb3ee7872cf3d23278b8e74acfd99.html
1、多线程之间可见性
2、禁止指令重排序
https://www.cnblogs.com/snowwhite/p/9532311.html
在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间。
这几个态都是和java 堆的回收有关。
几乎每个java对象都出生在年轻态,这里的出生和死亡频率非常高。对年轻态的垃圾回收被称作初级回收(minor gc)。每经过一个minor gc,存活的对象年龄都++,当年龄满足某个值时(默认时15,可以通过参数 -XX:MaxTenuringThreshold 来设定),这些对象就熬成了老年态。
老年态中的出生和死亡频率都不高,大家都是熬过来的老油条了。Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。full gc 频率比minor gc 低,每次时间更长。标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
就是JVM调优吧。我回答了硬件条件不佳、引用链较长、被回收内存比较零碎不连续又多。后来想了一下,应该还有一些因素,比如用的垃圾回收器不好、代码中可能存在软引用弱引用虚引用等。
新生代的垃圾回收使用的复制算法,通过可达性分析算法来标记存活对象,这里是否可能是新生代有大量的存活对象,需要通过复制到to幸存者区,数量多的情况复制效率会慢?还有是否有可能存在大量的局部变量,导致新生代需要频繁GC?(仅讨论,我也不清楚这题考的是什么)
https://www.huaweicloud.com/zhishi/arc-12588701.html
JVM内存模型构成:
JVM的内存模型主要包括:堆内存、方法区(包括运行时常量池)、栈内存(包括虚拟机栈和本地方法栈)、程序计数器。
从共享范围上来划分可分为两类:线程共享区域(堆和方法区);线程私有区域(虚拟机栈、本地方法栈和程序计数器)。
从堆概念来划分可分为两类:堆内存和非堆内存。
个人感觉是增加一种约定,就像注释一样,表明这里这个值该是什么样的。或者用来表示一些很明显的错误。
assert还可以作为调试工具。
检查参数有效性
检查数组越界
switch/if控制流中的can't reach here放一个
不要free空指针
java中都是值传递。
对象传的是引用的值,传对象前,有一个对象的引用。传过去后,会有一个引用的副本,但本质上对象只有一块内存空间。
验证了一下,传数组时,也是内存空间的传递,会有影响
元素重复与否是使用 equals()方法进行判断的。
基础类型只能用==。
对于对象来说,==是表示内存地址,但是equals是一个可以重写的函数,可以自己写逻辑来返回true或者false。
如果没有字节重写equals,那就是调用Object类的equals。Object中的equals返回的就是==的结果。
Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
Map
├Hashtable
├HashMap
└WeakHashMap
https://zhuanlan.zhihu.com/p/28241176:
1、Vector是线程安全的,ArrayList不是线程安全的。在关键性的函数上,vector比ArrayList多了synchronized的关键词。
2、ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍
区别:
1. Vector使用同步方法实现,synchronizedList使用同步代码块实现。
2. SynchronizedList只是使用同步代码块包裹了ArrayList的方法。
add的时候,synchronizedList还是调用ArrayList的add。
所以Collections.synchronizedList 和 Vector在扩容上面的区别就是ArrayList和Vector的区别。
3. SynchronizedList其中有listIterator和listIterator(int index)并没有做同步处理。但是Vector却对该方法加了方法锁。
所以说,在使用SynchronizedList进行遍历的时候要手动加锁。
4. SynchronizedList有更好的扩展和兼容功能,可以将所有的List子类都转换成线程安全的类。
PS.同步代码块和同步函数的区别:
1. 同步代码块在锁定的范围上可能比同步方法要小,一般来说锁的范围大小和性能是成反比的。
2. 同步块可以更加精确的控制锁的作用域(锁的作用域就是从锁被获取到其被释放的时间),同步方法的锁的作用域就是整个方法。
3. 静态代码块可以选择对哪个对象加锁,但是静态方法只能给this对象加锁。所以SynchronizedList可以指定锁定的对象。
1. HashMap不是线程安全的,而HashTable是线程安全的
2. HashMap允许将null作为一个entry的key或者value(但不可以都为空),而Hashtable不允许。
https://segmentfault.com/a/1190000023843490
因为hashmap内部查找过程分为两步:
1. 通过hashcode找到对应的bucket
2. 遍历bucket,用equals做比较找到key
value的类也要重写吗?
要。因为hashmap内部会找entry,entry的hashcode是key和value的hashcode一起计算出来的
https://blog.csdn.net/qq_41345281/article/details/89218522
java.util.Collections 是一个包装类。它包含有各种有关集合操作的 静态多态方法。此类 不能实例化,就像一 个工具类,服务于Java的Collection框架。
ArrayList实现了List接口,底层使用的是动态数组,之所以称为动态数组,是因为Arraylist在数组元素超过其容量大,Arraylist可以进行扩容(针对JDK1.8 数组扩容后的容量是扩容前的1.5倍);
LinkedList底层使用的是链表
动态数组查询特定元素比较快,插入和删除不如链表快
如果一直在尾部添加元素的话,ArrayList的效率会更高一些,LinkedList每次增加会new一个Node对象来保存新增加的元素,当数据量比较小的时候时间不明显,数据量很大的时候new一个Node的时间会大于动态数组扩容的时间,这样ArrayList的效率就高了
整型:
byte:1个字节 8位 -128~127
short :2个字节 16位
int:4个字节 32位
long:8个字节 64位
浮点型:
float:4个字节 32 位
double :8个字节 64位
注:默认的是double类型,如3.14是double类型的,加后缀F(3.14F)则为float类型的。
2个
1. string池中找不到xyz,string池中创建对象
2. 有new 在内存新建对象
不正确
3.4默认是double,而double精度比float高,无法自动转型。
可以 char存放的是unicode 而中文也是unicode 除了unicode中没有的特殊字符都可以。char是两字节
2 << 3
Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。
四舍五入的原理是在参数上加0.5 然后进行下取整。
short s1 = 1; s1 = s1 + 1;有什么错?
s1是short型,1是short型,通过+运算符,计算的时候s1转换为int型,最后把s1+1赋值给s1的时候,s1是short型,所以出错。
short s1 = 1; s1 += 1;有什么错?
x+=i表达式使用的是复合赋值操作符。原因和上面一样。一般byte、char、short不要用复合赋值操作。
数组没有length()方法,有length属性。
String有length方法。
不可以,String有final修饰符
作用域 | public | protected | default | private |
---|---|---|---|---|
当前类 | √ | √ | √ | √ |
同一package | √ | √ | √ | × |
子类 | √ | √ | × | × |
其他包 | √ | × | × | × |
overload:重载
override:重写
overload可以改变返回值的类型
Abstract class | Interface | |
---|---|---|
实例化 | 不能 | 不能 |
类 | 一种继承关系,一个类只能使用一次继承关系。可以通过继承多个接口实现多重继承 | 一个类可以实现多个interface |
数据成员 | 可有自己的 | 静态的不能被修改即必须是static final,一般不在此定义 |
方法 | 可以私有的,非abstract方法,必须实现 | 不可有私有的,默认是public,abstract 类型 |
变量 | 可有私有的,默认是friendly 型,其值可以在子类中重新定义,也可以重新赋值 | 不可有私有的,默认是public static final 型,且必须给其初值,实现类中不能重新定义,不能改变其值。 |
设计理念 | 表示的是“is-a”关系 | 表示的是“like-a”关系 |
实现 | 需要继承,要用extends | 要用implements |
对,因为equals可以被override
@Override
public boolean equals(Object obj) {
return true;
}
abstract:用来声明抽象方法,抽象方法没有方法体,不能被直接调用,必须在子类overriding后才能使用
static:用来声明静态方法,静态方法可以被类及其对象调用
native:用来声明本地方法,该方法的实现由非java 语言实现,比如C。一般用于java与外环境交互,或与操作系统交互
synchronized:用于防止多个线程同时调用一个对象的该方法,与static连用可防止多个线程同时调用一个类的该方法
1、abstract与static
(how)static与abstract不能同时使用
(why)用static声明方法表明这个方法在不生成类的实例时可直接被类调用,而abstract方法不能被调用,两者矛盾。
2、abstract与native
(how)native 可以与所有其它的java 标识符连用,但是abstract除外。
(why)因为native 暗示这些方法是有实现体的,只不过这些实现体是非java 的,但是abstract却显然的指明这些方法无实现体。
3、abstract与synchronized
(how)abstract与synchronized不能同时使用
(why)从synchronized的功能也可以看出,用synchronized的前提是该方法可以被直接调用,显然和abstract连用
接口可以继承接口。
抽象类可以实现(implements)接口。
抽象类是否可继承实体类,但前提是实体类必须有明确的构造函数。
构造器Constructor不能被继承,因此不能重写Overriding,但可以被重载Overloading。
[toc]
https://www.liaoxuefeng.com/wiki/1252599548343744/1255943750561472
1. wait只能在同步(synchronize)环境中被调用,而sleep不需要。
2. 进入wait状态的线程能够被notify和notifyAll线程唤醒,但是进入sleeping状态的线程不能被notify方法唤醒。
3. wait通常有条件地执行,线程会一直处于wait状态,直到某个条件变为真。但是sleep仅仅让你的线程进入睡眠状态。
4. wait方法在进入wait状态的时候会释放对象的锁,但是sleep方法不会。
5. wait方法是针对一个被同步代码块加锁的对象,而sleep是针对一个线程。
PS. Wait sleep yield 区别
Java中wait、sleep的区别或者Java中sleep、yield的区别是Java面试或者多线程面试中最常问的问题之一。在这3个在Java中能够用来暂停线程的方法中,sleep()和yield()方法是定义在Thread类中,而wait()方法是定义在Object类中的, 这也是面试中常问的一个问题。
wait()和sleep()的关键的区别在于,wait()是用于线程间通信的,而sleep()是用于短时间暂停当前线程。更加明显的一个区别在于,当一个线程调用wait()方法的时候,会释放它锁持有的对象的管程和锁,但是调用sleep()方法的时候,不会释放他所持有的管程。
回到yield()方法上来,与wait()和sleep()方法有一些区别,它仅仅释放线程所占有的CPU资源,从而让其他线程有机会运行,但是并不能保证某个特定的线程能够获得CPU资源。谁能获得CPU完全取决于调度器,在有些情况下调用yield方法的线程甚至会再次得到CPU资源。所以,依赖于yield方法是不可靠的,它只能尽力而为。
其实就是实现Runable然后传入Thread
本质上只有一个方法:实现Runable
一、继承Thread类,重写方法run()
Thread自己实现了Runable
二、实现Runnable接口,实现方法run()
然后new Thread(Runable xx)
三、实现Callable,通过FutureTask包装器来创建Thread
FutureTask 实现了 RunableFuture
RunableFuture 实现了 Runable
1. synchronized
2. wait与notify
PS.
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
启动线程肯定要用start()方法。当用start()开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。当cpu分配给它时间时,才开始执行run()方法(如果有的话)。start()是方法,它调用run()方法.而run()方法是你必须重写的. run()方法中包含的是线程的主体。
可以进入非synchronized方法,不可以进入synchronized方法
可以给每个订单加个volatile int state的状态,然后通过CAS方法进行修改state状态,谁成功修改state值,那么谁获取到该订单。
对分布式服务器:用redis或者zk实现分布式锁,200并发不大,zk完全hold住
区别:
根本区别:进程是操作系统资源调度的基本单位,线程是任务的调度执行的基本单位
开销方面:进程都有自己的独立数据空间,程序之间的切换开销大;线程也有自己的运行栈和程序计数器,线程间的切换开销较小。
共享空间:进程拥有各自独立的地址空间、资源,所以共享复杂,需要用IPC(Inter-Process Communication,进程间通信),但是同步简单。而线程共享所属进程的资源,因此共享简单,但是同步复杂,需要用加锁等措施。
联系:
线程是进程中的一部分,一个进程可以有多个线程,但线程只能存在于一个进程中。
线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
(1)互斥条件:一个资源每次只能被一个进程使用。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
1、newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。
2、newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
3、newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
4、newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
5、newSingleThreadScheduledExecutor
创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行并且可定时或者延迟执行线程活动。
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
https://www.cnblogs.com/wade-luffy/p/5969418.html
Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。