Part1 复习一下这几个方法:
Integer.toBinaryString(); //位图法,从海量数据中查找
记住Map的两个函数:
Object computeIfPresent(Object key,(key,value)->newValue); //key存在,oldValue!=null,计算新值并覆盖,但是新值为null,删除。
Object putIfAbsent(Object key,Object value); //key对应的value为null,删除
part2 .什么是自动装箱和拆箱(链接)
自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。原始类型byte, short, char, int, long, float, double 和 boolean 对应的封装类为Byte, Short, Character, Integer, Long, Float, Double, Boolean。
自动装箱和拆箱的原理
自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值。
Integer类型首先判断i值是否在-128和127之间,如果在-128和127之间则直接从IntegerCache.cache缓存中获取指定数字的包装类;不存在则new出一个新的包装类。
IntegerCache内部实现了一个Integer的静态常量数组,在类加载的时候,执行static静态块进行初始化-128到127之间的Integer对象,存放到cache数组中。cache属于常量,存放在java的方法区中。
通过分析源码发现,只有double和float的自动装箱代码没有使用缓存,每次都是new 新的对象,其它的6种基本类型都使用了缓存策略。使用缓存策略是因为,缓存的这些对象都是经常使用到的(如字符、-128至127之间的数字),防止每次自动装箱都创建一此对象的实例。
public class Test { public static void main(String[] args) { test(); } public static void test() { int i = 40; int i0 = 40; Integer i1 = 40; Integer i2 = 40; Integer i3 = 0; Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0); Double d1=1.0; Double d2=1.0; }
//1、这个没解释的就是true System.out.println("i=i0\t" + (i == i0)); //true //2、int值只要在-128和127之间的自动装箱对象都从缓存中获取的,所以为true System.out.println("i1=i2\t" + (i1 == i2)); //true //3、涉及到数字的计算,就必须先拆箱成int再做加法运算,所以不管他们的值是否在-128和127之间,只要数字一样就为true System.out.println("i1=i2+i3\t" + (i1 == i2 + i3));//true //比较的是对象内存地址,所以为false System.out.println("i4=i5\t" + (i4 == i5)); //false //5、同第3条解释,拆箱做加法运算,对比的是数字,所以为true System.out.println("i4=i5+i6\t" + (i4 == i5 + i6));//true //double的装箱操作没有使用缓存,每次都是new Double,所以false System.out.println("d1=d2\t" + (d1==d2));//false
part3 .Java并发编程之volatile关键字解析
普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。但是volatile不能保证原子性,原子性指的是一个操作或者多个操作要么全部执行并且执行过程不被其他因素所打断,要不全部不执行。但是对于i++这样的操作,包括读取i的值,加上1,写值三个部分,volatile只保证对于某一个线程的修改其他线程立刻可见。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
Part4 Spring的两种代理(使用的是代理模式)
先说一下AOP的概念,AOP从动态角度考虑程序运行过程,专门用于处理系统中分布于各个模块(不同方法)中交叉关注点的问题,能更好抽离出各个模块的交叉关注点。
java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP;2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP;
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换;
如何强制使用CGLIB实现AOP?
(1) 添加CGLIB库,SPRING_HOME/cglib/*.jar
(2) 在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
JDK动态代理和CGLIB字节码生成的区别?
(1) JDK动态代理只能对实现了接口的类生成代理,而不能针对类
(2) CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
因为是继承,所以该类或方法最好不要声明成final 。
part5: Spring中两种后处理器
Bean后处理器(接口):
BeanPostProcessor(两个方法:Object postProcessBeforeInitialization(Object bean,String name) 和 Object postProcessAfterInitialization((Object bean,String name))。
容器后处理器:接口:BeanFactoryPostProcessor(PropertyPlaceholderConfigurer PropertyOverrideConfigure)
Part6 maven
<dependencies>
<dependency>
<groupId>junit</groupId> <!--一般是域名反写-->
<artifactId>junit</artifactId> <!--项目名-->
<version>3.8.1</version> <!--Jar版本号-->
<scope>test</scope>
</dependency>
</dependencies>
part7 线程状态转移图
part8 线程和进程的区别
part9 yield和join的区别,执行完join之后放锁么?
wait()和sleep()的区别,执行完这两个方法之后放锁么?
简述:
1.sleep():在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。
sleep()使当前线程进入阻塞状态,在指定时间内不会执行。
2.wait(): 在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。
waite()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
3.yield(): 暂停当前正在执行的线程对象。yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。yield()只能使同优先级或更高优先级的线程有执行的机会。
4.join(): 等待该线程终止。等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。
注意Java并不限定线程是以时间片运行,但是大多数操作系统却有这样的要求。在术语中经常引起混淆:抢占经常与时间片混淆。事实上,抢占意味着只有拥有高优先级的线程可以优先于低优先级的线程执行,但是当线程拥有相同优先级的时候,他们不能相互抢占。它们通常受时间片管制,但这并不是Java的要求。
wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行, 只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还 在别人手里,别人还没释放。如果notify/notifyAll方法后面的代码还有很多,需要这些代码执行完后才会释放锁),调用wait方法的一个或多个线程就会解除wait状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。
1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。
2)当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能让当前线程阻塞,(这种阻塞是通过提前释放synchronized锁,重新去请求锁导致的阻塞,这种请求必须有其他线程通过notify()或者notifyAll()唤醒重新竞争获得锁)
3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;(notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁)
4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程,唤醒的线程获得锁的概率是随机的,取决于cpu调度。
(纠错:在main方法中 通过new ThreadTest(t).start()实例化 ThreadTest 线程对象, 它 通过 synchronized (thread) ,获取线程对象t的锁,并Sleep(9000)后释放,这就意味着,即使main方法t.join(1000)等待一秒钟,它必须等待ThreadTest 线程释放t锁后才能进入wait方法中,它实际等待时间是9000+1000ms❌。不是等待1s,而是main函数拿到锁之后只等待t线程执行1s中,如果1s之后t线程还没执行完,main函数就不等了):
t.join(1000); //等待 t 线程,等待时间是1000毫秒
class RunnableImpl implements Runnable { public void run() { try { System.out.println("Begin sleep"); Thread.sleep(2000); System.out.println("End sleep"); } catch (InterruptedException e) { e.printStackTrace(); } } } class ThreadTest extends Thread { Thread thread; public ThreadTest(Thread thread) { this.thread = thread; } @Override public void run() { synchronized (thread) { System.out.println("getObjectLock"); try { Thread.sleep(9000); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println("ReleaseObjectLock"); } } } public class JoinTest { public static void main(String[] args) { Thread t = new Thread(new RunnableImpl()); new ThreadTest(t).start(); t.start(); try { long start = System.currentTimeMillis(); t.join(1000); System.out.println("join time:"+(System.currentTimeMillis()-start)); System.out.println("joinFinish"); } catch (InterruptedException e) { e.printStackTrace(); } } }
结果:getObjectLock
Begin sleep
End sleep
ReleaseObjectLock
join time:9005
joinFinish
解析:join方法的源码中调用了wait方法,也就是说main函数调用t.join(1000)时,需要拿到t的锁,但是此时t的锁被synchronized锁住了,所以main方法拿不到就阻塞了,当ThreadTest执行完了之后,main函数才能拿到锁,然后最多等到t执行1秒。
如果把ThreadTest类run()中的synchronized注释掉,
结果:getObjectLock
Begin sleep
join time:1003
joinFinish
End sleep
ReleaseObjectLock
解析:因为没有synchronized握着t的锁,所以main函数在执行t.join(1000)的时候可以获得t线程的锁(对象本身),也就是说main函数等到t线程执行1000毫秒,如果t线程没有执行完,那么主线程也不管了,会正常和t线程竞争。
记住两点,容易混淆的地方:(1)哪个方法调用t.join(),那么该方法就会就会阻塞,等到t线程执行完之后才会继续执行,但是需要注意,这个方法必须拿到t的对象锁;
(2)t.join(1000)的意思是,调用还方法的线程在拿到t对象锁的情况下,最多等待t执行1秒,如果1秒之后t线程还没执行完,那么调用t.join(1000)的那个线程就会到就绪状态。
part9+死锁的产生原因以及怎么处理死锁?
synchronized是java的关键字,而Lock是一个接口;
synchronized在发生异常时,会自动释放所占有的锁,因此不会导致死锁,而Lock发生异常时,必须手动释放锁lock.unlock(),否则会发生死锁;
使用synchronized时,等到的线程会一直等待,不能响应中断,而Lock可以通过lockInterruptibly()方法获取锁,在未获取锁之前通过 线程.interrupt()方法中断该线程;
使用Lock可以判断当前线程是否获取到了锁,而synchronized却不能;
使用Lock可以提高多个线程的读写效率;
synchronized是一种互斥锁,Lock有可重入锁和读写锁。
2⃣️区分几种锁:
-- 独占锁
-- 可重入锁ReetrantLock: 如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
-- 可中断锁
-- 公平锁
-- 读写锁
3⃣️CAS和乐观锁
4⃣️Java程序死锁以及解决办法
5⃣️怎么查看哪里出现死锁
part10 voliate和synchronized区别
谈谈voliate(戳我)
同步
如用synchronized关键字,或者使用锁对象.
volatile
使用volatile关键字
用一句话概括volatile,它能够使变量在值发生改变时能尽快地让其他线程知道.
volatile详解
首先我们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操作会先在寄存器或者是CPU缓存上进行,最后才写入内存.
而在这个过程,变量的新值对其他线程是不可见的.而volatile的作用就是使它修饰的变量的读写操作都必须在内存中进行!
volatile与synchronized
1⃣️volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
2⃣️volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
3⃣️volatile仅能实现变量的修改可见性,但不具备原子特性,而synchronized则可以保证变量的修改可见性和原子性.
4⃣️volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
5⃣️volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化.
part11 ThreadLocal关键字
ThreadLocal的原理,每个Thread对象内部有个ThreadLocalMap,当线程访问ThreadLocal对象时,会在线程内部的ThreadLocalMap新建一个Entry,这样的话每个线程都有一个对象的副本,保证了并发场景下的线程安全。
ThreadLocal在web应用开发中是一种很常见的技巧,当web端采用无状态写法时(比如stateless session bean和spring默认的singleton),就可以考虑把一些变量放在ThreadLocal中。
例子1,以理解意思为主:你有两个方法A和B都要用到变量userId,又不想传来传去,一个很自然的想法就是把userId设为成员变量,但是在无状态时,这样做就很可能有问题,因为多个request在同时使用同一个instance,userId在不同request下值是不一样的,就会出现逻辑错误。但由于同一个request下一般都是处于同一个线程,如果放在ThreadLocal的话,这个变量就被各个方法共享了,而又不影响其他request,这种情况下,你可以简单把它理解为是一种没有副作用的成员变量。我理解ThreadLocal的使用场景是某些对象在多线程并发访问时可能出现问题,比如使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了,我们就可以用ThreadLocal<SimpleDataFormat>来解决并发修改的问题。
另一种场景是Spring事务,事务是和线程绑定起来的,Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离。
public abstract class TransactionSynchronizationManager {
//线程绑定的资源,比如DataSourceTransactionManager绑定是的某个数据源的一个Connection,在整个事务执行过程中
//都使用同一个Jdbc Connection
}
part12 当HashMap中链表过长,会转换成二叉树提高检索效率。
hashcode怎么计算?=>把八位的十六进制的逻辑内存转换成十进制。
part13 Http协议中GET与POST的区别
a) GET请求的数据会附在URL之后(就是把数据放置在HTTP协议头中),以?分割URL和传输数据,参数之间以&相连。POST把提交的数据则放置在是HTTP包的包体中。
b) GET传递的数据长度有限制,POST方式在理论上是没有大小限制的,可以传输大量数据。
c) POST的安全性要比GET的安全性高。因为GET方式传输的数据以明文的形式出现在URL中。
Http协议的状态:(200 – 服务器成功返回网页 404 – 请求的网页不存在 503 – 服务不可用 )
a) 2xx(成功),表示成功处理了请求。
b) 3xx(重定向),表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。
301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。 302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。305 (使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。
d) 4xx(请求错误),这些状态代码表示请求可能出错,妨碍了服务器的处理。
401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。403 (禁止) 服务器拒绝请求。404 (未找到) 服务器找不到请求的网页。
e) 5xx(服务器错误),这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误。
part14 线程池核心的概念
(1)handler:表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
这四个都是静态内部类,有下面的方法。
void |
part15 spring七个事务传播属性:
1.PROPAGATION_REQUIRED – 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
2.PROPAGATION_SUPPORTS – 支持当前事务,如果当前没有事务,就以非事务方式执行。
3.PROPAGATION_MANDATORY – 支持当前事务,如果当前没有事务,就抛出异常。
4.PROPAGATION_REQUIRES_NEW – 新建事务,如果当前存在事务,把当前事务挂起。
5.PROPAGATION_NOT_SUPPORTED – 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
6.PROPAGATION_NEVER – 以非事务方式执行,如果当前存在事务,则抛出异常。
7.PROPAGATION_NESTED – 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
备注:常用的两个事务传播属性是1和4,即PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW。
part16 MyBatis
MyBatis : SqlSessionFactoryBuilder -> SqlSessionFactory -> SqlSession
InputStream config = Resources.getResourcesAsStream("sqlMapconfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBUilder().build(config);
SqlSession session = factory.openSession();
Mybatis+Spring:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:org.sunny.*.xml"/>
</bean>
sql注入:SQL注入之所以发生,是因为我们碰到的是完全一样的问题:一个查询(一系列的指令)会有多个参数(数据)插入其中,而这些参数被当做指令执行从而导致异常。一个恶意的用户可以利用这样的漏洞来让数据库返回所有的用户的信息,很显然,这是不对的!
比如说,使用的是Statement,这相当于连接好字符串之后才进行sql编译,“select * from table where id”+condition;如果condition本来希望是=‘lily’,但是注入一个!=null,那么就会返回所有的信息。
解决办法:从前端来说,增加检查是否包含非法字符或者使用正则表达式过滤;后端,如果注入的是数据,尽量使用PreparedStatement,因为PreparedStatement可以预编译sql语句,不但执行速度快而且默认注入的是数据,防止sql注入,也可以使用正则表达式或者一些规则去过滤传进来的参数;myBatis中,#{}底层使用的是PreparedStatement,也就是说传进来的东西会默认当成数据处理,而${}底层使用的是Statement,比如定位一些表之类的参数就必须使用$。
PS:JDBC怎么连接
Class.forName("com.mysql.jdbc.Driver");//加载驱动
Connection conn = DriverManager.getConnection(url,user,password);
PreparedStatement ps = conn.prepareStatement(String sql);
或者Statement st = conn.createStatement();
part17:缓存
MyBatis的一级缓存是SqlSession级别的缓存,在操作数据库时需要构造SqlSession对象,在对象中有一个HashMap用于缓存数据,不同sqlSession之间的缓存数据区域是互不影响的。二级缓存指的是mapper级别的缓存,多个sqlSession使用同一个mapper的sql语句去操作数据库,得到的数据会存在二级缓存区域,同样使用HashMap缓存数据。
part18:JVM虚拟机的生命周期
--Bootstrap classloader
--Exetention classloader
--System classloader
--自定义的classloader
类的加载步骤:(1)加载 (2)连接:验证、准备、解析 (3)初始化
累加载机制:全盘负责;父类委托;缓存机制
part19:垃圾收集算法
(1)标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有标记的对象,标记过程在上面提过。
不足:1.效率问题,标记和清除两个过程效率不高
2.空间问题,标记清楚产生大量内存碎片
(2)复制算法:将内存安装容量分为大小相等的两块,每次使用其中一块。当回收时,将存活的对象复制到
为使用的内存中,清楚已使用过的内存。不足:内存缩小到运来的一半。
Java在新生代中采用这种算法,不过是将内存分为Eden和两块Survivor,每次使用Eden和其中的一块Suvivor,当回收时,将Eden和Suvivor存活的内存复制到未使用的Suvivor空间。HotSpot默认Eden与Suvivor比例为8:1,相当于可以使用90%的内存,如果存活的内存超过Suvivor空间,就是用老年代进行分配担保。
(3)标记-整理算法:过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后清理掉端边界以外的内存。
分代收集算法:将内存划分成几块,各个区域采用最适当的收集算法。Java一般把Java堆分为新生代和老年代,按各个年代特点采用适当的算法。
垃圾收集器:
新生代:Serial收集器,ParNew收集器,Parallel Scavenge收集器
老年代:CMS, Seral Old(MSC), Parallel Old收集器
GI收集器
新建对象优先放在Eden区分配内存,如果Eden区已满,在创建对象的时候,会因为无法申请到空间而触发minorGc操作,minorGc主要用来对年轻代垃圾进行回收,把eden中不能回收的对象放入到空的Survivor区,另一个Survivor区里不能被垃圾回收的对象也会被放入到这个Survivor区,这样能保证有一个Survivor区是空的。如果在这个过程中发现Survivor区的对象也满了,就会把这些对象复制到老年代,或者Survivor区没有满,但是有些对象已经存放非常长的时间,这些对象也会被放到老年代中,如果老年代也满了,就会出发fullGc。fullGc是用来清理整个堆空间的,包括年轻代和永久代。所以fullGc会造成很大的系统开销。Java8中已经移除了永久代,新加了一个称为元数据区的native的内存区,所以,大部分类的元数据都在本地内存中分配。
part20: Java四种引用
强引用(是指创建一个对象并把这个对象赋给一个引用变量);
软引用(SoftReference):如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。 SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之 后,get()方法将返回null。
弱引用(WeakReference):弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
虚引用(PhantomReference):仅用于在发生GC时接收一个系统通知。
part21:数据库的乐观锁和悲观锁?(数据库有两个线程在同时访问一行数据,一个线程写,一个线程读,可以读出来么?)
part22:Spring中使用了哪些设计模式
1.工厂模式,这个很明显,在各种BeanFactory以及ApplicationContext创建中都用到了;
2.模版模式,这个也很明显,在各种BeanFactory以及ApplicationContext实现中也都用到了;
3.代理模式,在Aop实现中用到了JDK的动态代理;
4.单例模式,这个比如在创建bean的时候。
5.Tomcat中有很多场景都使用到了外观模式,因为Tomcat中有很多不同的组件,每个组件需要相互通信,但又不能将自己内部数据过多地暴露给其他组件。用外观模式隔离数据是个很好的方法。
6.策略模式在Java中的应用,这个太明显了,因为Comparator这个接口简直就是为策略模式而生的。Comparable和Comparator的区别一文中,详细讲了Comparator的使用。比方说Collections里面有一个sort方法,因为集合里面的元素有可能是复合对象,复合对象并不像基本数据类型,可以根据大小排序,复合对象怎么排序呢?基于这个问题考虑,Java要求如果定义的复合对象要有排序的功能,就自行实现Comparable接口或Comparator接口.
7.原型模式:使用原型模式创建对象比直接new一个对象在性能上好得多,因为Object类的clone()方法是一个native方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。
8.迭代器模式:Iterable接口和Iterator接口 这两个都是迭代相关的接口,可以这么认为,实现了Iterable接口,则表示某个对象是可被迭代的;Iterator接口相当于是一个迭代器,实现了Iterator接口,等于具体定义了这个可被迭代的对象时如何进行迭代的。
危险!在HashMap中将可变对象用作Key
只要MutableKey 对象的成员变量改变,那么该对象的哈希值也改变了,所以该对象是一个可变的对象。
HashMap 的每个 bucket 里只有一个 Entry时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。
同时我们也看到,判断是否找到该对象,我们还需要判断他的哈希值是否相同,假如哈希值不相同,根本就找不到我们要找的值。如果Key对象是可变的,那么Key的哈希值就可能改变。在HashMap中可变对象作为Key会造成数据丢失。
在HashMap中使用不可变对象。在HashMap中,使用String、Integer等不可变类型用作Key是非常明智的。我们也能定义属于自己的不可变类。如果可变对象在HashMap中被用作键,那就要小心在改变对象状态的时候,不要改变它的哈希值了。我们只需要保证成员变量的改变能保证该对象的哈希值不变即可。
part24:复制implements Cloneable
import java.io.Serializable;
public class BinaryNode<T> implements Serializable,Cloneable{
private T data;
private BinaryNode<T> rightChild;
private BinaryNode<T> leftChild;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public BinaryNode<T> getRightChild() {
return rightChild;
}
public void setRightChild(BinaryNode<T> rightChild) {
this.rightChild = rightChild;
}
public BinaryNode<T> getLeftChild() {
return leftChild;
}
public void setLeftChild(BinaryNode<T> leftChild) {
this.leftChild = leftChild;
}
@Override
protected BinaryNode<T> clone() throws CloneNotSupportedException {
BinaryNode<T> newNode = (BinaryNode<T>) super.clone();
newNode.leftChild = newNode.leftChild.clone();
newNode.rightChild = newNode.rightChild.clone();
return newNode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BinaryNode<?> that = (BinaryNode<?>) o;
if (data != null ? !data.equals(that.data) : that.data != null) return false;
if (rightChild != null ? !rightChild.equals(that.rightChild) : that.rightChild != null) return false;
return leftChild != null ? leftChild.equals(that.leftChild) : that.leftChild == null;
}
@Override
public int hashCode() {
int result = data != null ? data.hashCode() : 0;
result = 31 * result + (rightChild != null ? rightChild.hashCode() : 0);
result = 31 * result + (leftChild != null ? leftChild.hashCode() : 0);
return result;
}
}