泛型
为什么需要泛型
- 重构相同函数体且不同类型返回值和参数时,可以使用泛型。而不需要多个重构函数
比如:
public int getMiddle(int... a) {
return a[a.length/2];
}
public float getMiddle(float... a) {
return a[a.length/2];
}
/**
* 使用泛型方法就可以直接不需要重构多个方法
*/
public T getMiddle(T...a){
return a[a.length/2];
}
- 泛型中的类型在使用时指定,不需要强制类型转换,在定义了特定的类型后,只能传入具体的类型,预防了传入不同类型时在使用时导致产生异常。
泛型类,泛型接口,泛型方法
1. 泛型类
直接在类后面加上"
public class Generic{
private T key;
private T data;
public Generic(T key) {
this.key = key;
}
}
派生(继承)方式:
- 派生类也使用泛型
public class GenericClass extends Generic{
public GenericClass(T key) {
super(key);
}
}
- 派生类使用具体类型
public class GenericClass extends Generic{
public GenericClass(String key) {
super(key);
}
2. 泛型接口
跟泛型类定义类似,也是在接口后面加上
public interface Generic{
T next();
}
3. 泛型方法
泛型方法需要在方法的修饰符和返回值之间加上"
public E getK(E e) {
return e;
}
泛型方法的参数跟泛型类所传递的参数没有任何关系。
限定类型变量
当对泛型的变量类型有约束限制时,可以使用限定类型变量 。该用法可以在泛型类,泛型方法和泛型接口上使用。
用法如下:在泛型T后面加上extends 再加上需要继承或实现的类。extends左右都允许多个,但是只能继承一个类,并且要把继承类写在最前面。可以实现多个接口。
public static E getKey(E e) {
return e;
}
public interface GenericInterface{
}
public class Generic{
}
泛型的约束和局限性
- 不能用基本数据类型实例化类型参数
// Generic generic = new Generic<>(1);
// 不能用基本数据类型,只能使用其包装类
Generic generic = new Generic<>(1);
- 泛型的实例不能用instanceof关键字来判断
Generic generic = new Generic<>("Steven");
//这种不允许进行判断
//if (generic instanceof Generic)
- 不能在静态域或方法中引用类型变量,但是泛型方法可以是静态的。
原因:泛型是在对象创建的时候才知道具体类型是什么,但是对象创建的代码执行先后顺序是先static的部分,然后才是构造函数等等,所以静态部分使用的泛型,虚拟机根本就不知道是什么东西。 - 不能创建参数化类型的数组,但是定义可以。
// Generic[] generic;
// 不能创建
// Generic[] generic = new Generic[10];
- 不能实例化类型变量
public static class Generic{
private T key;
private T data;
private void setKey() {
// 这个不允许实例化
// this.data = new T();
}
}
- 捕获异常时不能捕获泛型类的实例,但是可以抛出
public void getT(){
/*try {
}catch (T t){
// 不能捕获泛型类的实例
}*/
}
public void getT(T t) throws T{
try{
}catch (Throwable e){
throw t;
}
}
泛型的通配符
- ? extends X 表示的类型的上界,类型参数是X的子类。
- ? super X 表示的类型的下界,类型参数是X的父类。
? extends X
表示的是类型参数必须是X的子类。对于泛型类来说,如果提供了类型参数的get和set方法,只能调用get方法获取X类的对象,而不能调用set方法进行赋值操作。因为使用 ? extends X 只能确定是类型参数是X的子类或者X,get方法必然可以获取到一个X。而set方法,编译器则无法知道传入的是X的哪个子类或者他本身。
? super X
表示的是类型参数必须是X的父类。对于泛型类来说,如果提供了类型参数的get和set方法,使用get方法的话只能获取到一个Object的对象,因为无法确定传入的泛型参数是X的哪个父类,只能返回一个最顶级的Object对象。而set方法则可以调用返回一个X类或者X的子类对象。
无限定的通配符 ?
? 表示对类型没有任何限定,可以传入任何类型
ArrayList> arrayList = new ArrayList<>();
虚拟机是如何实现泛型的
Java中的泛型其实只是在源码中存在,当变异为字节码文件后,就已经替换为原来的原生类型了(Raw Type 裸类型),并且在相应的地方插入强制转型的代码。这种实现方方式称为类型擦除。基于这种方法实现的泛型称为伪泛型。对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类。
当然Java中还是保存了泛型擦除之前的类型信息的。如Signature、LocalVariableTypeTable。Signature的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息
反射
反射就是在运行的时候才知道要操作的类是什么,并且可以获取到该类的完整构造以及成员变量和方法,并且调用其所有方法。
反射的主要功能
反射主要是在运行时可以提供以下功能:
- 创建任何类对象。
- 获取任何类的成员属性和方法。
- 调用任何类的属性和方法。
Class类
Class类封装了当前对象所对应的类的信息。
获取Class对象的三种方式
- 通过类名获取 类名.class。
- 通过类的对象获取 对象.getClass()。
- 通过全类名获取 Class.forName("全类名")。
Class类的常用方法
类加载器
String className = "全类名";
Class clazz = Class.forName(className);
ClassLoader classLoader = clazz.getClassLoader();
构造器
public void testConstructor() throws Exception{
String className = "全类名";
Class clazz = (Class) Class.forName(className);
System.out.println("获取全部Constructor对象-----");
Constructor[] constructors
= (Constructor[]) clazz.getConstructors();
for(Constructor constructor: constructors){
System.out.println(constructor);
}
System.out.println("获取某一个Constructor 对象,需要参数列表----");
Constructor constructor
= clazz.getConstructor(String.class, int.class);
System.out.println(constructor);
//2. 调用构造器的 newInstance() 方法创建对象
System.out.println("调用构造器的 newInstance() 方法创建对象-----");
Person obj = constructor.newInstance("Steven", 18);
System.out.println(obj.getName());
}
Field成员变量
当成员变量是私有的时候,不管是取值还是赋值,都需要先调用setAccessible(true)方法。使用getDeclaredFields()方法可以获取自身的公有和私有所有字段,但不能获取父类字段。使用getFields()只能获取所有父类的公有字段,
public void testField() throws Exception{
String className = "全类名";
Class clazz = Class.forName(className);
System.out.println("只能获取所有父类的公有字段");
Field[] clazzFields = clazz.getFields();
for(Field field: clazzFields){
System.out.print(" "+ field.getName());
}
System.out.println("获取公有和私有的所有字段,但不能获取父类字段");
Field[] fields = clazz.getDeclaredFields();
for(Field field: fields){
System.out.print(" "+ field.getName());
}
System.out.println();
System.out.println("---------------------------");
System.out.println("获取指定字段");
Field field = clazz.getDeclaredField("name");
System.out.println(field.getName());
Person person = new Person("ABC",12);
System.out.println("获取指定字段的值");
Object val = field.get(person);
System.out.println(field.getName()+"="+val);
System.out.println("设置指定对象指定字段的值");
field.set(person,"DEF");
System.out.println(field.getName()+"="+person.getName());
System.out.println("字段是私有的,不管是读值还是写值," +
"都必须先调用setAccessible(true)方法");
// 比如Person类中,字段name字段是非私有的,age是私有的
field = clazz.getDeclaredField("age");
field.setAccessible(true);
System.out.println(field.get(person));
}
方法
使用反射获得方法进行使用时,需要注意以下几点:
- 使用getMethods()方法获取的方法,只能获取公有(public)方法,可以获取到所有父类(包括父类的父类)的公有(public)方法。
- 使用getDeclaredMethods()方法获取的方法,可以获得当前类的所有方法,包括私有方法, 但是只能获取该类父类的公有方法(父类的父类公有方法获取不到)。
- 私有方法的执行,必须在调用invoke之前加上一句method.setAccessible(true);
- 反射获取方法时,基本数据类型和其包装数据类型需要进行区分,否则的话获取不到。
public void testMethod() throws Exception{
Class clazz = Class.forName("全类名");
System.out.println("获取clazz对应类中的所有方法," +
"不能获取private方法,且获取从父类继承来的所有方法");
Method[] methods = clazz.getMethods();
for(Method method:methods){
System.out.print(" "+method.getName()+"()");
}
System.out.println("获取所有方法,包括私有方法," +
"所有声明的方法,都可以获取到,且只获取当前类的方法");
methods = clazz.getDeclaredMethods();
for(Method method:methods){
System.out.print(" "+method.getName()+"()");
}
System.out.println("获取指定的方法," +
"需要参数名称和参数列表,无参则不需要写");
// 方法public void setName(String name) { }
Method method = clazz.getDeclaredMethod("setName", String.class);
System.out.println(method);
// 方法public void setAge(int age) { }
// clazz.getDeclaredMethod("setAge", Integer.class);
/* 这样写是获取不到的,如果方法的参数类型是int型,
如果方法用于反射,那么要么int类型写成Integer: public void setAge(Integer age) { }
要么获取方法的参数写成int.class*/
method = clazz.getDeclaredMethod("setAge", int.class);
System.out.println(method);
System.out.println("执行方法,第一个参数表示执行哪个对象的方法" +
",剩下的参数是执行方法时需要传入的参数");
Object obje = clazz.newInstance();
method.invoke(obje,18);
/*私有方法的执行,必须在调用invoke之前加上一句method.setAccessible(true);*/
method = clazz.getDeclaredMethod("privateMethod");
System.out.println(method);
System.out.println("执行私有方法");
method.setAccessible(true);
method.invoke(obje);
}
代理
代理模式
代理模式就是给一个对象提供一个代理对象,通过代理对象控制对原对象的引用。通俗的来讲就是我们所说的中介。
代理模式中一般存在三个角色:
- 抽象角色:封装真实角色和代理角色对外提供的公共方法。一般为接口。
- 真实角色:实现抽象角色的接口,定义了真实角色所要实现的业务逻辑,以便提供给代理角色使用。这是真正的业务逻辑实现的地方。
- 代理角色:实现抽象角色的接口,是真实角色的代理。通过传入真实角色的对象,在内部实现抽象角色的方法中,调用真实角色的方法,并且可以加上自己的逻辑处理
访问者不再访问真实对象,而是直接通过代理对象完成业务需求。在使用的时候,都需要创建真实对象,然后传入代理对象方法中,通过代理对象进行处理。
静态代理
静态代理在使用的时候,需要定义接口或者父类,被代理的对象(真实对象)和代理对象共同实现接口或者父类(抽象对象),一般来说,静态代理的被代理对象和代理对象是一对一的(可以一个代理对象对应多个被代理对象)。
缺点:
一对一则会造成静态代理对象多,代码量大,从而导致代码复杂,可维护性差。
一对多则会导致代理对象扩展能力差。
动态代理
创建动态代理类时,需要实现InvocationHandler接口,实现其唯一的invoke()方法。通过Proxy类来创建动态代理类的实例对象,通过该实例对象调用代理方法。通过动态代理JDK自动生成的类,其父类都是Proxy类。Proxy类中有成员变量InvocationHandler h,而自动生成的类中会实现其抽象角色的接口,在实现该接口的方法中调用h.invoke()方法,来实现被代理类的逻辑。
public class Company implements InvocationHandler {
/*持有的真实对象*/
private Object factory;
public Object getFactory() {
return factory;
}
public void setFactory(Object factory) {
this.factory = factory;
}
/*通过Proxy获得动态代理对象*/
public Object getProxyInstance(){
return Proxy.newProxyInstance(factory.getClass().getClassLoader(),
factory.getClass().getInterfaces(),this);
}
/*通过动态代理对象方法进行增强*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
doSthBefore();
Object result = method.invoke(factory, args);
doSthAfter();
return result;
}
/*前置处理器*/
private void doSthAfter() {
}
/*后置处理器*/
private void doSthBefore() {
}
}
优点:
只需要一个动态代理类就可以解决创建多个静态代理的问题,避免重复冗余的代码,有更强的灵活性。
缺点:
效率低,需要通过反射机制从而获取调用真实对象的方法。
应用场景局限,因为Java的单继承性(每个代理类都继承了Proxy类),只能针对接口创建代理类,不能针对类。
多线程
基础概念
CPU核心数和线程数的关系
一般情况下,CUP核心数和线程数是1:1的关系,但是Intel引入了超线程技术后,使得核心数和线程数达到了1:2的关系。
并行:多个CPU同时并行地运行程序,称为并行处理。并行必须是同时执行的。
并发:通常所说的并发量,是指在一定的时间内执行的程序任务量。在说并发的时候,一定会加上一个时间单位来衡量。并发是交替执行的。
CPU时间片轮转机制
时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个线程分配了一个时间段(称为时间片),这就是该线程允许运行的时间。每次线程切换(从一个线程切换到另外一个线程,也称为上下文切换)都是需要时间的。
进程和线程
进程是程序运行资源分配的最小单位。 资源分配包括:CPU,内存空间,磁盘等。同一进程中,多条线程共享该进程的资源。进程和进程之间是相互独立的。
线程是CPU调度的最小单位,但是必须依赖于进程而存在 是比进程更小,能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)。线程无处不在
高并发编程
好处:
- 充分利用CPU资源。(地铁上看书)
- 加快用户响应时间。(迅雷多线程下载)
- 将代码模块化,异步化,简单化。
注意事项:
- 线程之间的安全性。
- 线程之间的死循环过程。
- 线程多了会将资源耗尽导致死机或者当机。
线程的启动和终止
线程的启动
- X extends Thread;在run()方法中实现自身逻辑,创建X对象,调用X.start()方法启动。
- X implements Runnable;在run()方法中实现自身逻辑,然后交给Thread创建对象调用start()方法启动。
- X implements Callable;在call()方法中实现自身逻辑,然后交给Thread创建对象调用start()方法启动。
/*扩展自Thread类*/
private static class UseThread extends Thread{
@Override
public void run() {
super.run();
//do my work
System.out.println("I am extends Thread");
}
}
/*实现Runnable接口*/
private static class UseRun implements Runnable{
@Override
public void run() {
System.out.println("I am implements Runnable");
}
}
/*实现Callable接口,允许有返回值*/
private static class UseCall implements Callable{
@Override
public String call() throws Exception {
System.out.println("I am implements Callable");
return "CallResult";
}
}
线程获取执行结果
启动线程的第一种和第二种方式,在执行完成后无法获取执行结果。 而第三种方式泛型接口Callable中的V call() 方法可以获取执行结果,返回类型为泛型接口中传入的参数。
Future接口能对具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。通过get方法获取执行结果,该方法会阻塞直到任务返回结果。但是Future只是一个接口,无法创建对象,所以采用它的唯一实现类FutureTask来实现创建需要获取执行结果的线程。FutureTask接收Callable或者Runnable对象。
UseCall useCall = new UseCall();
FutureTask futureTask = new FutureTask<>(useCall);
new Thread(futureTask).start();
//do my work
System.out.println(futureTask.get());
线程的终止
自然终止
- run()方法执行完成。
- 抛出了一个未处理的异常导致线程提前结束。
手动终止
不安全、过期的API:
- suspend() 暂停 -> 线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,容易引发死锁问题
- resume() 恢复
- stop() 停止 -> 在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
安全的终止:
- interrupt() 中断 (无法保证一定会中断) -> 将线程的中断标志位设置为true,由isInterrupted()方法来获取中断标志位是否为true。类似于其它线程给该线程打了招呼,通知其需要中断,但是具体要不要中断,还得看其自己代码里面的判断。
java里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。
run()和start()方法对比
run()方法是Thread类中实现Runnable接口所实现的方法,其本身没有具体的特别之处,可以重复执行,被单独调用。
start()方法让一个线程进入就绪队列等待分配CPU,分到CPU后才调用run()方法。start方法不能重复调用。
yield()、join()
yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源,所有执行yield()的线程有可能在进入到可执行状态后马上又被执行。
join方法:把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
线程间的共享与协作
线程间的共享
Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。synchronized本质上锁的是对象,是一个非公平的可重入锁。
对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
//类锁,实际是锁类的class对象
private static synchronized void synClass(){}
private static Object obj = new Object();
//类似于类锁,Obj在全虚拟机只有一份
private void synStaticObject(){
synchronized (obj){
}
}
//对象锁
private synchronized void instance(){}
线程间的协作
等待/通知机制: 线程A调用了对象O的wait()方法进入等待状态,而线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。wait()和notify()以及notifAll()需要在synchronized关键字包含的代码块中调用。
notify():
通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。
notifyAll():
通知所有等待在该对象上的线程
wait()
调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回.调用wait()方法后,会释放对象的锁。
wait(long)
超时等待一段时间,参数时间是毫秒,如果没有收到通知就超时返回。
wait (long,int)
对于超时时间更细粒度的控制,可以达到纳秒
等待和通知的标准范式
等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
synchronized(对象){
while(条件不满足){
对象.wait();
}
指定对应逻辑代码
}
通知方遵循如下原则。
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
synchronized(对象){
改变条件
对象.notifyAll();
}
尽可能用notifyall(),谨慎使用notify(),notify()不能够指定唤醒
sleep()方法 和wait()方法的区别
1、这两个方法来自不同的类,sleep()来自Thread,而wait()来自Object类。
2、sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)。
4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
5、sleep是Thread类的静态方法。sleep的作用是让线程休眠制定的时间,在时间到达时恢复,也就是说sleep将在接到时间到达事件事恢复线程执行。wait是Object的方法,也就是说可以对任意一个对象调用wait方法,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify或者notifyAll方法才会重新激活调用者。
ThreadLocal
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值, ThreadLocal往往用来实现变量在线程之间的隔离,能够保证线程安全。
ThreadLocal类中主要有五个方法:
- public void set(T value) -> 设置当前线程的线程局部变量的值
- public T get() -> 返回当前线程所对应的线程局部变量。
- public void remove() -> 将当前线程局部变量的值删除,目的是为了减少内存的占用。当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
- protected T initialValue() -> 返回该线程局部变量的初始值,是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。
- ThreadLocalMap getMap(Thread t) {return t.threadLocals;} -> get()方法也是从该方法中获取ThreadLocal的静态内部类ThreadLocalMap中数组Entry[] table存储的值,而Entry则是ThreadLocalMap中的静态内部类。而Thread类中存有ThreadLocal.ThreadLocalMap threadLocals成员变量。
显示锁
Lock接口和synchronized关键字的比较
synchronized关键字会隐式地获取锁,它将锁的获取和释放固化了,也就是先获取再释放。synchronized属于Java语言层面的锁,也被称之为内置锁。一旦开始获取锁,是不能中断的,也不提供尝试获取锁的机制。
Lock是由Java在语法层面提供的,锁的获取和释放需要我们明显的去获取,因此被称为显式锁。
Lock锁有以下特性:
- 尝试非阻塞的获取锁。 当前线程尝试获取锁,如果这一时刻锁没有被其它线程获取到,则成功获取并持有锁。
- 能被中断的获取锁。 与synchronized的关键字不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将被抛出,同时锁会被释放。
- 超时获取锁。 在指定的截止时间前获取锁,如果到了截止时间还没有获取到锁,则返回。
Lock接口的核心方法
Lock的常规用法:
// 使用 new ReentrantLock(); 创建锁对象 参数传入true表示为公平锁,不传默认为非公平锁
private Lock lock = new ReentrantLock(false);
public void incr(){
lock.lock();
try{
count++;
}finally {
// 保证锁能够被释放
lock.unlock();
}
}
核心方法:
可重入锁ReentrantLock
在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁。ReentrantLock在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。synchronized关键字隐式的支持重进入。
公平和非公平锁
如果在时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平锁按照等待时间最长的线程最优先获取锁
ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。传入true代表公平锁,不传或者传入false代表非公平锁。
公平的锁机制往往没有非公平的效率高。原因是,在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。非公平锁节省了上下文切换的时间。
读写锁ReentrantReadWriteLock
synchronized和ReentrantLock是排他锁,这些锁在同一时刻只允许一个线程进行访问。
读写锁在同一时刻可以允许多个读线程访问,在写线程访问时,所有的读线程和其他写线程均被阻塞。
读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁(读写分离),使得并发性相比一般的排他锁有了很大提升。
大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock getLock = lock.readLock();//读锁
private final Lock setLock = lock.writeLock();//写锁
Condition接口
Condition接口实现了类似Object的监视器方法,与Lock配合使用,可以实现等待/通知模式。
Lock接口中有一个方法Condition newCondition(); 返回的就是一个Condition。
而condition中提供await()、signal()和signalAll()来实现等待/通知。
Condititon和Lock配合常规使用方法如下:
// 创建一个Lock对象
private Lock lock = new ReentrantLock();
// 通过Lock创建一个Condition对象
private Condition kmCond = lock.newCondition();
public void changeKm(){
lock.lock();
try{
// 执行改变条件的逻辑
this.km = 101;
kmCond.signal();// 进行通知
}finally {
// 保证锁能够得到释放
lock.unlock();
}
}
public void waitKm(){
lock.lock();
try {
while(this.km<100){
try {
// 条件不满足,继续等待
kmCond.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
}
使用Condition的时候,调用signal()代表通知该条件满足一个线程进行唤醒,而signalAll()则是对全部满足条件的线程进行唤醒。一般来说使用signal()就行了。
线程池
什么是线程池?为什么需要使用线程池
线程池就是将线程进行池话,需要运行任务的时候从线程池中拿一个线程来执行,执行完毕,线程放回池中。
线程池的好处:
- 降低资源消耗。降低线程创建和销毁的消耗。
- 提高效应速度。线程创建和销毁都需要时间。
- 提高线程的可管理性。
线程池各个参数的意义
Java提供创建线程池的类为ThreadPoolExecutor,其完整构造函数如下。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize 核心线程池大小。当线程池中的线程小于该值时,有新的任务进入时,会创建新的线程。如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize 最大线程池数量。当线程池中数量达到核心线程池数并且阻塞队列也满了之后,再有新的任务进入,会根据该值再创建线程,但是不属于核心线程,超过存活时间后,会进行销毁。
keepAliveTime 线程空闲时的存活时间。 非核心线程存活的时间。
unit 存活时间单位。
BlockingQueue
workQueue 阻塞队列。当线程池中数量达到核心线程池数后,再有新的任务进入,会放入该阻塞队列中进行排队,等待被执行。ThreadFactory threadFactory 线程工厂。通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”
RejectedExecutionHandler handler 拒绝(饱和)策略。当阻塞队列满了,且没有空闲的工作线程,再有新的任务进入,所执行的策略。
1)AbortPolicy:直接抛出异常,默认策略;
2)CallerRunsPolicy:用调用者所在的线程来执行任务;
3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4)DiscardPolicy:直接丢弃任务;
可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
阻塞队列
队列是一种特殊的线性表,只允许在其前端删除元素,在后端插入元素。队列又称为先进先出线性表。
阻塞队列
- 支持阻塞的插入方法:当队列满时,队列会阻塞插入队列的线程,直到队列不满。
- 支持阻塞的移除方法:当队列为空时,队列会阻塞移除元素的线程,直到队列不为空。
阻塞队列常用于生产者和消费者场景。
阻塞队列常用方法:
- 抛出异常:队列满时,会抛出IllegalStateException异常,队列为空时会抛出NoSuchElementException异常。
- 返回特殊值:插入成功返回ture,移除时有元素则返回,没有则返回null。
- 一直阻塞:当队列满时,put元素进入会阻塞生产者线程,直到队列可用或者响应中断退出。当队列为空时,take元素队列会阻塞消费者线程,直到队列不为空。
- 超时退出:当阻塞队列满时,如果生产者线程往队列插入元素,会阻塞线程一段时间,超过指定时间,生产者线程就会退出。
常用阻塞队列:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
ArrayBlockingQueue 必须传入队列的大小,存取元素用的是通一把锁。
LinkedBlockingQueue 可以不传入队列的大小,最大为int的最大值。存取元素不是用的同一把锁。
线程池的工作机制图解
execute()没有返回值,submit()有返回值。
合理配置线程池
要想合理地配置线程池,就必须首先分析任务特性。
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
- 任务的优先级:高、中和低。
- 任务的执行时间:长、中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
CPU密集型任务,配置CPU核心数N+1个核心线程数。
IO密集型任务,配置CPU核心数2*N个核心线程数。
混合型任务就尽量进行拆分处理。
一般线程池最大线程数为核心线程数 * 2个
CPU核心数的获取:Runtime.getRuntime().availableProcessors()
悲观锁和乐观锁
悲观锁:就是在线程更改数据之前一定要先抢到锁。synchronized和Lock都是悲观锁。
乐观锁:更改数据时先获取数据,更改后再写回数据,在写回时使用一个CAS(compare and swap 比较并且交换)机制。会对旧的数据进行比较,然后再写回新的数据。
volatile 易变的,多线程情况下只能保证可见性,不能保证正确性,禁止指令重排。使用时需要经常从主内存中进行读取,修改后也要及时写入主内存中。
AQS
AbstractQueuedSynchronizer
一般使用需要在同步工具类的内部,使用内部类继承抽象类AbstractQueuedSynchronizer
使用模板方法的设计模式:存在必须实现的方法,继承类实现这些方法,实现自己的特定的逻辑。这些方法组合运行的顺序一致(不确定是不是对的)
重要的成员变量state,通过这个变量来判断是否已经被持有锁了。用来保存当前的同步状态
private volatile int state;
AQS的基本实现其实是一个 CLH队列锁(公平锁的实现) 基于链表的自旋锁
每一个需要拿锁的线程,打包成一个节点,放入链表中。节点中包括myPred字段,用来表示自己的前驱节点,locked字段,表示当前线程是否已经释放锁了,释放了就置为false。每一线程会不断去检测他的前一个节点是否已经释放锁了(自旋到一定的程度后会阻塞),前驱节点获取到锁之后,当前线程接下来就可以获取锁了。
公平锁和非公平锁的实现差异就是公平锁会去判断队列锁里面是否有元素在进行等待,如果有就需要去进行排队等待,就多了一个方法的判断。tryAcquire()
方法中加了一个 !hasQueuedPredecessors()
判断
使用getExclusiveOwnerThread()和setExclusiveOwnerThread(Thread thread)来实现可重入锁,通过维护state的值来标记状态
Synchronize代码块中,虚拟机会给方法加入两条指令,monitorenter和monitorexit
新生代晋升老年代 缺省值是15次
轻量级锁
通过CAS操作来加锁和解锁。 --出现了自旋锁
适应性自旋锁 --控制自旋的次数 默认10次 JDK1.6动态自行进行判定执行的次数,大概就是一个上下文切换的时间
重量锁
没有拿到锁,就会把线程挂起,能够获得锁了再进行唤醒。2个上下文切换
偏向锁
CAS操作都不做了,只是去测试一下,获取锁的线程是不是第一次拿到锁的线程(大部分时候,都是同一个线程去获得同一个锁)
锁消除,锁粗化,逃逸分析。如果虚拟机发现锁里面对象只在方法中使用到,就会对变量做一些优化
虚拟机在类进行加载的时候,会对类进行加锁,保证类只会被初始化一次
学习就是要不断的重复,做笔记 重点 开始学习之前复习前一天的知识,一个专题学完之后写博客,写思维导图,把重点和难点标记出来,没事就看思维导图。