面向对象
优点: 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点: 性能比面向过程低
Java和C++
- 都是面向对象的语言,都支持封装、继承和多态
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承
- Java 有自动内存管理机制,不需要程序员手动释放无用内存
构造器 Constructor 是否可被 override?
父类的私有属性和构造方法并不能被继承,所以Constructor 也就不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
overload和override
重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法
方法重载跟返回值类型和修饰符无关
多态
程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定。
两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)
条件:1.继承 2.方法的override 3.父类引用指向子类对象
静态方法内调用一个非静态成员
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
解决方法
一、静态方法只能访问静态方法和静态成员。
class Test{
public static int sum(int a,int b){//加入static关键字,变成静态方法
return a+b;
}
public static void main(String[] args){
int result=sum(1,2);//静态方法调用静态方法
System.out.println("result="+result);
}
}
二、非静态方法要被实例化才能被静态方法调用。
class Test{
public int sum(int a,int b){
return a+b;
}
public static void main(String[] args){
Test test=new Test();//实例化类
int result=test.sum(1,2);//调用非静态方法
System.out.println("result="+result);
}
}
无参构造的作用
Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特定的构造方法,则会调用父类中的无参构造。
在调用子类构造方法之前会先调用父类无参构造方法,是为了帮助子类做初始化工作。
接口和抽象类
abstract起到模板的作用,可以有属性和方法,interface起到规范方法行为的作用,只能有抽象方法存在,两者都不可以实例化,abstract单继承(视乎语言),interface可以多实现。
https://www.jianshu.com/p/d1acaec2299b
- 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法
- 接口中的实例变量默认是 final 类型的,而抽象类中则不一定
- 一个类可以实现多个接口,但最多只能继承一个抽象类
- 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
- 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范
成员变量和局部变量
- 从语法形式上,成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数
- 从变量在内存中的存储方式来看,成员变量是对象的一部分,而对象存在于堆内存,局部变量存在于栈内存
- 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失
- 成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外被 final 修饰的成员变量也必须显示地赋值);而局部变量则不会自动赋值
Java获取对象的四种方式
new class
生成的对象置于内存中的堆空间中
clone
最大的区别就是,有没有复制对象,在堆内存中的是否为一个
new 与 clone 的区别
复制引用:Person p = new Person(23, "zhang"); Person p1 = p;
复制对象:Person p = new Person(23, "zhang"); Person p1 = (Person) p.clone();
new操作符的本意是分配内存。程序执行到new操作符时, 首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
而clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域, 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
浅拷贝
仅仅复制所考虑的对象,而不复制它所引用的对象。
深拷贝
把要复制的对象所引用的对象都复制了一遍。
clone是浅拷贝的
反射 获取对象
还有所谓的工厂创建对象都是根据反射而创建的框架产生的,而其中的原理就是利用了反射。
从本地文件的反序列化
- 序列化 :把对象转换为字节序列存储于磁盘或者进行网络传输的过程称为对象的序列化。
- 反序列化:把磁盘或网络节点上的字节序列恢复到对象的过程称为对象的反序列化。
- 综上,可以得出对象的序列化和反序列化主要有两种用途:
- 把对象的字节序列永久地保存到磁盘上。(持久化对象)
- 可以将Java对象以字节序列的方式在网络中传输。(网络传输对象)
【1】、必须实现序列化接口 Serializable: Java.io.Serializable 接口。
【2】、serialVersionUID:序列化的版本号,凡是实现 Serializable 接口的类都有一个静态的表示序列化版本标识符的变量。
【3】、serialVersionUID 的取值:此值是通过 Java 运行时环境根据类的内部细节自动生成的。如果类的源代码进行了修改, 再重新编译,新生成的类文件的 serialVersionUID 的值也会发生变化。不同的编译器也可能会导致不同的 serialVersionUID。为了提高 serialVersionUID 的独立性和确定性,建议在一个序列化类中显示的定义 serialVersionUID,为它赋予明确的值。
静态方法和实例方法
- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
因为两个对象有相同的 hashcode 值,它们也不一定是相等的
hashCode 的默认行为是对堆上的对象产生独特值。如果没有重写hashCode,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
类中所有的 private 方法都隐式地指定为 final
not in和not exists
查询语句使用了not in 那么内外表都进行全表扫描,没有用到索引,而not extsts 的子查询依然能用到表上的索引。所以无论哪个表大,用not exists都比not in要快。
sleep,wait,join,yield
锁池:所有需要竞争同步锁的线程都会放到锁池中。
等待池:当调用wait方法后线程会放到等待池中,等待池不会竞争同步锁。只有调用了notify或notifyAll方法后才开始竞争。
- sleep是Thread类的静态本地方法,wait是Object类的本地方法。
- sleep方法不会释放lock,而wait会释放后加入等待队列。
- sleep不依赖synchronized,而wait需要synchronized
- sleep不需要被唤醒,wait需要
- sleep一般为当前线程休眠,wait则多用于多线程之间的通信
- sleep会让出cpu执行事件强制上下文切换,wait不一定
yield执行后线程直接进入就绪状态,马上释放cpu的执行权
join执行后线程进入阻塞状态,例如线程B中调用了线程A的join方法,那线程B会进入阻塞队列,直到线程A结束或中断线程
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW(初始):线程被创建后尚未启动
- RUNNABLE(运行):包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,也可能正在等待系统资源,如等待CPU为它分配时间片
- BLOCKED(阻塞):线程阻塞于锁
- WAITING(等待):线程需要等待其他线程做出一些特定动作(通知或中断)
- TIME_WAITING(超时等待):该状态不同于WAITING,它可以在指定的时间内自行返回
- TERMINATED(终止):该线程已经执行完毕
进程和线程,线程的通信方式
进程是资源分配的最小单位,线程是CPU调度的最小单位
- 基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想
- Object类提供了线程间通信的方法:
wait()、notify()、notifyaAl()
,注意: wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁 - 使用JUC工具类 CountDownLatch,相当于也是维护了一个线程间共享变量state
- 使用 ReentrantLock 结合 Condition,condition.signal和condition.await
进程的通信方式
- 匿名管道实现父进程和子进程之间的通信,半全双工,单向流动,匿名管道不支持跨网络之间的两个进程之间的通信,且不是任意的两个进程。通过匿名管道可以实现子进程输出的重定向。
- 管道,通常在非父子进程通信,创建管道=》打开管道文件=》管道的写
- 信号量,控制多个进程对共享资源的访问,PV操作
- 消息队列链表,存放在内核中,克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点
- 共享内存,效率最高
- 嵌套字,socket
处理不想序列化的字段
使用 transient 关键字修饰。
作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
transient 只能修饰变量,不能修饰类和方法。
Native方法
Native Method就是一个java调用非java代码的接口。
被native关键字修饰的方法叫做本地方法,本地方法和其它方法不一样,本地方法意味着和平台有关,因此使用了native的程序可移植性都不太高。另外native方法在JVM中运行时数据区也和其它方法不一样,它有专门的本地方法栈。native方法主要用于加载文件和动态链接库,由于Java语言无法访问操作系统底层信息(比如:底层硬件设备等),这时候就需要借助C语言来完成了。被native修饰的方法可以被C语言重写。
堆和栈
堆是进程和线程共有的空间,唯一目的是存放对象实例,创建的对象和数组都保存在堆中。
栈是每个线程独有的,每个线程的栈相互独立。每个方法在执行的同时都会创建一个栈帧用来存储局部变量表、操作数栈、动态链接(代码中的符号引用(#10)=>方法区中的直接引用)、方法出口等信息。
无法通过虚引用来获取对象的真实地址
ThreadLocal
每条线程都还有私有的ThreadLocalMap容器,无需使用同步机制保证多线程访问容器的互斥性。(使用static,又不想考虑线程安全的时候用)
使用场景:
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
- 线程间数据隔离
- 进行事务操作,用于存储线程事务信息(Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection)
- 数据库连接,Session会话管理
ThreadLocal内存泄漏的根源:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
正确用法:
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
synchronized
对象锁(monitor)
synchronized的具体底层实现,demo:
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
}
method();
}
private static void method() {
}
}
上面的代码中有一个同步代码块,锁住的是类对象,并且还有一个同步静态方法,锁住的依然是该类的类对象。编译之后,切换到SynchronizedDemo.class的同级目录之后,然后用javap -v SynchronizedDemo.class
查看字节码文件:
使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。
从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
看图,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
CAS
在J.U.C包中利用CAS实现类,在Lock实现中会有CAS改变state变量,在atomic包也是
CAS的问题
ABA问题
解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。
自旋时间过长
使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。
并发的三大特性
总线lock
在CPU1要操作共享变量的时候,其在总线上发出一个 LOCK# 信号,其他处理器就不能操作缓存了该共享变量内存地址的缓存。
缓存一致性协议
当某块CPU对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,或者从主内存中重新读取
MESI协议:在每个缓存行上维护两个状态位
- M:被修改的。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。
- E:独占的。处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。
- S:共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。
- I:无效的。本CPU中的这份缓存已经无效。
M状态必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
E状态必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。
S状态必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。
当CPU需要写数据时,只有在其缓存行是M或者E的时候才能执行,否则需要发出特殊的RFO指令(Read Or Ownership,这是一种总线事务),通知其他CPU置缓存无效(I),这种情况下性能开销是相对较大的。在写入完成后,修改其缓存状态为M。
原子性
AtomicInteger
AtomicInteger.incrementAndGet(用了CAS)
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
quartz实现高级定制化定时任务中用AtomicInteger标识程序执行过程中是否发生了异常
把普通变量升级为原子变量:主要是AtomicIntegerFieldUpdater<T>
类
在高并发情况下,LongAdder(累加器)比AtomicLong原子操作效率更高,LongAdder累加器是java8新加入的
在高度并发竞争情形下,AtomicLong每次进行add都需要flush和refresh(这一块涉及到java内存模型中的工作内存和主内存的,所有变量操作只能在工作内存中进行,然后写回主内存,其它线程再次读取新值),每次add都需要同步,在高并发时会有比较多冲突,比较耗时导致效率低;而LongAdder中每个线程会维护自己的一个计数器,在最后执行LongAdder.sum()
方法时候才需要同步,把所有计数器全部加起来,不需要flush和refresh操作。
转载:https://blog.csdn.net/fanrenxiang/article/details/80623884
可见性
// final也保证可见性
synchronized(阻塞同步)
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值(注意:加锁与解锁需要是同一把锁)
- 线程解锁前,必须把共享变量的最新值刷新到主内存中
volatile(非阻塞同步):在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令(缓存一致性)
- 它先对总线和缓存加锁,再执行后面的指令,期间其他CPU的读写请求都会被阻塞(其他线程可以看见)
- 最后释放锁后会把高速缓存中的脏数据全部刷新回主内存,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效(M操作)。
有序性
synchronized:排他的、可重入的锁。通过排他锁保证了是单线程执行的。满足了as-if-serial语义(单线程),单线程的有序性就天然存在了(不能禁止重排序)
as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
volatile:禁止重排序,使用内存屏障,统一由jvm来生成内存屏障的指令,Lock是软件指令。
内存屏障的作用:
- 屏障下面的代码不能跟屏障上面的代码交换执行顺序
- 线程修改完共享变量以后会马上把该变量从本地内存写回到主内存,并且让其他线程本地内存中该变量副本失效(使用MESI协议)
内存屏障是CPU指令。volatile读前插读屏障,写后加写屏障:
- Load Barrier,读前插读屏障,可以让高速缓存中的数据失效,强制重新从主内存加载新数据
- Store Barrier,写后加写屏障,能让写入缓存中的最新数据更新写入主内存,让其他线程可见
//hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp
if (cache->is_volatile()) {
if (tos_type == itos) {
obj->release_int_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == atos) {
VERIFY_OOP(STACK_OBJECT(-1));
obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
} else if (tos_type == btos) {
obj->release_byte_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == ltos) {
obj->release_long_field_put(field_offset, STACK_LONG(-1));
} else if (tos_type == ctos) {
obj->release_char_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == stos) {
obj->release_short_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == ftos) {
obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
} else {
obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
}
OrderAccess::storeload();
}
//hotspot/src/os_cpu/linux_x86/vm/orderAccess_linux_x86.inline.hpp
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
如果指令甲happens-before指令乙,那么指令甲必须排序在指令乙之前,并且指令甲的执行结果对指令乙可见。
synchronized和ReentrantLock的区别
synchronized是Java关键字,在jvm层面上,lock是一个类,juc,在jdk上
lock可以获得锁的状态
ReentrantLock的特点,synchronized只能是非公平锁,ReentrantLock可以指定;ReentrantLock有condition,condition可以唤醒指定线程;lock.interrupt方法可以等待线程中断
在发生异常时,synchronized会自动释放锁,lock不会自动释放锁,需要在finally释放锁
synchronized:假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待
ReentrantLock就提供了2种机制:可中断/可不中断
第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);
第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。
ThreadPool 中 submit 和 execute
都是用来执行线程池的,只不过使用 execute 执行线程池不能有返回方法,而使用 submit 可以使用 Future 接收线程池执行的返回值。
ThreadPoolExecutor 都需要哪些参数
- corePoolSize:线程池中的核心线程数
- maximumPoolSize:线程池中最大线程数
- keepAliveTime:闲置超时时间
- unit:keepAliveTime 超时时间的单位(时/分/秒等)
- workQueue:线程池中的任务队列
- threadFactory:为线程池提供创建新线程的线程工厂
- rejectedExecutionHandler:线程池任务队列超过最大值之后的拒绝策略
线程池有哪些
1.newCachedThreadPool创建一个可缓存线程池程,线程数量不定的线程池,并且其最大线程数为Integer.MAX_VALUE,适合执行大量的耗时较少的任务
2.newFixedThreadPool 创建一个定长线程池,只有核心线程并且这些核心线程不会被回收,这样它更加快速地相应外界的请求
3.newScheduledThreadPool 创建一个周期性执行任务的线程池,核心线程数量是固定的,而非核心线程数是没有限制的,主要用于执行定时任务和具有固定周期的重复任务
4.newSingleThreadExecutor 创建一个单线程化的线程池,只有一个核心线程
线程池的队列类型
1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool(5)使用了这个队列。
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
拒绝策略
CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
DiscardPolicy - 直接丢弃,其他啥都没有
DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
线程池中为什么先入队列而不是先创建最大线程
在创建新线程时,要获取全局锁,这时其他都得阻塞,影响效率。
线程池的作用
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。
线程池中线程复用原理
线程池将线程和任务进行解耦,摆脱了之前通过Thread创建线程时一个线程必须对应一个任务的限制。
同一个线程可以从阻塞队列中不对获取新任务来执行,其核心原理在于线程池对Thread封装,并不是每次执行任务都调用Thread.start()来创建新线程,而是让每个线程执行一个“循环任务”,不断检查是否有任务需要被执行,有就调用任务中的run方法。
对象池对垃圾回收的影响
对象池的缺点:
(1)现在Java的对象分配操作不比c语言的malloc调用慢, 对于轻中量级的对象, 分配/释放对象的开销可以忽略不计;
(2)并发环境中, 多个线程可能(同时)需要获取池中对象, 进而需要在堆数据结构上进行同步或者因为锁竞争而产生阻塞, 这种开销要比创建销毁对象的开销高数百倍;
(3)由于池中对象的数量有限, 势必成为一个可伸缩性瓶颈;
(4)很难正确的设定对象池的大小, 如果太小则起不到作用, 如果过大, 则占用内存资源高, 可以起一个线程定期扫描分析, 将池压缩到一个合适的尺寸以节约内存,但为了获得不错的分析结果, 在扫描期间可能需要暂停复用以避免干扰(造成效率低下), 或者使用非常复杂的算法策略(增加维护难度);
(5)设计和使用对象池容易出错, 设计上需要注意状态同步, 这是个难点, 使用上可能存在忘记归还(就像c语言编程忘记free一样), 重复归还(可能需要做个循环判断一下是否池中存在此对象, 这也是个开销), 归还后仍旧使用对象(可能造成多个线程并发使用一个对象的情况)等问题;
红黑树的性质
二叉查找树。叶子节点都是黑色的null。根节点是黑的。不能有两个连续的红色节点,红色的子节点必须是黑的。从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
jdk8的流原理
stream流原理:中间操作与结束操作,中间操作只是对操作进行了记录,只有结束操作才会触发实际的计算(即惰性求值),这也是Stream在迭代大集合时高效的原因之一。这些Stream对象以双向链表的形式组织在一起,构成整个流水线。由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。
JUC
操作系统的阻塞,加锁怎么实现的
https://blog.csdn.net/weixin_44367006/article/details/101637239
阻塞:进程是有时间片调度算法的,一个进程阻塞了,会放入等待队列里面,此时调度算法就不会再调度他。等他收到信号或者等待时间过了之后,再放入执行队列里面。
加锁:linux底层有futex函数可以放入操作系统的等待队列,同时告诉操作系统解锁的条件。具体到底层,还是cpu指令比如xchg可以保证原子性的比较,总线mesi协议可以保证各个cpu的值都是最新的。
unsafe.park和unpark
unpark是发放许可,park调用时会检查是否有许可,如果没有就阻塞
AQS-AbstractQueuedSynchronizer
https://segmentfault.com/a/1190000015804888
加锁:把线程包装成节点加入队列。其他线程加锁时会cas插入到队列尾部,并Locksupport.park。a,b,c三个竞争锁,A先获取,此时没有竞争,直接获取,不进入队列;b获取时发现竞争,需要进入队列,aqs队列的特点是前一个节点表示后一个节点的状态,所以有一个伪头节点,b放入伪头节点之后,并修改伪头节点的标志位为-1,表示后面节点被阻塞。c同理。
所以现在队列是-1=>-1=>0
解锁:unlock的时候,会调用release唤醒首节点。此时需要设置首节点状态为0,表示下一个节点将要被唤醒。然后把后继节点找出来,准备unpark节点。对于节点的状态还有一种情况是超时或者取消,也就是status=1的状态,此时需要从后向前查找第一个不是被取消的节点(为什么从后往前?考虑并发入队,首先node的next和prev指针都是volatile的,具体没想明白)
此时B节点被唤醒了,需要设置头节点是自己。
现在是0=>-1=>0
无论哪种状态,B都会被唤醒,然后把自己变成头节点(如果是取消节点,会在此时进行遍历,修改队列结构)。然后c唤醒,释放锁,此时后面没有节点了,所以只剩下一个伪头节点,是当前节点。
AQS的condition
https://segmentfault.com/a/1190000015807209
//ThreadA先调用lock方法获取到锁,然后调用con.await()
//ThreadB获取锁,调用con.signal()
唤醒ThreadA
//ThreadB释放锁
condtion复用了aqs的node节点类型,condition的队列我们称作条件队列,原来的队列称作等待队列。a先获取到锁,然后await。await释放锁,然后把线程包装成节点入条件队列。
b获取锁,然后signal。signal会删除条件队列的头结点,并添加到等待队列,此时等待队列是-1=>0,在释放锁之后,这个等待队列的节点会被唤醒。
b释放锁,等待队列的a会被唤醒。
CountdownLatch
await都会加入到等待队列里面。在countdown时,如果计数器为0,会唤醒(unpark)等待队列的头结点。并且会修改当前节点的状态为0,再调用propagate,向后传播状态,来唤醒队列里面的所有节点。
数据库数据
数值,int,float,double
字符,char,varchar,text
时间,date,datetime,timestamp