Java 基础
-
封装、继承、多态
- 封装
- 也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。 简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
- 继承
- 可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。继承概念的实现方式有二类:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力。
- 多态
- 一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
- 封装
-
重载和重写的区别
- 重载
- 1.参数类型、个数、顺序至少有一个不相同
- 2.不能重载只有返回值不同的方法名
- 3.存在于父类和子类、同类中
- 重写
- 1.方法名、参数、返回值相同
- 2.子类方法不能缩小父类方法的访问权限
- 3.子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)
- 3.存在于父类和子类之间
- 4.方法被定义为final不能被重写
- 重载
-
Object 类下有哪些方法
- registerNatives()
- JVM找到你的本地函数,非重点
- clone()
- clone()函数的用途是用来另存一个当前存在的对象。只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常
- getClass()
- final方法,用于获得运行时的类型。该方法返回的是此Object对象的类对象/运行时类对象Class
- equals()
- equals用来比较两个对象的内容是否相等
- hashCode()
- 该方法用来返回其所在对象的物理地址(哈希码值),常会和equals方法同时重写,确保相等的两个对象拥有相等的hashCode
- toString()
- 返回该对象的字符串表示
- wait(…)
- 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法
- notify()
- 唤醒在此对象监视器上等待的单个线程
- notifyAll()
- 唤醒在此对象监视器上等待的所有线程
- finalize()
- 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
- registerNatives()
-
HashCode、equals、==
- ==
- 1.若是基本数据类型比较,是比较值
- 2.若是对象,则比较的是他们在内存中存放的地址
- equals
- 是Object类中的方法,Object类的equals方法用于判断对象的内存地址引用是不是同一个地址。若是 类中覆盖了equals方法,就要根据具体代码来确定,一般覆盖后都是通对象的内容是否相等来判断对象是否相等
- HashCode
- 计算出对象实例的哈希码,在对象进行散列时作为key存入
- equals与hashCode方法关系
- hashCode()是一个本地方法
- equals相等的对象,hashCode也一定相等
- hashCode不等,equals一定也不等
- hashCode相等,equals可能相等,可能不相等:
public static void main(String[] args) { String s1=new String("Aa"); String s2=new String("BB"); System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); System.out.println(s1==s2); System.out.println(s1.equals(s2)); }
- equals与==的关系
- 以Integer b1 = 127为例;在 java 编译时被编译成 Integer b1 = Integer.valueOf(127);
对于-128 到 127 之间的 Integer 值,用的是原生数据类型 int,会在内存里供重用,也就是这之间的 Integer 值进行比较时,只是进行 int 原生数据类型的数值进行比较。
而超出-128〜127 的范围,进行比较时是进行地址及数值比较。
- 以Integer b1 = 127为例;在 java 编译时被编译成 Integer b1 = Integer.valueOf(127);
- ==
-
String、StringBuiler、StringBuffer
String | StringBuffer | StringBuilder |
---|---|---|
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 | StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 | 可变类,速度更快 |
不可变 | 可变 | 可变 |
线程安全 | 线程不安全 | |
多线程操作字符串 | 单线程操作字符串 |
- 自动拆箱与自动装箱(特别是 Integer)
String | StringBuffer |
---|---|
boolean | Boolean |
char | Character |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
void | Void |
-
比如Integer的装箱与拆箱
-
如果要生成一个数值为10的Integer对象,只需要这样就可以了:
Integer i = new Integer(10);
-
而从Java SE5开始就提供了自动装箱的特性,如果生成一个数值为10的Integer对象,只需这样就可以了
Integer i = 10;
-
这个过程中会自动根据数值创建对应的Integer对象,这就是装箱。那么什么是拆箱呢,顾名思义,跟装箱对应,就是自动将包装器类型转换为基本数据类型
Integer i = 10 //装箱 int n = i; //拆箱
-
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
Java 集合
-
HashMap 的所有知识(1.7、1.8、头插、尾插)
-
线程不安全的,如何线程安全的使用HashMap
- Hashtable
- ConcurrentHashMap
- SynchronizedMap
-
基于Map接口
-
允许Key和Value都允许为null
-
非同步
-
不保证顺序和插入时相同
-
不保证顺序时不变
-
HashMap在底层是用数组+链表实现的。其中数组是HashMap的主体,而链表主要是为了解决哈希冲突而存在的。查找时,如果定位到的数组位置不含链表,时间复杂度为O(1);
如果定位到链表,那么需要遍历链表,存在即覆盖,不存在就添加。链表出现的越少,HashMap性能越高 -
为什么要重写hashCode()和equals()方法?
class MyTest { private static class Book{ int id; String name; public Book(int id, String name) { this.id = id; this.name = name; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()){ return false; } Book person = (Book) o; //两个对象是否等值,通过id来确定 return this.id == person.id; } } public static void main(String []args){ HashMap<Book,String> map = new HashMap<>(); Book person = new Book(1,"Java"); //put到hashmap中去 map.put(person,"Java"); //get取出,从逻辑上讲应该能输出“Java”,但是结果是null System.out.println("结果:"+map.get(new Book(1,"JavaScript"))); } }
-
那么又为什么要重写hashCode方法呢?
- 1.每次应用执行期间,在对象做equals比较所用到的信息没有修改之前,hashcode方法的返回值应该是相同的
- 2.两个对象equals方法相同,那么hashCode值必须相同
- 3.两个对象equals方法不同,那么hashCode值不一定不同
-
如果只重写equals方法,不重写hashCode方法,那么显然会违反第2条约定
-
再具体到HashMap中,有如下源码
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
-
其中if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
这句明显可以看出,get的时候会先进行hash的比较,之后才会进行equals判断。上面源码分析过,hash的计算是基于hashCode()方法的。不重写的话,一般情况下hashCode()方法的返回值不同,那么就无法get到正确的key所对应的value。
如果只重写equals,不重写hashCode,那么存入key内容相同的对象时,会存入两个key内容相同的对象,而不是覆盖掉原有的对象。
取出的时候也取不出正确的对象。
因此必须要重写hashCode()和equals()方法。
-
-
ConcurrentHashMap、HashTable、Collections.SynchronizedMap
- ConcurrentHashMap
- 1.底层采用分段的数组+链表实现,线程安全
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
2.Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
3.有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
4.扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
- 1.底层采用分段的数组+链表实现,线程安全
- HashTable
- 1.底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
2.初始size为11,扩容:newsize = olesize*2+1
3.计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
- 1.底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
- Collections.SynchronizedMap
- SynchronizedMap 是一个实现了Map接口的代理类,该类中对Map接口中的方法使用synchronized 同步关键字来保证对Map的操作是线程安全的。
LinkedList、ArrayList
- SynchronizedMap 是一个实现了Map接口的代理类,该类中对Map接口中的方法使用synchronized 同步关键字来保证对Map的操作是线程安全的。
- ConcurrentHashMap
多线程
-
线程池的7大参数、拒绝策略
-
线程池的7大参数
- corePoolSize: 核心线程最大数量,通俗点来讲就是,线程池中常驻线程的最大数量
- workQueue: 用于保存等待执行任务的阻塞队列
- maximunPoolSize: 线程池中运行最大线程数(包括核心线程和非核心线程)
- ThreadFactory: 创建线程的工厂
- RejectedExecutionHandler: 线程池饱和策略
- keeyAliveTime: 空闲线程存活时间
- TimeUnit: 存活时间单位
-
拒绝策略
- AbortPolicy(默认):直接抛出一个异常,默认策略。
- DiscardPolicy:直接丢弃任务。
- DiscardOldestPolicy:抛弃下一个将要被执行的任务(最旧任务)
- CallerRunsPolicy:主线程中执行任务。
-
-
Synchronized(作用于对象、代码块、方法)
- 修饰对象
- 共用一个对象,多线程调用obj2同步方法,因为使用的是一个对象锁,会阻塞。
String str=new String("lock"); //对象放在方法外,调用方法的时候不会新创建一个对象。 public void obj2() { synchronized (str) { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } }
- 代码块
- 修饰代码块,这个this就是指当前对象(类的实例),多个线程调用同一个对象的同步方法会阻塞,调用不同对象的同步方法不会阻塞。
public void obj2() { synchronized (this) { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } }
- 方法
- 修饰在方法上,多个线程调用同一个对象的同步方法会阻塞,调用不同对象的同步方法不会阻塞。
public synchronized void obj3() { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } }
- 类锁和对象锁多线程访问两个方法的时候,线程会不会阻塞?
public static synchronized void obj3() { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public synchronized void obj4() { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } ... Thread-0 : 4 Thread-1 : 4 Thread-0 : 3 Thread-1 : 3 Thread-1 : 2 Thread-0 : 2 Thread-1 : 1 Thread-0 : 1 Thread-1 : 0 Thread-0 : 0
- 不会阻塞。
- 1.要满足方法同步(或者代码块同步)就必须保证多线程访问的是同一个对象(在java内存中的地址是否相同)。
- 2.类锁和对象锁同时存在时,多线程访问时不会阻塞,因为他们不是一个锁。
- 修饰对象
-
Volatile(缓存行、可见性、重排序)
- Volatile是什么?
- 1.它是内存屏障多线程环境下禁止指令重排序
- 2.多线程环境下保证内存可见性
- volatile修饰的变量,每次线程使用的时候都会从主存里面获取最新的值,每次修改的时候也会立即反馈到主内存中,通过JMM这几个连续有序的动作,保证了变量对所有线程的可见性,但是无法保证整体操作的原子性。
- Volatile是什么?
-
AQS(ReentranLock)
- 什么是AQS?
- AQS的全称为(AbstractQueuedSynchronizer),即队列同步器,它是JUC包下面的核心组件,它的主要使用方式是继承,子类通过继承AQS,并实现它的抽象方法来管理同步状态,它分为独占锁和共享锁。很多同步组件都是基于它实现的,比如:
ReentrantLock就是基于AQS的独占锁实现,它表示每次只能有一个线程持有锁。再比如CountDownLatch、Semaphore等是基于AQS的共享锁实现的,它允许多个线程同时获取锁,并发的访问资源。AQS是建立在CAS上的一种FIFO的双向队列,通过维护一个使用volatile修饰的int类型的state表示资源的锁状态
- AQS的全称为(AbstractQueuedSynchronizer),即队列同步器,它是JUC包下面的核心组件,它的主要使用方式是继承,子类通过继承AQS,并实现它的抽象方法来管理同步状态,它分为独占锁和共享锁。很多同步组件都是基于它实现的,比如:
- AQS实现的核心思想?
- 如果被请求的共享资源共享,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒是锁分配的机制,这个机制是AQS内部维护着一个FIFO的队列,即CLH队列。CLH队列是FIFO双端双向队列,实现公平锁。线程通过AQS获取锁失败,就会将线程封装成一个Node节点,插入队列尾;当有线程释放锁时,将队列中最先入队的节点拿出来去抢夺锁,然后这个节点出队
- 什么是AQS?
JVM
-
方法运行时的内存区域
- 内存去分为Heap和NonHeap
- heap区又被分为 老年代,年轻代
- 年轻代有被分为 Eden(Young),S0,S1区
- heap区又被分为 老年代,年轻代
- 堆是生命周期与JVM一致
- 堆是Java虚拟机运行时数据区共享数据区最大的区域
- “几乎”所有的对象和数组都在堆中进行分配
- 堆是JVM GC工作重点区域
- jdk1.8默认使用的是PS,PO(Parallel Scavenge & Parallel Old)垃圾回收器,这套系统关注的是吞吐量
- 堆内存不足时,将抛出OutOfMemoryError
- 内存去分为Heap和NonHeap
-
双亲委派加载机制
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
- 向上委派,向下尝试
- 避免对象重复加载,保证程序的安全性,防止java核心语言环境被破坏
垃圾回收算法与垃圾回收器
-
垃圾回收算法
-
标记清除算法
- 缺点:会产生大量空间碎片
-
标记整理算法
-
复制算法
-
-
年轻代垃圾收集器(JKD8以前)
- Serial ParNew Parallel Scavenge
- 均使用标记复制算法
-
老年代垃圾收集器(JDK8以前)
- CMS Serial Old Parallel Old
- 其中CMS用的是标记清除算法
- Serial Old 和 Parallel Old 和 Serial Old 使用的标记整理算法
-
jdk8以后默认使用的是G1垃圾回收器,不在是分代模型,改用Region化的内存分布Region使用的是标记复制算法
-
垃圾回收器的搭配
计算机网络
操作系统
Redis
- 五大数据结构
- String,Hashtable,LinkedList,Set,Zset
- Redis 持久化
-
RDB
- 将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化
-
AOF
- 将Reids的操作日志以追加的方式写入文件
-
RDB持久化配置
- save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
- save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
- save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
-
AOF持久化配置
- appendfsync always #每次有数据修改发生时都会写入AOF文件。
- appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
- appendfsync no #从不同步。高效但是数据不会被持久化。
-
- 缓存雪崩、缓存穿透、缓存击穿、双写一致性
- 缓存雪崩
- 缓存里面的大部分数据在同一时间失效,而且缓存也把这些失效的缓存数据给删除,本应该访问缓存的请求现在都去访问数据库,导致数据库在短期内CPU和内存压力剧增,严重的话还会发生宕机。
- 解决方案:
- 给每个数据的过期时间加个随机值,这样就可以避免在同一时间大部分数据全都过期。
- 缓存穿透
- 访问一个不在数据库里面的数据,那肯定也不在缓存里面,这样就会越过缓存直接去访问数据库,如果请求很多的话也会拖垮数据库
- 解决方案:
- 方法一是加一个布隆过滤器,来过滤掉那些不合法的数据请求;
方法二是将不合法的数据保存在缓存里,加一个比较短的过期时间
- 方法一是加一个布隆过滤器,来过滤掉那些不合法的数据请求;
- 缓存击穿
- key被高并发访问是正好失效请求落到数据库
- 解决方案:
- 分布式互斥锁
- key永不过期
- 双写一致性
- 同时写缓存和数据库的一致性问题,无论是先写缓存还是先写数据库,都有可能失败,而且在高并发场景下还会有顺序未知的读写操作
- 解决方案:
- 用消息队列保证结果的最终一致性,还需要利用消息队列的失败重试机制确保操作能更新成功
- 缓存雪崩
- 内存淘汰机制
- LRU
- 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key
- LRU