Java基础篇
Java基础知识
1. 面向对象的特征(了解)
面向对象的特征:封装,继承,多态,抽象
封装: 就是把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,就是把不想告诉或不该告诉别人的东西藏起来,把可以告诉别人的东西公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性
继承: 子类继承父类的属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性。
多态: 指允许不同的对象对同一消息做出回应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。
抽象: 表示对领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但本质上相同的具体概念的抽象。在Java中抽象用abstract修饰类时,此类就不能被实例化,从这里就可以看出抽象类(接口)就是为继承而存在的。
JDK JRE JVM的区别(必会)
JDK(Java Development Kit)是整个Java的核心,是Java开发工具包,包括了Java运行环境JRE,Java工具和Java基础类库。
JRE(Java Runtime Environment)是运行Java程序所必须的环境的集合,包含了java虚拟机和java程序的一些核心类库。
JVM是真个Java实现跨平台的最核心的部分,能够运行以java语言写作的软件程序。
重载和重写的区别
重载: 发生在同一个类中,方法名必须相同,参数类型不同,参数个数不同,参数顺序不同。
重写: 发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出异常范围小于等于父类,访问修饰符大于等于小类,如果父类方法访问修饰符为private则子类就不能重写该方法。
Java中== 和equals 的区别
==的作用:
基本类型:比较的就是值是否相同。
引用类型:比较的就是地址值是否相同。
equals的作用:
引用类型:默认情况下,比较的是地址的值。
String Integer Date这些类库中equalse被重写,比较的是内容还是地址。
面试题:请解释字符串比较之中“==”和equals()的区别?
答:==:比较的是两个字符串内存的地址(堆内存)的数值是否相等。属于数值比较。
equals():比较的是两个字符串的内容,属于内容比较。
String、StringBuffer、StringBuilder三者之间的区别
String 字符串常量、StringBuffer 字符串变量(线程安全)、StringBuilder 字符串变量(非线程安全)
String类中使用final关键字修饰字符数组来保存字符串,private final char value[]
,String对象是不可变的,也就可以理解为常量,线程安全的。任何对String修改的其实是在堆空间里面开辟出一个新的String对象。
StringBuffer:对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的,而且是可以修改的。
StringBuilder:并没有对方法加同步锁,所以是非线程安全的,也可以修改的。
总结:
- 如果要操作少量数据使用:String
- 多线程操作字符串缓冲区下操作大量的字符串数据使用:StringBuffer
- 单线程操作字符串缓冲区下操作大量的字符串数据使用:StringBuilder
接口和抽象类的区别是什么?
实现:抽象类的子类必须要用extends来继承,接口必须使用implements来实现接口。
构造函数:抽象类可以有构造函数,接口不能有。
main方法:抽象类可以有main方法,并且能运行它,接口不能有main方法。
实现数量:一个类可以实现多个接口但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用public修饰,抽象类中的方法可以是任意访问修饰符。
什么是单例模式?有几种?
单例模式:某个类的实例在多线程环境下只会被创建一次出来。
单例模式有饿汉式单例模式,懒汉式单例模式,双检锁单例模式三种
饿汉式:线程安全,一开始就初始化。
public class Singleton{
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getSingleton(){
return singleton;
}
}
懒汉式:非线程安全,延迟初始化。
public class Singleton{
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
双检锁:线程安全,延迟初始化。
public class Singleton{
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
反射
在Java中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类的属性和方法;并且对于任意一个对象都能够调用到它的任意一个方法;这种动态获取信息以及动态调用对象的方法能够成为Java语言的反射机制。
获取Class对象的3种方法:
调用某个对象的getClass方法
Person p = new Person();
Class clazz = p.getClass();
调用某个类的class属性来获取该类对应的class对象
Class clazz = Person.class;
使用Class类中的forName()静态方法(最安全、性能最好)
Class clazz = Class.forName(“类的全路径”);最常用
JDK1.8的新特性
Lambda表达式:Lambda允许把函数作为一个方法的参数。new Thread () -> System.out.println(“abc”).start();
方法引用:方法引用允许直接引用已有Java类或对象的的方法或构造方法
函数式接口:有且仅有一个抽象方法的接口叫做函数式接口,函数式接口可以被隐式转换为Lambda表达式。通常函数式接口上面会添加@Functionallnterface注解。
接口允许定义默认方法和静态方法:从JDK8开始,允许接口中存在一个或多个默认非抽象方法或静态方法。
Stream API:新添加的 Stream API把真正的函数式编程风格引入到Java中。这种风格将要处理的元素集合看做一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。
日期、时间类改进:之前JDK自带的日期处理类非常不方便,我们处理的时候经常是使用第三方工具包,比如commons-lang包等。不过JDK8出现之后这个改观了很多,比如日期时间的创建,比较调整,格式化,时间间隔等。这些类都在java.time包下,LocalDate/LocalTime/LocalDateTime。
Optional类:Optional类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
Java Base64实现:Java 8 内置了Base 64 编码的编码器和解码器。
Java的异常
Throwable是所有Java程序中错误处理的父类,有两种资源类:Error和Exception。
Error: 表示由JVM所侦测到的无法预估的错误,由于这是属于JVM层次的严重错误,导致JVM无法继续执行,因此,这是无法捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。
Exception: 表示可恢复的异常,这是可以捕捉的。
- 运行时异常: 都是RuntimeException类及其子类异常,如NullPointerException(空指针异常),IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理,这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这些异常,即便没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
- 非运行时异常: 是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException,SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
- 常见的Runtime异常如下
- NullPointerException:空指针异常
- ClassCastException:类型强制转换异常
- IllegalArgumentException:传递非法参数异常
- AritmeticException:算数运算异常
- ArrayStoreException:向数组中存放与声明类型不兼容异常
- IndexOutOfException:下标越界异常
- NegativeArraySizeException:创建一个大小为负数的数组错误异常
- NumberFormatException:数字格式异常
- SecurityException:安全异常
10.UnsupportedOperationException:不支持的操作异常
BIO 、NIO 、AIO有声明区别
BIO:Block IO 同步阻塞式IO,就是我们平常传统使用的IO,它的特点是模式简单使用方便,并发处理能力低。
NIO:New IO 同步非阻塞IO,是传统IO的升级,客户端和服务端通过Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO是NIO的升级,也叫NIO2,实现了异步非堵塞IO,异步IO的操作基于事件和回调机制。
Threadloal的原理
ThreadLocal:为共享变量在每个线程中创建一个副本,每个线程都可以访问自己内部的副本变量。通过threadlocal保证线程的安全。
其实在ThreadLocal类中有一个静态内部类ThradLocalMap(类似于Map),用键值对的形式存储每一个线程变量的副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本。
ThreadLocal本身并不存储值,它只是作为一个key保存到ThreadLocalMap中,但是这里要注意的是它作为一个key用的是弱引用,因为没有强引用链,弱引用在GC的时候有可能会被回收。这样就会在ThreadLocalMap中存在一些key为null的键值对(Entry)。因为key变成null了,我们是没法访问这些Entry的,但是这些Entry本身是不会被清除的。如果没有手动删除对应的key 就会导致这块内存既不会回收也无法访问,也就是内存泄露。
使用完TreadLocal之后,记得调用remove方法,在不使用线程池的前提下,即使不调用remove方法,线程池的“变量副本”也会被gc回收,即不会造成内存泄露的情况。
同步锁、死锁、乐观锁、悲观锁
同步锁: 当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行多个线程,在同一时间内只允许一个线程访问共享数据。Java中可以使用synchronized关键字来取得一个对象的同步锁。
死锁: 何为死锁,就是多个线程同时被阻塞,他们中的一个或者全部都在等待资源被释放。
乐观锁: 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_conditio机制,其实都是提供乐观锁。在Java中java.util.concurrent.actomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其他线程阻塞,用完之后再把资源转让给其他线程)。传统关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在操作之前先上锁。Java中synchronized和ReentranLock等独占锁就是悲观锁思想的实现。
说一下 synchronized 底层实现原理?
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同事它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础。
- 普通同步方法:锁是当前实例对象
- 静态同步方法:锁是当前类的class对象
- 同步方法块:锁是括号里的对象
synchronized 和 volatile 的区别是什么?
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当先线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别的。
volatile仅能实现变量的修改可见性,不能保证原子性;而xynchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
synchronized 和 Lock 有什么区别?
首先synchroniezd是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
手写冒泡排序?(必会)
集合(必会)
常见的数据结构(了解)
常用的数据结构有:数组,栈,队列,链表,树,散列,堆,图等
数组是最常用的数据结构,数组的特点是长度固定,数组的大小固定后就无法扩容了 ,数组只能存储一种类型的数据 ,添加,删除的操作慢,因为要移动其他的元素。
栈是一种基于先进后出(FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先被读取出来。
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结节(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。根据指针的指向,链表能形成不同的结构,例如单链表,双向链表,循环链表等。
树是我们计算机中非常重要的一种数据结构,同时使用树这种数据结构,可以描述现实生活中的很多事物,例如家谱、单位的组织架构等等。有二叉树、平衡树、红黑树、B树、B+树。
散列表,也叫哈希表,是根据关键码和值 (key和value) 直接进行访问的数据结构,通过key和value来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。
堆是计算机学科中一类特殊的数据结构的统称,堆通常可以被看作是一棵完全二叉树的数组对象。
图的定义:图是由一组顶点和一组能够将两个顶点相连的边组成的
集合和数组的区别(了解)
区别: 数组长度固定 集合长度可变
数组 中存储的是同一种数据类型的元素,可以存储基本数据类型,也可以存储引用数据类型;
集合 存储的都是对象,而且对象的数据类型可以不一致。在开发当中一般当对象较多的时候,使用集合来存储对象。
List 和 Map、Set 的区别(必会)
List 和Set是存储单列数据的集合,Map是存储键值对这样的双列数据的集合;
List 中存储的数据是有顺序的,并且值允许重复;
Map 中存储的数据是无序的,它的键是不允许重复的,但是值是允许重复的;
Set 中存储的数据是无顺序的,并且不允许重复,但元素在集合中的位置是由元素的hashcode决定,即位置是固定的(Set集合是根据hashcode来进行数据存储的,所以位置是固定的,但是这个位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的)。
List 和 Map、Set 的实现类(必会)
(1)Connection接口:
List 有序,可重复
ArrayList
优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程不安全,效率高
Vector
优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程安全,效率低, 已给舍弃了
LinkedList
优点: 底层数据结构是链表,查询慢,增删快。
缺点: 线程不安全,效率高
Set 无序,唯一
HashSet
底层数据结构是哈希表。(无序,唯一)
如何来保证元素唯一性?
依赖两个方法:hashCode()和equals()
LinkedHashSet
底层数据结构是链表和哈希表。(FIFO插入有序,唯一)
1.由链表保证元素有序
2.由哈希表保证元素唯一
TreeSet
底层数据结构是红黑树。(唯一,有序)
1. 如何保证元素排序的呢?
自然排序
比较器排序
2.如何保证元素唯一性的呢?
根据比较的返回值是否是0来决定
(2)Map接口有四个实现类:
HashMap
基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键, 线程不安全。
HashTable
线程安全,低效,不支持 null 值和 null 键;
LinkedHashMap
线程不安全,是 HashMap 的一个子类,保存了记录的插入顺序;
TreeMap
能够把它保存的记录根据键排序,默认是键值的升序排序,线程不安全。
Hashmap的底层原理(高薪常问)
HashMap在JDK1.8之前的实现方式 数组+链表,
但是在JDK1.8后对HashMap进行了底层优化,改为了由 数组+链表或者数值+红黑树实现,主要的目的是提高查找效率
- Jdk8数组+链表或者数组+红黑树实现,当链表中的元素超过了 8 个以后, 会将链表转换为红黑树,当红黑树节点 小于 等于6 时又会退化为链表。
- 当new HashMap():底层没有创建数组,首次调用put()方法示时,底层创建长度为16的数组,jdk8底层的数组是:Node[],而非Entry[],用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍,专业术语叫做扩容,在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能.
默认的负载因子大小为0.75,数组大小为16。也就是说,默认情况下,那么当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍。 - 在我们Java中任何对象都有hashcode,hash算法就是通过hashcode与自己进行向右位移16的异或运算。这样做是为了计算出来的hash值足够随机,足够分散,还有产生的数组下标足够随机,
map.put(k,v)实现原理
(1)首先将k,v封装到Node对象当中(节点)。
(2)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(3)下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
map.get(k)实现原理
(1)、先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
(2)、在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。 - Hash冲突
不同的对象算出来的数组下标是相同的这样就会产生hash冲突,当单线链表达到一定长度后效率会非常低。 - 在链表长度大于8的时候,将链表就会变成红黑树,提高查询的效率。
Hashmap和hashtable ConcurrentHashMap区别(高薪常问)
区别对比一(HashMap 和 HashTable 区别):
1、HashMap 是非线程安全的,HashTable 是线程安全的。
2、HashMap 的键和值都允许有 null 值存在,而 HashTable 则不行。
3、因为线程安全的问题,HashMap 效率比 HashTable 的要高。
4、Hashtable 是同步的,而 HashMap 不是。因此,HashMap 更适合于单线
程环境,而 Hashtable 适合于多线程环境。一般现在不建议用 HashTable, ①
是 HashTable 是遗留类,内部实现很多没优化和冗余。②即使在多线程环境下,
现在也有同步的 ConcurrentHashMap 替代,没有必要因为是多线程而用
HashTable。
区别对比二(HashTable 和 ConcurrentHashMap 区别):
HashTable 使用的是 Synchronized 关键字修饰,ConcurrentHashMap 是JDK1.7使用了锁分段技术来保证线程安全的。JDK1.8ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
多线程(必会)
什么是线程?线程和进程的区别?(了解)
**线程:**是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的 可以独立运行的基本单位。
**进程:**具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
**特点:**线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行 时各自内存单元相互独立,线程之间 内存共享,这使多线程编程可以拥有更好 的性能和用户体验。
创建线程有几种方式(必会)
1 .继承Thread类并重写 run 方法创建线程,实现简单但不可以继承其他类
2. 实现Runnable接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。
3. 实现 Callable接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常。
4. 使用线程池创建(使用java.util.concurrent.Executor接口)
Runnable和Callable的区别?(必会)
主要区别
Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,支持泛型
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
如何启动一个新线程、调用start和run方法的区别?(必会)
线程对象调用run方法不开启线程。仅是对象调用方法。
线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行
调用start方法可以启动线程,并且使得线程进入就绪状态,而run方法只是thread的一个普通方法,还是在主线程中执行。
线程有哪几种状态以及各种状态之间的转换?(必会)
- 第一是new->新建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
- 第二是Runnable->就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。
- 第三是Running->运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
- 第四是阻塞状态。阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(1)等待 – 通过调用线程的wait() 方法,让线程等待某工作的完成。
(2)超时等待 – 通过调用线程的sleep() 或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(3)同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。 - 第五是dead->死亡状态: 线程执行完了或者因异常退出了run()方法,该线程结束生命周期.
线程相关的基本方法?(必会)
线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等
1.线程等待(wait)
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中
断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方
法一般用在同步方法或同步代码块中。
2.线程睡眠(sleep)
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占
有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法
会导致当前线程进入 WATING 状态.
3.线程让步(yield)
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争
CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU
时间片,但这又不是绝对的,有的操作系统对 线程优先级并不敏感。
4.线程中断(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的
一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)
5.Join 等待其他线程终止
join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方
法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变
为就绪状态,等待 cpu 的宠幸.
6.线程唤醒(notify)
Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如
果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并 在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视 器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程, 被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类 似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
wait()和sleep()的区别?(必会)
1. 来自不同的类
wait():来自Object类;
sleep():来自Thread类;
2.关于锁的释放:
wait():在等待的过程中会释放锁;
sleep():在等待的过程中不会释放锁
3.使用的范围:
wait():必须在同步代码块中使用;
sleep():可以在任何地方使用;
4.是否需要捕获异常
wait():不需要捕获异常;
sleep():需要捕获异常;
线程池
为什么需要线程池(了解)
在实际使用中,线程是很占用系统资源的,如果对线程管理不完善的话很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处:
1、使用线程池可以重复利用已有的线程继续执行任务,避免线程在创建销毁时造成的消耗
2、由于没有线程创建和销毁时的消耗,可以提高系统响应速度
3、通过线程可以对线程进行合理的管理,根据系统的承受能力调整可运行线程数量的大小等
线程池的分类(高薪常问)
- newCachedThreadPool: 创建一个可进行缓存重复利用的线程池
- newFixedThreadPool: 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,线程池中的线程处于一定的量,可以很好的控制线程的并发量
- newSingleThreadExecutor: 创建一个使用单个 worker 线程的Executor ,以无界队列方式来运行该线程。线程池中最多执行一个线程,之后提交的线程将会排在队列中以此执行
- newSingleThreadScheduledExecutor: 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期执行
- newScheduledThreadPool: 创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行
- newWorkStealingPool: 创建一个带并行级别的线程池,并行级别决定了同一时刻最多有多少个线程在执行,如不传并行级别参数,将默认为当前系统的CPU个数
核心参数(高薪常问)
corePoolSize:核心线程池的大小
maximumPoolSize:线程池能创建线程的最大个数
keepAliveTime:空闲线程存活时间
unit:时间单位,为keepAliveTime指定时间单位
workQueue:阻塞队列,用于保存任务的阻塞队列
threadFactory:创建线程的工程类
handler:饱和策略(拒绝策略)
线程池的原理(高薪常问)
线程池的工作过程如下:
当一个任务提交至线程池之后,
- 线程池首先判断核心线程池里的线程是否已经满了。如果不是,则创建一个新的工作线程来执行任务。否则进入2.
- 判断工作队列是否已经满了,倘若还没有满,将线程放入工作队列。否则进入3.
- 判断线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行。如果线程池满了,则交给饱和策略来处理任务。
拒绝策略(了解)
ThreadPoolExecutor.AbortPolicy(系统默认): 丢弃任务并抛出RejectedExecutionException异常,让你感知到任务被拒绝了,我们可以根据业务逻辑选择重试或者放弃提交等策略
ThreadPoolExecutor.DiscardPolicy: 也是丢弃任务,但是不抛出异常,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程),通常是存活时间最长的任务,它也存在一定的数据丢失风险ThreadPoolExecutor.CallerRunsPolicy:既不抛弃任务也不抛出异常,而是将某些任务回退到调用者,让调用者去执行它。
线程池的关闭(了解)
关闭线程池,可以通过shutdown和shutdownNow两个方法
原理:遍历线程池中的所有线程,然后依次中断
1、shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;
2、shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程
6. Jvm
虚拟机,一种能够运行java字节码的虚拟机。
JDK1.8 JVM运行时内存(高薪)
程序计数器:
线程私有的(每个线程都有一个自己的程序计数器), 是一个指针. 代码运行, 执行命令. 而每个命令都是有行号的,会使用程序计数器来记录命令执行到多少行了.记录代码执行的位置
Java虚拟机栈:
线程私有的(每个线程都有一个自己的Java虚拟机栈). 一个方法运行, 就会给这个方法创建一个栈帧, 栈帧入栈执行代码, 执行完毕之后出栈(弹栈)存引用变量,基本数据类型
本地方法栈:
线程私有的(每个线程都有一个自己的本地方法栈), 和Java虚拟机栈类似, Java虚拟机栈加载的是普通方法,本地方法加载的是native修饰的方法.
native:在java中有用native修饰的,表示这个方法不是java原生的.
堆:
线程共享的(所有的线程共享一份). 存放对象的,new的对象都存储在这个区域.还有就是常量池.
元空间: 存储.class 信息, 类的信息,方法的定义,静态变量等.而常量池放到堆里存储
JDK1.8和JDK1.7的jvm内存最大的区别是, 在1.8中方法区是由元空间(元数据区)来实现的, 常量池.
1.8不存在方法区,将方法区的实现给去掉了.而是在本地内存中,新加入元数据区(元空间).
JDK1.8堆内存结构(高薪常问)
Young 年轻区(代): Eden+S0+S1, S0和S1大小相等, 新创建的对象都在年轻代】Tenured 年老区: 经过年轻代多次垃圾回收存活下来的对象存在年老代中.
Jdk1.7和Jdk1.8的区别在于, 1.8将永久代中的对象放到了元数据区, 不存永久代这一区域了.
Gc垃圾回收(高薪常问)
JVM的垃圾回收动作可以大致分为两大步,首先是「如何发现垃圾」,然后是「如何回收垃圾」。说明一点, 线程私有的不存在垃圾回收, 只有线程共享的才会存在垃圾回收, 所以堆中存在垃圾回收.
6.3.1 如何发现垃圾
Java语言规范并没有明确的说明JVM使用哪种垃圾回收算法,但是常见的用于「发现垃圾」的算法有两种,引用计数算法和根搜索算法。
1.引用计数算法
该算法很古老(了解即可)。核心思想是,堆中的对象每被引用一次,则计数器加1,每减少一个引用就减1,当对象的引用计数器为0时可以被当作垃圾收集。
优点:快。
缺点:无法检测出循环引用。如两个对象互相引用时,他们的引用计数永远不可能为0。
2.根搜索算法(也叫可达性分析)
根搜索算法是把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即可以当作垃圾。
Java中可作为GC Root的对象有
1.虚拟机栈中引用的对象
2.本地方法栈引用的对象
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
如何回收垃圾
Java中用于「回收垃圾」的常见算法有4种:
- 标记-清除算法(mark and sweep)
分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。
缺点:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片。 - 标记-整理算法
是在标记-清除算法基础上做了改进,标记阶段是相同的,但标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。
优点:内存被整理后不会产生大量不连续内存碎片。 - 复制算法(copying)
将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。
缺点:可使用的内存只有原来一半。 - 分代收集算法(generation)
当前主流JVM都采用分代收集(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为年轻代、年老代、永久代,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
(1) 年轻代(Young Generation)
1.所有新生成的对象首先都是放在年轻代的。
2.新生代内存按照8:1:1的比例分为一个eden区和两个Survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
3.当survivor1区不足以存放eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)
(2) 年老代(Old Generation)
1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2.内存比新生代也大很多(大概是2倍),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率比较高。
(3) 持久代(Permanent Generation)
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,从JDK8以后已经废弃, 将存放静态文件,如Java类、方法等这些存储到了元数据区.
JVM调优参数(了解)
这里只给出一些常见的性能调优的参数及其代表的含义。(大家记住5.6个就行, 并不需要都记住.)
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM初始内存为3550m。注意:此值一般设置成和-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss256k:设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M,以前每个线程栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4。(该值默认为2)
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4。