什么是方法重写
在Java和其他一些高级面向对象的编程语言中,子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。方法重写又称方法覆盖。
方法重写和方法重载的区别:
重载(overload):发生在同一个类中 , 方法名相同,参数列表不同,不考虑返回值。同名的方法如果有不同的参数列表(类型不同、个数 不同、顺序不同)则视为重载。
重写(overrides):发生在父子类中,子类继承父类的方法,方法名相同,参数也相同,但是实现方法体不同。
重写的注意事项
- 必须保证父子类中重写的方法,方法名相同,参数列表相同。
- 子类方法的返回值必须小于或等于父类方法的返回值范围。
- 子类方法的权限必须要大于等于父类方法的权限(不包括 private)
- 子类重写方法异常声明必须是父类的异常或子级异常
- 私有的不能被继承
可变性
String 类中使用 final 关键字修饰字符数组来保存字符串,所以 String 对象是不可变的。 而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是 使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰, 所以这两种对象都是可变的。
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是 线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程 安全的。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象, 然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引 用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获 得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险
对于基本类型和引用类型 == 的作用效果是不同的,基本类型:比较的是值 是否相同; 引用类型:比较的是引用是否相同;
equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较
修饰实例方法
作用于当前对象实例加锁,进入同步代码前要获得当前对象 实例的锁
修饰静态方法
作用于当前类对象加锁,进入同步代码前要获得当前类对象 的锁
修饰代码块
指定加锁对象,对给定对象加锁,进入同步代码库前要获得给 定对象的锁。
双重校验锁实现对象单例(线程安全)
使用场景
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
实现:
抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
构造函数:
抽象类可以有构造函数;接口不能有。
实现数量:
类可以实现很多个接口;但只能继承一个抽象类【java只支持单 继承】。
访问修饰符:
接口中的方法默认使用 public 修饰;抽象类中的抽象方法 可以使用Public和Protected修饰,如果抽象方法修饰符为Private,则 报错:The abstract method 方法名 in type Test can only set a visibility modifier, one of public or protected。 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定
设计层面:
抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象, 是一种行为的规范。
Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口 方法,所有集合都是它的 子类,比如 List、Set 等。
Collections 是一个包装类,包含了很多静态方法,不能被实例化, 就像一个 工具类,比如提供的排序方法: Collections. sort(list)
存储:
HashMap 运行 key 和 value 为 null,而 Hashtable 不允许。
线程安全:
Hashtable 是线程安全的,而 HashMap 是非线程安全的。
推荐使用:
在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使 用 , 推 荐 在 单 线 程 环 境下 使 用 HashMap 替 代 , 如 果 需 要 多 线 程使 用 则 用 ConcurrentHashMap 替代。
HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储,get(key)来获取。 当传入 key 时,HashMap 会根据 key. hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出的 hash 值相同时,我们称之为 hash 冲突,HashMap 的 做法是用链表和红 黑树存储相同 hash 值的 value。当 hash 冲突的个数比较少时,使用链 表否则使用红黑树。
HashSet 是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相 关方法来完成, HashSet 不允许重复的值。
ArrayList:
基于动态数组,连续内存存储,适合下标访问(随机访问),扩 容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组 的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复 制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至 超过linkedList (需要创建大量的node对象)
LinkedList:
基于链表,可以存储在分散的内存中,适合做数据插入及删除 操作,不适合查询:需要逐一遍历,遍历LinkedList必须使用iterator, 不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需 要对list重新进行遍历,性能消耗极大。
jdk7:
数据结构:
ReentrantLock+Segment+HashEntry,一个Segment中包 含了一个HashEntry数组,每个HashEntry又是一个链表结构。
元素查询:
二次hash,第一次hash定位到Segment,第二次hash定位元 素所在链表的头部
锁:
Segment分段锁,Segment继承了ReentrantLock,锁定操作的 Segment,其他Segment不受影响,并发度为Segment的个数,可以通过 构造函数指定,数组扩容不影响其他Segment get方法无须加锁, volatile保证
jdk8:
数据结构:
synchronized+CAS+Node+红黑树,Node的val和next都用 volatile修饰,保证可见性
查找,替换,赋值操作都使用CAS
锁:
锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更 高,扩容时,阻塞所有读写操作,并发扩容
读操作无锁:Node的val和next使用volatile修饰,读写线程对该变量 互相可见
数组使用volatile修饰,保证扩容时被读线程感知。
Vector、Hashtable、Stack 都是线程安全的,而像 HashMap 则是非 线程安全 的,不过在JDK1.5之后随着 Java.util.concurrent 并发包 的出现,它们也有 了 自 己 对 应 的 线 程 安 全 类 , 比 如 HashMap 对 应 的 线 程 安 全 类 就 是 ConcurrentHashMap.
创建线程有三种方式:
继承 Thread 重写 run 方法;
实现 Runnable 接口;
实现 Callable 接口。
runnable 没有返回值,callable 可以拿到有返回值,callable 可以 看作是 runnable 的补充。
当线程对象对创建后,即进入了新建状态(NEW);
当调用线程对象的start()方法(t.start();)线程即进入就绪状态(Runnable);
CPU获取到时间片,进入到运行状态(Running);
当线程调用 wait()或者sleep()时,进入阻塞状态(Blocked),
当休眠时间结束后,或者调用notify或 notifyAll时会重新进入就绪状态(Runnable),再重新获取时间片,进入运行状态
线程执行完了或 者因异常退出了run()方法,该线程结束生命周期,进入终结状态(Dead)
类的不同:
sleep() 来自 Thread,wait() 来自 Object。
释放锁:
sleep() 不释放锁;wait() 释放锁。
用 法 不 同 :
sleep() 时 间 到 会 自 动 恢 复 ; wait() 可 以 使 用 notify()/notifyAll()直接唤醒。
notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后, 会将全部线 程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则 留在锁池等待锁被释放后 再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个 线程由虚拟机控制。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复 调用,而 start() 只能调用一次。
FixedThreadPool:(固定线程池)
可重用固定线程数的线程池。(适用于负载比较重的服务器)
SingleThreadExecutor:(单一线程池)
只会创建一个线程执行任务。(适用于需要保证顺序执行各个任务;并且在 任意时间点,没有多线程活动的场景。)
CachedThreadPool:(缓存线程池)
是一个会根据需要调整线程数量的线程池。(大小无界,适用于执行很多的短期异 步任务的小程序,或负载较轻的服务器)
ScheduledThreadPool:(延迟线程池)
继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行任务,或 者定期执行任务。使用DelayQueue作为任务队列。
execute():
只能执行 Runnable 类型的任务。
submit():
可以执行 Runnable 和 Callable 类型的任务。 Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返 回值。
方法一:
使用安全类,比如 Java. util. concurrent 下的类。
方法二:
使用自动锁 synchronized。
方法三:
使用手动锁 Lock
当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独 占锁 b,并 尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互 相持有对方需要的锁,而发生 的阻塞现象,我们称为死锁。
尽量使用 tryLock(long timeout, TimeUnit unit)的方法 (ReentrantLock、 ReentrantReadWriteLock),设置超时时间,超 时可以退出防止死锁。
尽量使用 Java. util. concurrent 并发类代替自己手写锁。
尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
尽量减少同步的代码块
ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个 线程都可以独 立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 的经典使用场景是数据库连接和 session 管理等。
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码 块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自 动释放 锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不 当没有 unLock() 去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办 到。
Java 序列化是为了保存各种对象在内存中的状态,并且可以把保存的对象 状态再读出来。
以下情况需要使用 Java 序列化:
想把的内存中的对象状态保存到一个文件中或者数据库中时候;
想用套接字在网络上传送对象的时候;
想通过 RMI(远程方法调用)传输对象的时候。
动态代理是运行时动态生成代理类。
应用:
动态代理的应用有 spring aop、rpc, Java 注解对象获取等。
JDK 原生动态代理和 cglib 动态代理。
JDK 原生动态代理是基于接口实 现的
cglib 是基于继承当前类的子类实现的。
克隆的对象可能包含一些已经修改过的属性,而 new 出来的对象的属性都 还是初始化 时候的值,所以当需要一个新的对象来保存当前对象的“状态”就 靠克隆方法了。
Redis 是一个使用 C 语言开发的高速缓存数据库。
Redis 使用场景:
记录帖子点赞数、点击数、评论数;
缓存近期热点数据;
记录用户会话信息.
数据缓存功能
分布式锁功能
支持数据持久化
解决分布式会话
缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据 库查询,查 不到数据则不写入缓存,这将导致这个不存在的数据每次请求都 要到数据库去查询,造成缓 存穿透
解决方法:
1. 最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大 的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免 了对底层存储系统的查询压力。
2.另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查 询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个 空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这 时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数 据,引起数据库压力瞬间增大,造成过大压力。
解决方法:
1.设置永久不过期。【这种只适合】
2.使用互斥锁(mutex key)业界比较常用的做法,是使用mutex。简单 地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的 SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功 时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方 法。
代码如下:
- public String get(key) {
- String value = redis.get(key);
- if (value == null) { //代表缓存值过期
- //设置3min的超时,防止del操作失败的时候,下次缓存过
- 期一直不能load db
- if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {
- //代表设置成功
- value = db.get(key);
- redis.set(key, value,
- expire_secs);
- redis.del(key_mutex);
- } else { //这个时候代表同时候的其他线程已经
- load db并回设到缓存了,这时候重试获取缓存值即可
- sleep(50);
- get(key); //重试
- }
- } else {
- return value;
- }
- }
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据 库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发 查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数 据库。
解决方案:
1.缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2.如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据 库中。
3.设置热点数据永远不过期。
每日一更新,祝大家找到自己心仪的工作!!!!!!