<< >>运算符
<<表示左移移,不分正负数,低位补0;
负数在机器中都是补码,因此左移右移都i是根据补码进行。
负数:r = -20 << 2
-20 的二进制原码 :1001 0100
-20 的二进制反码 :1110 1011
-20 的二进制补码 :1110 1100
左移两位后的补码:1011 0000 (逻辑右移,低位补零)
反码:1010 1111
原码:1101 0000
结果:r = -80
>>表示右移,如果该数为正,则高位补0,若为负数,则高位补1;
正数:r = 20 >> 2
20的二进制补码:0001 0100
向右移动两位后:0000 0101
结果:r = 5
负数:r = -20 >> 2
-20 的二进制原码 :1001 0100
-20 的二进制反码 :1110 1011
-20 的二进制补码 :1110 1100
右移两位后的补码:1111 1011 (负数高位补1)
反码:1111 1010
原码:1000 0101
结果:r = -5
>>>表示无符号右移,不管正数负数都是高位补0
注解
@Documented
@Documented注解表明这个注释是由 javadoc记录的,在默认情况下也有类似的记录工具。 如果一个类型声明被注释了文档化,它的注释成为公共API的一部分
@RequestParam
@RequestParam
接收的参数,
- 一可以是来自requestHeader中,即请求头。
- 二可以是**
Content-Type
为application/x-www-form-urlencoded
编码的内容,Content-Type
默认为该属性,也就是body里的参数。**
所以RequestParam可以用于get,post,delete,put等各种方法。但是post,put
不支持批量插入数据,如果改用 json
字符串来传值的话,类型设置为 application/json
,点击发送的话,会报错,后台接收不到值,为 null
。
@RequestBody
@RequestBody
接收的参数是来自requestBody中,即body。一般用于处理非 Content-Type: application/x-www-form-urlencoded
编码格式的数据,比如:application/json
、application/xml
等类型的数据。
GET请求中,因为没有HttpEntity,所以@RequestBody并不适用,一般用于Post,put方法。
POST请求中,通过HttpEntity传递的参数,必须要在请求头中声明数据的类型Content-Type,否则默认是application/x-www-form-urlencoded
Mybatis
@Interceptor
作用:拦截某个方法,在此方法进行前,先进行某些行动。
@Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = { Statement.class }) })
@signature
,含有三个参数:
type :选择拦截的类,只有四种选择: Executor、StatementHandler、ParameterHandler 和 ResultSetHandler
,也就是不是这四种类型的,它不会拦截。
method:选择的type中某类方法。
args:根据方法参数,确定唯一的方法。(因为有重载的存在)。
需要继承Interceptor
接口,随后写入Mybatis配置文件中
拦截器顺序
1 不同拦截器顺序: Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
2 对于同一个类型的拦截器的不同对象拦截顺序: 在 mybatis 核心配置文件根据配置的位置,拦截顺序是 从上往下
<plugin interceptor="com.demo.mybatis.study.ResultSetHandlerInterceptor">
<property name="dialect" value="com.demo.mybatis.study.ialect.MySQLDialect"/>
</plugin>
传入的Invocation
是什么,其实就是我们要拦截对象的一个代理。在这里就是ResultSetHandler
的代理类
@Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = { Statement.class }) })
public class ResultSetHandlerInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
//从代理类中得到拦截对象
ResultSetHandler resultSet = (ResultSetHandler) invocation.getTarget();
//在这里,利用方法工具,得到此类resultSet某些字段,随后可以进行合法判断,或者写入日志等操作
//假设这里有一大串代码
return invocation.proceed();//invocation.proceed(),启动被拦截的方法。
}
// Mybatis只能拦截四种类型的接口:Executor、StatementHandler、ParameterHandler 和 ResultSetHandler, 通过@Intercepts和@Signature} 两个注解共同完成。
//试想一下,开发人员在没有指明类型,或者随便写一个拦截点,比如Object,那Mybatis疯了,难道所有对象都去拦截,所以Mybatis会做一次判断,拦截点看看是不是这四个接口里面的方法,是的话,才会进行拦截
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//mybatis 配置文件读取properties,也就是上面写的<property name="dialect" value="com.demo.mybatis.study.ialect.MySQLDialect"/>
public void setProperties(Properties properties) {
}
}
Plugin
的wrap
方法,判断依据是利用反射,获取这个拦截器ResultSetHandler
的注解 Intercepts
和Signature
,然后解析里面的值,它判断当前目标对象是否有实现对应需要拦截的接口,
- 如果没有则返回目标对象本身,也就是不拦截。
- 如果有,则拦截,返回一个代理对象。而这个代理对象的
InvocationHandler
正是一个Plugin。所以当目标对象在执行接口方法时,通过代理对象执行的,调用对应InvocationHandler的invoke方法,也就是Plugin的invoke方法。
这里invoke方法的逻辑是:把目标对象、要执行的方法以及方法参数封装成一个Invocation对象,再把封装好的Invocation作为参数传递给当前拦截器的intercept方法,也就是我们的拦截器中传入的参数Invocation。如果不需要拦截,则直接调用当前的方法。Invocation中定义了定义了一个proceed方法,其逻辑就是调用当前方法,所以如果在intercept中需要继续调用当前方法的话可以调用invocation的procced方法。
StatementHandler
StatementHandler
为一个接口,有一个实现类:RoutingStatementHandler
为路由类,它内部持有一个private final StatementHandler delegate
,构造函数会选择SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler
其中一个实现,RoutingStatementHandler
的query、update等方法都是调用delegate的方法。
StatementHandler
还有一个抽象类BaseStatementHandler
,它共有3个实现类,也就是上面三个类。
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
我们一般使用PreparedStatementHandler,他会处理PreparedStatement
。
BaseStatementHandler
拥有一个BoundSql
字段,直译就是受约束的SQL。BoundSql语句的解析主要是通过对#{}
字符的解析,将其替换成?
。最后均包装成预表达式供PrepareStatement调用执行。#{}中的key属性以及相应的参数映射,均保存至BoundSql的List<parameterMapping>
属性中供最后的预表达式对象PrepareStatement赋值使用
@ControllerAdvice
ControllerAdvice有三个使用场景:
- 全局异常处理
@ExceptionHandler(value = MethodArgumentNotValidException.class)
- 全局数据绑定
@ModelAttribute
把参数添加到一个全局Model
中,其中 value 属性表示这条返回数据的 key,而方法的返回值是返回数据的 value。用于添加一些通用属性,添加后,在controller中参数输入model,便可以根据key选择需要的属性。
@ControllerAdvice(annotations = {Controller.class, RestController.class})
public class WebControllerAdvice {
@ModelAttribute(value = "param")
public String m(){
return "123435";
}
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("msg", "欢迎访问 csdn.cd");
HashMap<String, String> map = new HashMap<>();
map.put("name", "li");
map.put("age", "20");
model.addAttribute("info", map);
}
}
@RestController
public class HelthCheckController {
@GetMapping
public String health(@ModelAttribute("param") String param){
return param;
}
}
- 全局请求数据预处理
@InitBinder
。将请求参数进行预处理,处理完在输入给controller。一般有两个作用:
1、当多个不同实体的多个参数名称相同时,使用前缀进行区分
2、使用@InitBinder格式化Date类型参数。在实际开发中后端某个参数是Date类型,而前端传回来的值是String类型,这时后端接参时不会自动转化为Date类型。
setFieldDefaultPrefix()
的参数是url参数的前缀,然后@InitBinder("ccb")
的ccb是传给controller的参数的前缀
@InitBinder("ccb")
public void b(WebDataBinder binder) {
binder.setFieldDefaultPrefix("b.");
}
@InitBinder("cca")
public void a(WebDataBinder binder) {
binder.setFieldDefaultPrefix("a.");
}
@PostMapping("/book")
public void addBook(@ModelAttribute("ccb") Book book, @ModelAttribute("cca") Author author) {
System.out.println(book);
System.out.println(author);
}
传入的url
基础知识
堆、栈
引用变量:
当变量指向一个对象时,这个变量就被称为引用变量,比如
A a =new A();
a就是引用变量,它指向了一个A对象,也可以说它引用了一个A对象。我们通过操纵这个a来操作A对象。 此时,变量a的值为它所引用对象的地址。A是new 的,所以是在堆上的。
在方法内部声明,当该方法运行完时,内存即被释放。
成员变量:
通过new建立,只要还有引用,那么就不会被GC。
从系统的角度来说,声明局部变量有利于内存空间的更高效利用(方法运行完即回收)。
成员变量可用于各个方法间进行数据共享。
java类型转换
我们经常看到将一个方法的返回值object强转为某个类型,其实这个object就是具体的某类型,因此才可以这样强转。运行下面代码,可以看到object其实为integer。
public static void main(String[] args) {
Object object=new Integer(155);
Integer integer = (Integer) good();
System.out.println("helloworld");
}
public static Object good() {
return new Integer(15);
}
list.toarray() //不带参数时,转化为object[]
list.toarray(new Integer[list.size()]) //带参数时,转为为对用参数的类型(不能时string)
Object里拥有的方法、
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
protected void finalize() throws Throwable { }
我们可以看到,因为java.lang.Object类里已有public方法.toString(),所以对任何严格意义上的java对象都可以调用下面方法。但在使用时要注意,必须保证object不是null值,否则将抛出NullPointerException异常。采用这种方法时,通常派生类会覆盖Object里的toString()方法。
1 Object object = getObject();
2 System.out.println(object.toString());
但是使用下面方法时,这是标准的类型转换,将object转成String类型的值。使用这种方法时,需要注意的是类型必须能转成String类型。因此最好用instanceof做个类型检查,以判断是否可以转换。否则容易抛出CalssCastException异常。比如下面就会报错
Object obj = new Integer(100);
String strVal = (String)obj;
使用Interger
进行比较时,千万不能用==
,因为==
比较的是对象地址,在[-128,127]的数会被cache缓存,此时数值相同,==就是true,但是超过这个范围就是false.
向上转型
public class haha {
public static void main(String[] args) {
person person_1 =new student("hah",18);
System.out.println(person_1.getClass());
System.out.println(person_1.getClass().getDeclaredFields()[0]);
System.out.println("helloworld");
}
}
class person{
private String name;
public person(String name) {
this.name = name;
}
}
class student extends person{
public Integer age;
public student(String name, Integer age) {
super(name);
this.age = age;
}
}
结果为
很明显,person_1还是student。
向上转型中,class并没有发生变化,字段,方法均没有减少,仅仅是如果直接在父类中访问子类的字段/方法,那么是过不了编译的。
& && | || 短路
String test=null;
System.out.println(test==null & test.equals(""));
结果报NullPointerException
,说明&
为非短路,也就是&
两边的条件都会判断,而&&
则是短路的,第一个为false则直接返回false,不判断第二个,同理|
与||
也是一样的。
&&
的执行优先级,即是执行顺序高于||
System.out.println( true || true && false ); //true
明显得出,&&是优于||执行的。
&&
和||
多个连接起来时,则没有短路了,全部都会判断。
String test=null;
System.out.println( false && false || test.equals("") ); //NullPointerException
String test=null;
System.out.println( false && false && test.equals("") ); //false
明显,第一个判断到了最后一个条件,而仅仅&&
连接时,发生短路。因此有些时候,我们需要用将||
用( )
抱起来,保证短路发生。
享元
包装类型如Byte
、Integer
都是不变类,因此,反复创建同一个值相同的包装类型是没有必要的。以Integer
为例,如果我们通过Integer.valueOf()
这个静态工厂方法创建Integer
实例,当传入的int
范围在-128
~+127
之间时,会直接返回缓存的Integer
实例;对于Byte
来说,因为它一共只有256个状态,所以,通过Byte.valueOf()
创建的Byte
实例,全部都是缓存对象。
Integer i = 1 ; 这种写法叫做装箱(基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成),而装箱操作是通过 Integer.valueOf(1) 完成的,所以:
Integer i = 1 等同于 Integer.valueOf(1)
toString和String.valueOf
1.相同点
- Object中有public方法toString,因此任何一个对象都能调用toSring
- valueOf的参数是Object,可以传入任何一个对象
- 两者返回类型都是String,均能够将一个对象转化为String
2.区别
- 当对象为null时,调用toString会报java.lang.NullPointerException
- 当对象为null时,调用String.valueOf()会返回"null"字符串
关键字
transient
java 的transient
关键字,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
静态变量不管是不是transient关键字修饰,都不会被序列化。
因为静态变量在全局区,本来流里面就没有写入静态变量,我打印静态变量当然会去全局区查找,而我们的序列化是写到磁盘上的,所以JVM查找这个静态变量的值,是从全局区查找的,而不是磁盘上。被写到了全局区,其实就是方法区,只不过被所有的线程共享的一块空间。
集合
Linkedlist
Linkedlist
,双向链表,优点,增加删除,用时间很短,但是因为没有索引,对索引的操作,比较麻烦,只能循环遍历,但是每次循环的时候,都会先判断一下,这个索引位于链表的前部分还是后部分,每次都会遍历链表的一半 ,而不是全部遍历。
双向链表,都有一个previous和next, 链表最开始的部分都有一个fiest和last 指向第一个元素,和最后一个元素。增加和删除的时候,只需要更改一个previous和next,就可以实现增加和删除,所以说,LinkedList对于数据的删除和增加相当的方便。
多线程
线程安全
- 如果一个类被设计为允许多线程正确访问,比如对方法加锁,我们就说这个类就是线程安全的(thread-safe)。Java标准库的
java.lang.StringBuffer
也是线程安全的。 - 一些不变类,例如
String
,Integer
,LocalDate
,它们的所有成员变量都是final
,多线程同时访问时只能读不能写,这些不变类也是线程安全的。 - 类似
Math
这些只提供静态方法,没有成员变量的类,也是线程安全的。
除了上述几种少数情况,大部分类,例如ArrayList
,都是非线程安全的类,我们不能在多线程中修改它们。
而线程安全不是多线程协调运行,多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务
synchronized
synchronized
是Java语言层面提供的语法,所以我们不需要考虑异常,而ReentrantLock
是Java代码实现的锁,我们就必须先获取锁,然后在finally
中正确释放锁
static synchronized
- synchronized 锁的是this对象
- static synchronized 锁的是 当前类的Class对象
两个锁毫不相干,双方都不影响,static方法的锁是static method之间共享,static方法需要等到上一个static方法把锁释放才能获得锁。
wait()与notify()
notify()
notifyAll()
将唤醒所有当前正在this
锁等待的线程notify()
只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。- 通常来说,
notifyAll()
更安全。有些时候,如果我们的代码逻辑考虑不周,用notify()
会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。
wait()
wait()
方法的执行机制非常复杂,它不是一个普通的Java方法,而是定义在Object
类的一个native
方法,必须在synchronized
块中才能调用wait()
方法。- 要始终在
while
循环中wait()
,并且每次被唤醒后拿到this
锁就必须再次判断:
while (queue.isEmpty()) {
// 释放this锁:
this.wait();
// 重新获取this锁
}
死锁
由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized
块,记录-1,减到0的时候,才会真正释放锁。
死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待。
避免死锁的方法是多线程获取锁的顺序要一致。
线程的状态
┌─────────────┐
│ New │
└─────────────┘
│
▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
┌─────────────┐ ┌─────────────┐
││ Runnable │ │ Blocked ││
└─────────────┘ └─────────────┘
│┌─────────────┐ ┌─────────────┐│
│ Waiting │ │Timed Waiting│
│└─────────────┘ └─────────────┘│
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│
▼
┌─────────────┐
│ Terminated │
└─────────────┘
当线程启动后,它可以在Runnable
、Blocked
、Waiting
和Timed Waiting
这几个状态之间切换,直到最后变成Terminated
状态,线程终止。
Blocked
:A thread that is blocked waiting for a monitor lock is in this state.
Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling
Object.wait
.
WAITING
A thread that is waiting indefinitely for another thread to perform a particular action is in this state.A thread is in the waiting state due to calling one of the following methods:
Object.wait
with no timeoutThread.join
with no timeoutLockSupport.park
A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called
Object.wait()
on an object is waiting for another thread to callObject.notify()
orObject.notifyAll()
on that object. A thread that has calledThread.join()
is waiting for a specified thread to terminate.
ReentrantLock
ReentrantLock
是Java代码实现的锁,我们就必须先获取锁,然后在finally
中正确释放锁。同时需要 private final Lock lock = new ReentrantLock()
。
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()
返回false
,程序就可以做一些额外处理,而不是无限等待下去。
所以,使用ReentrantLock
比直接使用synchronized
更安全,线程在tryLock()
失败的时候不会导致死锁。
condition
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition()//使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回
Condition
提供的await()
、signal()
、signalAll()
原理和synchronized
锁对象的wait()
、notify()
、notifyAll()
是一致的,并且其行为也是一样的
和tryLock()
类似,await()
可以在等待指定时间后,如果还没有被其他线程通过signal()
或signalAll()
唤醒,可以自己醒来:
while(queue.isEmpty()){
if (condition.await(1, TimeUnit.SECOND)) {
// 被其他线程唤醒后的代码
} else {
// 指定时间内没有被其他线程唤醒后的代码
}
}
ReadWriteLock
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
read | write | |
---|---|---|
read | yes | no |
write | no | no |
StampedLock 乐观读锁
读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
StampedLock
是不可重入锁。
private final StampedLock stampedLock = new StampedLock(); //需要初始化一个stampedLock
long stamp = stampedLock.writeLock(); // 获取写锁,stamp为写锁版本号
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁,不需要解锁
stampedLock.validate(stamp) // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 假如有写锁发生,获取一个悲观读锁
CAS Compare and Set(乐观写锁)
Atomic
类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。
public int incrementAndGet(AtomicInteger var) {
int prev, next;
do {
prev = var.get();
next = prev + 1;
} while ( ! var.compareAndSet(prev, next));
return next;
//如果AtomicInteger的当前值是prev,那么就更新为next,返回true。
//如果AtomicInteger的当前值不是prev,就什么也不干,返回false。
//通过CAS操作并配合do ... while循环,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的。
}
线程池
接口:ExecutorService
FixedThreadPool
:线程数固定的线程池;CachedThreadPool
:线程数根据任务动态调整的线程池;SingleThreadExecutor
:仅单线程执行的线程池。
ExecutorService es = Executors.newFixedThreadPool(4); //返回ThreadPoolExecutor
接口 :ScheduledExecutorService
ScheduledThreadPool
:任务本身固定,需要反复执行的
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
Executors.newFixedThreadPool(4)
返回的是ThreadPoolExecutorThreadPoolExecutor
,他extendsAbstractExecutorService
,是个抽象类AbstractExecutorService
,里面实现了submit
方法
随后它又implement
ExecutorService
,他定义了线程池的一系列抽象方法。
Future 与callable
Runnable
的方法没有返回值。Callable
,它可以有返回值,并且是一个泛型接口,可以返回指定类型的结果。
// 定义任务:
Callable<String> task = new Task();
// 提交任务并获得Future:
Future<String> future = executor.submit(task);
线程池传入Runable,也会有结果,但是不是计算的结果,而是我们在submit中传入的参数,结果是固定的,在内部FutureTask将会其转为Callable
一个Future<V>
接口表示一个未来可能会返回的结果,它定义的方法有:
-
get()
:获取结果(假如异步任务还没有完成,那么get()
会阻塞,直到任务完成后才返回结果)。 -
get(long timeout, TimeUnit unit)
:获取结果,但只等待指定的时间; -
cancel(boolean mayInterruptIfRunning)
:取消当前任务; -
isDone()
:判断任务是否已完成。 -
cancel(boolean mayInterruptIfRunning)
如果任务已经取消、已经完成或者其他原因不能取消,返回false。
如果任务还没有启动就调用了cancel(true),任务将永远不会被执行。
如果任务已经启动,参数mayInterruptIfRunning将决定任务是否应该中断执行该任务的线程,以尝试中断该任务。
FutureTask
AbstractExecutorService
,里面实现了submit
方法,可以看到返回的是FutureTask
,它是Future的唯一实现类
public class FutureTask<V> implements RunnableFuture<V> {
private Callable<V> callable; //
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; //它内部持有Callble,也就是会将Runnable转为Callble,然后那个Adapet其实就是把你传入的参数直接当作为结果输出,其实没什么意义
}
···
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
FutureTask
implements RunnableFuture
,这东西同时实现了Runnable, Future,说明可以直接FutureTask
交由Executor执行,并无需接住submit的返回值
Callale<Integer> task = new Task();
FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
executor.submit(futureTask);
为什么并无需接住submit的返回值?
FutureTask是建立在堆中的。submit(task),new 了一个FutureTask。注意该函数的返回值,为无定界符,不能读,不能写,其实就是没有用。
该ftask中的
Callable
字段(子类,原本的Callable没有字段的)如图。execute()后,调用ftask的Callable.call()
,即调用了task.run()
,run()结束后将结果返回给task.outcome
,而ftaskoutcome
字段因为传入参数为null,从而永远为空。因为FutureTask是建立在堆中的,从而主线程才能读到task,更新Outcome值。
想一想如果FutureTask是存在栈里,那它怎么从别的线程返回结果给主线程呢。
FutureTask在Callable运行结束后,会将字段callable设置为null。
CompletableFuture
创建一个CompletableFuture
是通过CompletableFuture.supplyAsync()
实现的,它需要一个实现了Supplier
接口的对象:
public interface Supplier<T> {
T get();
}
用lambda语法简化了一下,直接传入Main::fetchPrice
。紧接着,CompletableFuture
已经被提交给默认的线程池执行了。
完成时,CompletableFuture
会调用Consumer
对象:
public interface Consumer<T> {
void accept(T t);
}
异常时,CompletableFuture
会调用Function
对象:
public interface Function<T, R> {
R apply(T t);
}
**使用Future,还需要再主线程主动使用get(),**因此可见CompletableFuture
的优点是:
- 异步任务结束时,会自动回调某个对象的方法;
- 异步任务出错时,会自动回调某个对象的方法;
- 主线程设置好回调后,不再关心异步任务的执行。
串行执行:
thenApplyAsync
,等待上一个线程执行成功后才执行此线程并发执行:
anyOf()
可以实现“任意个CompletableFuture
只要一个成功”,allOf()
可以实现“所有CompletableFuture
都必须成功”,用他们将两个CompletableFuture
合并为一个新的CompletableFuture
ThreadLocal
static final ThreadLocal<String> ctx = new ThreadLocal<>();
ThreadLocal
相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal
关联的实例互不干扰。
void processUser(user) {
try {
threadLocalUser.set(user);
step1();
step2();
} finally {
threadLocalUser.remove();
}
}
ThreadLocal
一定要在finally
中清除:当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal
没有被清除,该线程执行其他代码时,会把上一次的状态带进去。
volatile
Java内存模型告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。
volatile
作用:
- Lock前缀的指令会引起处理器缓存写回内存;
- 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
- 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
反射
用instanceof
不但匹配指定类型,还匹配指定类型的子类
Integer n = new Integer(123);
boolean b1 = n instanceof Integer; // true,因为n是Integer类型
boolean b2 = n instanceof Number; // true,因为n是Number类型的子类
JVM为每一种基本类型如int也创建了Class
,通过int.class
访问。
// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
String s = (String) cls.newInstance();
它的局限是:只能调用public
的无参数构造方法。带参数的构造方法,或者非public
的构造方法都无法通过Class.newInstance()
被调用。
Field
一个Field
对象包含了一个字段的所有信息:
getName()
:返回字段名称,例如,"name"
;getType()
:返回字段类型,也是一个Class
实例,例如,String.class
;getModifiers()
:返回字段的修饰符,它是一个int
,不同的bit表示不同的含义,可以用其判断是否为Public、Final等。f.get(Object)
获取指定实例的指定字段的值,object为某个具体类。f.set(p, "Xiao Hong");
设置某个具体类的指定字段值。get(),set()
无法直接访问private字段,若要访问private,需要在之前加上这一句。
f.setAccessible(true); //别管这个字段是不是public,一律允许访问
//setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。
Method
public Method getMethod(String name,Class<?>... parameterTypes)
一个Method
对象包含一个方法的所有信息:
-
getName()
:返回方法名称,例如:"getScore"
; -
getReturnType()
:返回方法返回值类型,也是一个Class实例,例如:String.class
; -
getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
; -
getModifiers()
:返回方法的修饰符,它是一个int
,不同的bit表示不同的含义。String s = "Hello world"; Method m = String.class.getMethod("substring", int.class); // 获取String substring(int)方法,参数为int: String r = (String) m.invoke(s, 6); // 在s对象上调用该方法并获取结果: Stringr n = (String) m.invoke(null, "12345"); // 调用该静态方法并获取结果:
同样需要使用 m.setAccessible(true)
调用非public方法。
使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。
调用构造方法
两种办法
-
Class.newInstance()
Person p = Person.class.newInstance();
调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
-
Constructor
通过Class实例获取Constructor的方法:
getConstructor(Class...)
:获取某个public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
。
注意Constructor
总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非public
的Constructor
时,必须首先通过setAccessible(true)
设置允许访问。setAccessible(true)
可能会失败。
类加载器ClassLoader
getClass().getClassLoader()
,getClassLoader():取得该Class对象的类装载器
一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。