Java注解
咱们理解注解?
注解是代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。
在开发中用到的注解,比如spring中的@controller、@service,lombok的注解,比如@data,java也有自己的注解,比如@override、@resource
Java中有一种注解是元注解,所谓元注解就是用来修饰注解的注解。
常见的元注解比如@rentition,设置注解的生命周期,@target表示这个注解可以修饰哪些地方。
Java泛型
使用泛型在编译期就能够检查类型是否安全,同时也能够避免强制类型转换。
对泛型上限的限定:<? extends T>
对泛型下限的限定:<? super T>
Java NIO
nio是从jdk 1.4开始有的,其目的是为了提高速度,
传统的io是一次一个字节地处理数据,nio是以块(缓冲区)的形式处理数据。最主要的是,
nio可以实现非阻塞,而传统的io只能是阻塞的。io的实际场景是文件io和网络io,nio在网络io场景下提升就尤其明显了。
在java nio有三个核心部分组成。分别是buffer(缓冲区)、channel(管道)以及selector(选择器)。
buffer是存储数据的地方,channel是运输数据的载体,selector用于检查多个channel的状态变更情况。
你知道io模型有几种吗?
在unix下io模型分别有:阻塞io、非阻塞io、io复用、信号驱动以及异步io,在开发中碰得最多的就是阻塞io、非阻塞io以及io复用。
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
阻塞io模型是常见的io模型,在读写数据时客户端会发生阻塞。阻塞io模型的工作流程:客户端发起io请求,内核会检查数据是否就绪,如果数据就绪,
内核将数据复制到用户线程,并返回io执行结果到用户线程,此时用户线程将解除阻塞状态并开始处理数据。
非阻塞io模型就是用户线程在发起一个io请求后,无须阻塞便可以马上得到内核返回的一个结果。如果返回false,则表示内核数据还没有准备好,需要稍后发起io操作。
在非阻塞io模型中,用户线程需要不断询问内核数据是否就绪,在内存数据还未就绪时,用户线程可以处理其他任务。
典型的非阻塞io模型一般如下:
while(true) {
data = socket.read();
if(data == true) {
//1:内核数据就绪,获取并处理内核数据
break;
} else {
//2:内核数据未就绪,用户线程处理其他任务
}
}
多路复用io模型:java nio就是基于多路复用io模型实现的。
在多路复用io模型中会有一个被称为selector的线程不断轮询多个socket的状态,只有在socket有读写事件时,才会通知用户线程进行io读写操作。
java nio有三个核心部分组成。分别是buffer(缓冲区)、channel(管道)以及selector(选择器)。
Java nio是基于channel和buffer进行io读写操作,并且数据总是被从channel读取到buffer中,或者从buffer写入到channel中。
Java反射 && 动态代理
Java反射:我们通过反射可以在程序运行时获取类的信息。
https://blog.csdn.net/ljphhj/article/details/12858767
//获取class的三种方法,先new出来对象dataService,dataService.getClass(); //二、DataService.class三、class1 = Class.forName("cn.lee.demo.Person"); //创建对象Person person = (Person) class1.newInstance(); //获取构造函数, //Constructor<?>[] constructors = class1.getConstructors(); //person1 = (Person) constructors[0].newInstance(); //person2 = (Person) constructors[1].newInstance(20,"leeFeng"); //反射操作成员变量 //Field personNameField = class1.getDeclaredField("name"); //personNameField.setAccessible(true); //personNameField.set(obj, "胖虎先森"); //取得类方法 //Method[] methods = class1.getDeclaredMethods(); //Method method = class1.getMethod("fly"); //method.invoke(class1.newInstance()); //System.out.println("调用有参方法walk(int m):"); //method = class1.getMethod("walk",int.class); //method.invoke(class1.newInstance(),100); //通过Java反射机制得到类加载器信息 //class1.getClassLoader().getClass().getName();
动态代理
代理模式是设计模式之一,代理模式有静态代理和动态代理,静态代理需要自己写代理类,实现对应的接口,比较麻烦。
在Java中,动态代理常见的又有两种实现方式:JDK动态代理和CGLIB代理。
JDK动态代理其实就是运用了反射的机制,而CGLIB代理则用的是利用ASM框架,通过修改其字节码生成子类来处理。
多线程基础
进程是资源分配的基本单位,线程是CPU调度的基本单位。
实际开发中用到多线程的例子:
系统中会用到生产者与消费者模式,会用多个线程去消费队列的消息,来提高并发度。
很多时候,我们判断是否要处理线程安全问题,就看有没有多个线程同时访问一个共享变量。
解决线程安全的思路:
1、能不能保证操作的原子性,考虑atomic包下的类够不够我们使用。
2、能不能保证操作的可见性,考虑volatile关键字够不够我们使用。
3、如果涉及到对线程的控制(比如一次能使用多少个线程,当前线程触发的条件是否依赖其他线程的结果),
考虑CountDownLatch/Semaphore等等。
4、如果是集合,考虑java.util.concurrent包下的集合类。
5、如果synchronized无法满足,考虑lock包下的类
总的来说就是看有没有线程安全问题,如果存在则根据具体的情况去判断使用什么方式去处理线程安全的问题。
造成死锁的愿意就是两个线程相互持有对方所需要的资源,而且都不放弃自己持有的资源。
避免死锁的方式:我们可以缩减加锁的范围,等到操作共享变量的时候才加锁;使用可释放的定时锁。
CAS
cas比较并交换,它是一个原子性操作,cas有三个操作数,当前值A,内存值V、要修改的新值B
假设当前值a与内存值v相等,那么将内存值v改成b;假设当前值a与内存值v不相等,要么重试,要么放弃更新。
将当前值与内存值进行对比,判断是否被修改过,这就是cas的核心。
synchronized锁每次只会让一个线程去操作共享资源,而cas相当于没有加锁,多个线程都可以直接操作共享资源,在实际去修改的时候
才去判断能否修改成功。
cas在很多情况下会比synchronized高效很多,比如对一个值进行累加,就没必要使用synchronized锁,使用juc包下的Atomic类就足够了。
cas会有aba问题,要解决aba问题,Java也提供了AtomicStampedReference类供我们用,说白类就是加个版本号,比对的是内存值+版本号是否一致。
synchronized
synchronized是一种互斥锁,一次只允许一个线程进入被锁住的代码块。
如果synchronized修饰的是实例方法,对应的锁是对象实例;
如果synchronized修饰的是静态方法,对应的锁是当前类的Class实例;
可见锁是一个实例(对象)
在内存中,对象一般由三个部分组成,分别是对象头、对象实际数据和对齐填充。
对象头又由几部分组成,但我们重点关注对象头的Mark Word的信息就好了。
Mark Word会记录对象关于锁的信息,又因为每个对象都会有一个与之对应的monitor对象,monitor对象存储着
当前持有锁的线程以及等待锁的线程队列。
了解Mark Word和monitor是理解synchronized原理的前提。
synchronized加锁是依赖底层操作系统的mutex相关指令实现,所以会有用户态和内核态之间的切换,性能损耗十分明显。
而jdk1.6以后引入偏向锁和轻量级锁在jvm层面实现加锁的逻辑,不依赖底层操作系统,就没有切换的消耗。
所以,mark word对锁的状态记录一共有4种:无锁、偏向锁、轻量级锁、重量级锁。
偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而
引入偏向锁。mark word会记录线程id,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。
一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。升级为轻量级锁。
轻量级锁请看上篇博客锁优化。
AQS&&ReentrantLock
公平锁:在锁竞争的情况下,先到临界区的线程比后到临界区的线程一定更早地获取到锁。
非公平锁:先到临界区的线程未必比后到临界区的线程更早地获取到锁。
为什么进队列呢?线程一直尝试获取锁不就行了吗?
一直尝试获取锁是自旋,需要耗费资源的。
多个线程一直在自旋,大多数都是竞争失败的,会耗费很多资源。
重量级锁通过monitor对象中的队列存储线程,但在进入线程之前,还是会尝试获取锁,当获取不到再进入队列中,
所以synchronized是非公平的。
AQS
AbstractQueuedSynchronizer
aqs是给我们实现锁的一个“框架”,内部实现的关键就是维护了一个先进先出的队列以及state状态变量。
先进先出队列存储的载体叫作Node节点,该节点标识着当前的状态值、是独占还是共享模式以及它的前驱和后继节点等等信息。
简单理解就是,aqs定义了一个模板,具体实现由各个子类完成。
总体流程是,把需要等待的线程以node的形式放在先进先出的队列中,state变量则表示当前锁的状态。
下面以reentrantlock为例讲一下加锁和解锁的过程:
以非公平锁为例,我们在外界调用lock方法的时候,源码是这样的:
1、CAS尝试获取锁,获取成功则可以执行同步代码。
2、CAS获取失败,则调用acquire方法。
3、acquire会调用子类的tryAcquire方法
4、tryAcquire方法实际上会判断当前的state是否等于0,等于0说明没有线程持有锁,则又尝试CAS直接获取锁。
5、如果cas获取成功,则可以执行同步代码块。
简化一下上面的就:当线程cas获取锁失败,当前线程入队列,把前驱节点状态设置为SIGNAL状态,并将自己挂起。
为什么要把前驱节点状态设置为SIGNAL状态?其实就是表示后继节点需要被唤醒。
解锁过程:
public void unlock() {
sync.release(1);
}
线程池:
怎么用线程池的呢?
用Executors去创建的吗?
不是的,我这边用的是ThreadPoolExecutor去创建线程池。
看阿里巴巴开发手册有提到不要用Executors去创建,
线程不是越大越好,如果线程设置过多,线程大量有上下文切换,这一部分也会带来系统的开销,这就得不偿失了。
ThreadLocal
通过ThreadLocal我们能够设置线程的局部变量,让每个线程都可以通过set/get来对这个局部变量进行操作。
不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap这么个内部类。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
为什么要把ThreadLocal作为key,而不是Thread做为key?