admin 管理员组

文章数量: 887028

JAVAJAVA

java常见的集合及特性

hashMap 允许key value 为null, 无序,值 1.7数组+链表 ,线程不安全

hashTable 不允许keyvalue不允许为null,所有方法都加了synchronized,效率低,底层也是链表

hashset 底层是hashmap,value相同,允许是用null元素,不能保证元素顺序

linkedList 链表 不安全的

linkedHashMap, 可以理解为有序6hashmap,插入的时候会建立一个横线的链表接口

ArrayList 底层是一个数组,顺序添加,自动扩容为当前数组的1.5倍,删5操作时把删除的下一个节点的时候到最后一个数据往前移动一位(copy),移动后队尾设置为null方便回收。set(index,value)时获取index的oldvalue然后存入新的value

ArrayList的扩容机制

  1. arrayList扩容会创建一个新的数组,其长度是当前数组长度的1.5倍,将原数组中的数据复制到新数组中。
  2. 为什么不采用在原数组上进行扩容,原因是根据数组的特点,当一个数组创建完,其长度是不能改变的。

ArrayList和LinkedList的区别

  1. ArrayList底层使用的是数组,支持动态扩容,当内存不足时会自动扩容当前数组长度的1.5倍,随机查询速度快,这里的随机查询指的是指定下标查询,增删慢
  2. LinkedList底层是双向链表,查询慢只能一个一个遍历,增删快只需修改前后指针。

hashMap1.7和1.8之间区别

  • 1.7,1.7采用的方式为数组+链表存储,默认初始化大小16,负载因子0.75,采用头插法
    • Map map = new HashMap(),调用hashMap构造方法时会创建一个长度为0的数组,当进行put操作时才会真正的进行初始化操作,即延迟初始化。
    • put:当添加时,会先判断table的长度是否为0,如果为0则进行初始化长度为16,然后判断key是否为null如为null则,将key存到数组的第一个位置,如果不为null,通过key的hash值与数组的长度-1按位与运算,(得到的结果作为数组的下标),存放到对应的数组下标上,然后判断当前的位置是否为空,如果为空则创建链表放在链表头上 ,如果不为空则判断链表中是否有相同的key,有的话就替换value,并将旧value返回,如果key不相同,则把新插入进来的key放到链表的头位置,之前的链表放在新插入的key后面,1.8以后改为尾插法。
    • get:get时,通过key计算hashcode 与数组进行模运算,计算出在数组中的位置,然后用key与链表中的key相比较,相同的就返回。查询的时间复杂为为O(N)
    • 扩容:当数组长度大于(当前长度*负载因子)时进行扩容操作,将长度变为当前数据的长度*2,扩容后将原数组的值重新计算hashcode%数组长度,然后放到计算后的位置,这是之前在链表末尾数据,会存到链表的头位置
    • containskey:流程与get方法大致相同
    • containsvalue:暴力查找所有数组中的数据,效率极低
  • 1.8,1.8采用数组+链表+红黑树,当链表长度大于8时将链表转换为红黑树
    • put:put方法与1.7的不同之处是,流程中会判断当前存储的是否为红黑树,如果为红黑树则进行红黑树的插入操作。
    • get:判断是否为红黑树节点如果是就进行红黑树查找,其查找的时间复杂度为O(logN)
    • 扩容:与1.7不同之处在于,扩容时不在重新计算hashcode,而是通过hashcode的高16位异或低16位,不存在1.7扩容时链表顺序倒置的情况
    • 为什么在8的时候扩容
      • 因为一个链表长度为8的几率只有0.00000006。

hashmap树化的条件和从树退化的条件

  1. 每一颗树的结点数小于等于临界值 6 时退化成链表
  2. remove时,如果红黑树根root为空,或者root的左子树/右子树为空,或者root.left.left根的左子树的左子树为空,红黑树会退化成链表

concurrentHashmap 1.7和1.8之间区别

  • 1.7,concurrentHashMap采用分段锁确保可以在多线程下保证线程安全 ,其原理是在每个segment中使用ReentrantLock进行加锁,这样做的好处是,锁只在当前的segment中生效,不同的线程可以同时访问不同的segment。segment的数据结构为sengment数组+entry数组,segment虽然默认长度为16,但初始化时只有segment[0],其他槽位当使用时才会被创建
    • put:put方法交由sengment操作,先进性加锁,后续操作其工作原理与hashmap大致相同。
    • get:get方法不用加锁,流程与hashmap大致相同,说一下为什么不用加锁,因为value是由volatile修饰的,volatile可以保证可见性
    • 扩容:发生扩容时,只对当前的segment进行扩容,其余的segment不变以免浪费资源
    • size:获取size时需要对整个concurrentHashMap进行加锁,在加锁之前,会去先尝试两次不加锁的获取size,原因时当有put、remove等操作时,concurrentHashMap会有一个计数的操作,mothedMod+1,如果获取size后发现mothedmod没有发生变化则表示获取成功,如果发生了变化则进行concurrentHashMap全局加锁获取size。
    • concurrenthashmap的value值不能存null,原因如下:
      • 原因一:假设可以存入null值,有这样一个场景,A线程调用concurrentHashMap.containsKey(key),我们期望返回fasle,但在我们调用调用containsKey之后,未返回结果钱,线程B存入了null,put(key,null);,那么线程A最终的返回结果就是true了。这样就引发的歧义。
      • 原因二:因为containsKey中的实现为 return get(key)!=null; 如果返回get返回的是null,那么null!=null则等式结果为false,所以说value还是不能为null。
  • 1.8,在1.8中取消了segment和分段锁,不在使用reentrantLock,而是引用了cas+synchronized,结构变为(数组+链表+红黑树),node中的每个根节点加入了synchronized,保证每个线程可以操作一条链表或红黑树,其中node时用volatail修饰的,保证可见性与防止指令重排序。
    • get:根据key计算hash值,然后找到node的对应节点,如果此时正在扩容,则会进入ForwardingNode的find方法,去nextTable中查找,nextTable为扩容时新的nodetable,如果都不符合就查找其他节点,都不符合就返回null
    • put:根据key计算hash值,判断节点是否为空,如果为空以cas的方式在对应的位置添加一个节点,随后会判断node是否为ForwardingNode,如果是正在扩容,需要请求当前访问线程去协助扩容操作。如无需扩容则判断节点是否为链表如果是链表就按照链表的方式保存value,保存后判断链表长度是否大于8,大于8转红黑树,如果节点为红黑树,则按红黑树的方式去存value
    • 扩容:新创建一个nextTable,遍历原table,将其中的数据根据规则写入到新的table中,创建过程只能由一个线程来操作,转移的过程可以由多个线程来操作。

hashmap为什么是2的指数幂

  1. 确保后几位是连续的1,这样在按位与长度按位与(按位与,全为1才是1)的时候,可确保一定会落在数组上,如果后几位是1001&0110,那么会浪费存储空间,并增加hash碰撞

hashmap不安全的地方

在jdk1.7中,由于hashmap使用的是头插法,在多线程下hashmap扩容时可能会发生死循环,如下。

  1. 线程A和线程B同时执行扩容,A线程获取完e和e.next并保存到工作内存,此时A线程被挂起;
  2. 此时开始执行线程B的扩容,由于jdk1.7采用头插法,所以在原链表中的链表指向顺序可能会翻转,比如原来1->2,改成2->1
  3. 线程B扩容完成后,线程A重新获取cpu上下文进行扩容操作,把e设置为newTable[i],e = e.next,本次循环结束
  4. 进行下一次循环会从主中获取e.next,此时e.next是线程A中的e

jdk1.8中解决了这个问题,但是会出现数据覆盖的问题

if ((p = tab[i = (n - 1) & hash]) == null)

  1. 当出现hash碰撞时,线程A通过hash函数计算完下标时,cpu时间片耗尽,此时执行线程B,线程B通过hash函数计算出与线程A相同的下标,并完成写入,此时cpu时间片重新分给线程A,这时线程A开始写入把线程B的数据覆盖了,造成了线程不安全。

if (++size > threshold) //++size此处线程不安全

resize();

  1. 进行resize判断的时候,当线程A从主内存中获取了size的值后准备进行+1操作,cpu时间片耗尽,线程B从主内存中获取size值进行+1操作写会主内存,此时线程A继续执行+1后写入主内存,两次put操作size只增加了1,还是数据覆盖问题,导致线程不安全。

hashcode的计算方式是什么

  1. 当对象为string时 用字符串中的每个字母*长度-1个31,然后进行累加操作,
    1. 如Objects.hashCode("java"):106*31*31*31+97*31*31+118*31+97
    2. Objects.hash("java"):和hashCode相比在此基础上多加一个31 ,106*31*31*31+97*31*31+118*31+97 + 31
  2. 当对象是整型数字时,hashCode为数字本身
    1. Objects.hashCode(11),他的hashCode = 11;
    2. Object.hash(11),他的hashCode为 11 + 31
  3. 重写equals时一定要重写hashCode,使用类中的数据进行计算hashCode,确保相同属性的hashCode相等。

hashset是怎么保证数据不重复的

底层使用的是hashMap,在hashSet.add时,调用hashmap.put方法,如果key不存在就进行存储,存在就替换并且把旧数据返回,hashSet.add()方法中实现 return map.put(key.value) == null,如果是重复添加返回false,否则返回true。

基础数据类型的长度,1字节=8位,

  • byte 1字节
  • short 2字节
  • int 4字节
  • long 8字节
  • char 2字节
  • float 4字节
  • double 8字节
  • boolean 1字节

int 最大值位2^31-1,为什么要-1

因为最后一位是符号位 0为正数,1为负数 ,比如 数字7的二进制为 0111 当 7+1的时候 二进制变为 1000 这时 十进制数表示为-8,int型的二进制表示方式

  • 正数的反码和补码都与原码相同
  • 负数的反码除符号位外,其余各位取反(1变0,0变1)
  • 负数的补码在反码的基础上+1

说一下 final string为什么要用final修饰

可以充分的利用常量池

安全性高,因为不可变,确保在多线程可以安全使用

自定义类作为HashMap的key会要注意什么?

一个关于自定义类型作为HashMap的key的问题 - nanoix9 - 博客园

  1. 定义class TestKey{ public String name; public String job}
  2. 当时用TestKey作为map的key时,设置TestKey testkey = new TestKey("zhangsan","java"),put(testkey,1),根据hashcode()计算,假设存在hashmap下标为2的数组中
  3. 此时设置testkey.name="lisi";
  4. 然后使用map.get(testkey)从map中获取,此时根据hashcode计算,最后得出的下标很有可能不为2,那么获取的值就会为空。
  5. 如果使用map.get(new TestKey("zhangsan",1))去map中获取,当计算hashcode()获取下标=1后,会使用传入的键跟链表的键进行比较,因为testkey的数据已经被修改所以比较结果为false返回null
  6. 综上所诉,在挑选hashmap的key时,最好使用不可变类型,比如string,integer等。

重写equals要注意哪些问题

  1. 自反性:如果X有任意引用,那么x.equals(x) == true
  2. 对称性:如果x.equals(y) == true,那么y.equals(x) == true
  3. 传递性:如果x.equals(y) == true,y.equals(z) == true,那么x.equals(z) == true
  4. 一致性:如果x.equals(y) == true,那么如果x,y引用没有发生变化,无论进行多少次equals操作结果都是true
  5. 非空性:如果x有任意非空引用,那么x.equals(null) == false

jmm内存模式

一个变量正常的内存交互流程如下:

  1. 线程开始执行,从主内存中读取(read)共享变量。
  2. 将读取到的共享变量加载(load)到工作内存中。
  3. 使用(use)变量,对变量的值进行修改。
  4. 将修改后的新值赋值(assign)到工作内存中的变量。
  5. 当线程执行结束后,将工作内存中的变量存储(store)到主内存。
  6. 再将刚刚存储的变量值写入(write)到主内存的共享变量。

当变量被volatile修饰后:

  1. 计算机会开启总线级别的MESI缓存一致性协议,和嗅探机制;
  2. 当工作内存中的变量被修改后,会直接写入主内存;
  3. 同时根据嗅探机制,会把其他线程中引用了这个共享变量的工作内存中属性设置为失效;
  4. 然后下一次获取时从主内存中重新加载,即获取到最新的值,实现了数据可见性。

volatile 原理

  • 可见性原理:每次刷新主内存
    • 线程A将工作内存的data更改后,强制将data值刷回主内存
    • 如果线程B的工作内存中有data变量的缓存时,会强制让这个data变量缓存失效
    • 当线程B需要读取data变量的值时,先从工作内存中读,发现已经过期,就会从主内存中加载data变量的最新值了
  • 防止指令重排序:
    • 声明了volatile的属性 不会被先执行,volatile后面的字段不会在volatile 前执行
    • 在dcl中volatile为了防止指令重排序而引入的。了解下singleton = new Singleton()这段代码其实不是原子性的操作,它至少分为以下3个步骤:
      • 给singleton对象分配内存空间
      • 调用Singleton类的构造函数等,初始化singleton对象
      • 将singleton对象指向分配的内存空间,这步一旦执行了,那singleton对象就不等于null了
  • 不能保证原子性原理:
    • 如有多个线程进行int++操作,线程1在工作内存中获取int=10,还没有进行+1操作,此时线程2也在工作内容中获取int=10,由于线程1没有完成+1操作所以没有刷新主内存,所以线程2获取的值是在工作内存中的10。
    • 然后线程2完成了int++,将11写入主内存。由于线程1已经获取了int所以不会重新获取,线程1完成+1操作后将int=11写入主内存。
    • 由此可见两次+1操作,int只加了1,所以volatile无法保证原子性。

内存屏障

  1. lfance读屏障,在读指令之前插入lfance,让缓存中的数据失效,重新从主内存中加载数据,确保读取的数据是最新的。
  2. sfance写屏障,在写指令之后插入sfance,让写入缓存中的数据写入主内存中,以保证写入的数据对其他线程可见。
  3. mfance全能屏障,具备lfance和sfance的能力

happens-before:前面的操作结果,可以被后续的操作获取

  1. 程序的顺序性规则
    1. 一个线程中,按照顺序执行,前面的操作 habbens-before 后续的任何操作
  2. volatile规则
    1. volatile变量写操作 habbens-before 后续对该变量的读操作
  3. 传递性规则
    1. 如果A habbens-before B ,B habbens-before C ,那么 A habbens-before C
  4. 管程中的锁规则
    1. 一个锁的解锁操作 habbens-before 这个锁的加锁操作
  5. 线程的start规则
    1. 线程A启动线程B,那么线程B可见A启动B之前的操作,即strat habbens-before 线程B
  6. 线程的join规则
    1. 线程A join 线程B,线程A被释放时可见线程B的所有操作,即线程B中的任意操作 habbens-before join返回。

synchronized 是怎么实现锁的。

  • 当修饰代码块时,主要有monitorenter和monitorexit完成的同步操作,执行monitorenter会进入monitor对象,从而获得锁,此时计数器+1,执行monitorexit会释放monitor对象释放锁,当同一个线程重复获取monitor时,计数器+1,这就是可重入锁,每次获取的锁monitorenter都会有一个对应的monitorenter,释放锁时直到计数器为0时,其他线程才有机会获取锁。
  • 当修饰方法时,线程访问会先检查是否设置了ACC_SYNCHRNIZED,如果设置了,则可以持有monitor,然后执行方法,释放完成后释放monitor。
  • 线程获取锁时会通过cas操作进行尝试获取,如果线程成功获取到锁,会存入_owner中,如果获取锁失败,会将线程存到enterList中,当线程释放锁后会唤醒enterList,其中的线程或尝试获取锁。
  • 如果持有锁的线程调用了wait()方法,线程会释放锁,并把线程放入waitSet中,并将_owner设置为null,此时其他线程可以重新获取锁。
  • 当调用notfiy方法时,会随机唤醒waitSet中的某一个线程,调用notfiy all会唤醒waitSet中的所有锁。
  • 不可中断、非公平

synchronized 锁升级过程

  1. 锁分为:无锁、偏向锁、轻量级锁、重量级锁
  2. 偏向锁:当线程1获取锁时,会将线程1的threadId记录到java对象头(markword)中,由于偏向锁不会主动释放锁,所以线程1释放锁后再次获取锁时,先会判断java对象头中的threadid和线程1的threadid是否相同,如果相同则可以直接使用锁。如果是线程2来获取锁,那么判断的是否会发现与对象头中的threadid不相同,此时会去查找对象头中的threadid是否存活,如果没有存活则获取锁,如果存活,则判断是否需要持有锁,如果需要持有锁,那么会将锁升级为轻量级锁。如果不需要持有锁,则释放锁,让其他线程获取锁。
  3. 轻量级锁:线程1获取锁时会拿到锁对象头信息存到线程的锁对象空间,然后通过cas操作将线程1锁对象替换到锁的对象头中,如果此时有其他线程2来获取锁,那么线程2会进行自旋,这里自旋是有次数,如100次。当线程2的自旋结束了,或者自旋没有结束但是有新的线程3进来的,此时会锁升级为重量级锁。
  4. 重量级锁:重量级锁会将出持有锁的线程以外的线程全部阻塞。
  5. 面试官:说一下Synchronized底层实现,锁升级的具体过程?_Java识堂的博客-CSDN博客_说一下synchronized
  6. 偏向锁可以在jvm参数中设置是否启动-XX:UseBiasedLocking = ture
  7. 偏向锁默认存在延迟启动4秒,通过配置参数可以进行修改 -XX:BiasedLockingStartupDelay = 4000

ReentrantLock

  1. 当有多个线程获取琐时,如果 t1,t2,t3,此时t1先进来会判断status是否为0,如果为0,将持有锁,并处理后续的业务流程,随后t2尝试获取锁,判断status=1,这是会将t2放入AQS的CLH队列,最后t3尝试获取锁,status=1,会将t3也放入到CLH队列,并排列到t2的后面
  2. t1处理完逻辑释放锁后,将staus设置为0,并唤醒CLH队列,CLH队列中的头结点出列(t2),t2获取锁后将status设置为1,t3亦然。这也是公平锁的体现。
  3. 非公平锁(默认):如果有其他其他线程获取锁
  4. 不去排队,而是直接获取锁即为不公平锁。
  5. 公平锁:有其他线程进来发现他不在队首,自动去排队即为公平锁。RentrantLock有两个构造方法,无参构造会创建一个非公平锁,有参构造传入true创建公平锁。
  6. 可重入锁:同一个线程可以多次获取同一把锁,测试锁的状态+1,释放锁的次数需要与加锁次数相同。
  7. lock():此方法会始终处于等待获取锁,直到锁被unLock释放。
  8. tryLock():使用此方法如果获取不到锁不会等待,而是继续执行后面的逻辑代码。
  9. tryLock(10, TimeUtil.SECONDS):表示会在10秒内等待获取锁,同时也会参与排队,当10秒后没有获取锁会返回false。

synchronized 和 reentrantLock 区别

  1. 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  3. synchronized会自动释放锁(线程执行完同步代码会释放锁 ;线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  4. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  5. synchronized的锁可重入、不可中断、非公平;而Lock锁可重入、可判断、可公平(两者皆可),默认为非公平锁,RentrantLock有两个构造方法,无参构造会创建一个非公平锁,有参构造传入true创建公平锁。
  6. reentrantLock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
  7. reentrantLock可以判断是否有其他线程正在使用锁,使用tryLock

类锁,方法锁

  • 类锁:synchronized修饰静态方法,或者用类名.class 进行加锁
  • 方法锁:synchronized修饰普通方法,或者用this进行加锁
  • 类锁与类锁互斥,方法锁与方法锁互斥

如何产生死锁 ,java mysql

java:

线程1持有锁A,线程2持有锁B;同时,线程1尝试获取锁B,线程2尝试获取锁A,此时会出现互相等待的情况,从而造成死锁。

mysql:

事务1更新表A、表B,事务2更新表B、表A,在事务1更新完A,想要获取B的锁,事务2更新完B,想要获取A的锁,由于A和B的锁都在事务中没有被释放,所以会出现一直等待,造成死锁

AQS是什么

  • AQS是抽象队列同步器,多线程访问控制框架
  • AQS维护了一个volatile status 和一个CLH队列(FIFO双向队列(链表))
  • AQS定义了两种资源共享方案
    • 独占,只有一个线程能够访问,ReentrantLock
    • 共享,多个线程可同时访问,CountDownLatch/Semaphore/CyclicBarrier
  • 处理逻辑在他的实现类中举例
  • aqs基于模板设计模式

cas最底层使用lock comper and change 进行对写的内存块进行上锁,确保这块内存只有当前线程可以使用

//AQS的队列是一个双向链表

private transient volatile Node head;

private transient volatile Node tail;

private volatile int state;

CountDownLatch/Semaphore/CyclicBarrier

  • CountDownLatch:通过实现一个计数器,计数器的初始值为线程的数量,每个线程完成后调用countDown方法使计数器数量减1。当计数器数量为0时才继续执行被await挂起的线程。
    • 方法:countDown(); / await();
  • CyclicBarrier:设置需要同时操作的线程数,比如需要3个线程都到达后才能执行后续操作,先到的线程调用await进行等待,计数器会加1,当最后一个线程到达后计数器结果为3,可执行后去的操作,计数器可重复使用。
    • 方法:await();
  • Semaphore:可以控制同时访问资源的线程个数,如可以允许3个线程同时访问资源。每当有线程进入时会获取一个令牌,如果此时有三个线程来访问分别持有这三个令牌,后续在有线程进入只能等待之前的三个线程释放令牌。
    • 方法: semaphore.acquire(); / semaphore.release();

cas原理,3个参数和4个参数的

  • CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现
    • CAS的核心类unsafe,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门:Unsafe,它提供了硬件级别的原子操作。
  • 4个参数是Unsafe的本地方法的参数,分别为:对象、对象的地址、预期值、修改值

unsafe 除了可以用cas外,还可以用作干啥

  • 除了原子数据 AtomicXXX,还有LockSupport类 以及在 线程池 ThreadPool 类也是用了该类

CAS操作实现

import java.util.concurrent.atomic.*;
public class CasTest {//cas操作 带版本号private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference("刘德华", 10001);public static void main(String[] args) {while (!incr()) {}}private static boolean casTest1() {int stamp = atomicStampedReference.getStamp();System.out.println(stamp);boolean b = atomicStampedReference.compareAndSet("刘德华", "张学友", stamp, stamp+1);System.out.println("reference.getReference()==="+atomicStampedReference.getReference());int after__stamp = atomicStampedReference.getStamp();System.out.println("after__stamp===="+after__stamp);return b;}//cas 不带版本号public static AtomicReference atomicReference = new AtomicReference("黎明");public static boolean casTest2(){boolean b = atomicReference.compareAndSet("黎明", "张学友");System.out.println(b);System.out.println("========"+atomicReference.get());return b;}
}

condition

  • condition可以用来是实现synchronized中的wait、notify、notifyAll
  • 使用方法:
    • 创建实例必须从reenTractLock中获取一个condition实例,这样才能绑定lock
    • await(),让线程等待
    • signal(),唤醒某个线程
    • signalAll(),唤醒全部等待线程

cas的优缺点

  • 优点,在并发量不是很高的情况下,使用cas可以提高效率,如sync轻量级锁等
  • 缺点
    • 在并发量高的情况下,大量的自旋操作会占用cpu资源,影响性能
    • 只能用于一个变量的原子性操作,不能用于代码块的原子性操作
    • 会存在aba的问题,例子如下:
      • 用户A去提款机取钱,卡上有200元,要取100元由于提款提太卡,用户A点了几下,产生了线程1和线程2,线程1将余额从200变为100,同时用户B向用户A打款100元,线程3将卡上余额从100变为200元,这是线程2开始处理,发现卡内余额还是200元,又将200元变为100元,这样一番操作下来,用户少了100元。
      • 解决方案是通过版本号去控制,当cas操作时不仅对值比较也要对版本进行比较,参考java:AtomicStampedReference就是用版本号实现cas机制。

接口和抽象类的区别

接口中定义了外部或内部请求的方法,抽象类中可以定义的处理流程的框架,详细的逻辑需要实现类去完善。

  1. 接口中的只有方法名没有方法体(在jdk1.8之前),jdk1.8之后接口中也可以有方法体;抽象类中可以有方法体。
  2. 一个类可以实现多个接口;但是可以继承一个抽象类。
  3. 接口没有构造方法;抽象类有构造方法。
  4. 接口中只有常量,因为如果定义变量编译的时候默认会加上public static final;抽象类可以有普通属性、静态属性。
  5. 接口中的方法都是抽象的默认用public abstract修饰;抽象类中可以包含非抽象方法,并且可以使用public、protected修饰。
  6. 接口中不能包含静态方法,抽象类中可以包含静态方法。

异常体系

java中的异常分为Error和Exception,他们的父类为Throwable。

  1. Error是非程序异常,即程序不能捕获的异常,一般是编译或者系统性的错误,如OutOfMemorryError内存溢出。
  2. Exception是程序异常类,由程序内部产生。Exception又分为运行时异常、编译时异常。
    1. 运行时异常的特点是java编译器不会检查它,也就是说,当程序中可能出现这类异常时,会编译通过,但在运行时会出现错误。比如NullPointException,ArrayIndexoutOfBuoundsException等。
    2. 编译时异常是程序必须进行处理的异常,否则编译不通过,必须捕获或者抛出。如IOException,sdf的paserException等。

异常捕获流程

  1. try(书写业务逻辑)
  2. catch(可以有多个) 如果发生异常先执行return之前的代码,包括return 语句中的表达式运算,如果finally中有return 则执行finally中的方法后,在finally中return,如果finally中没有return,则执行完finally方法后进行catch中的return。
  3. finally(可以不写,如果写了无论是否有异常都会执行,常用于连接的关闭,释放锁等功能)

Java中的访问修饰符

  1. public:公共修饰符,任何类都可以访问
  2. 无修饰符:默认,同一包下及子类可以访问
  3. protected:保护修饰符,只有同一类及子类可以访问
  4. private:私有修饰符,只有类内部可以访问

重载和重写的区别

  1. 重载:发生在同一个类中,方法名相同,参数类型不同,参数个数不同。返回值不同或修饰符不同不算是重载,并且在编译时会报错。
  2. 重写:发生在子类重写父类的方法,方法名和参数名必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。如果父类的修饰符是private,则子类无法进行重写。

深拷贝和浅拷贝

  1. 浅拷贝:只拷贝基本数据类型,以及实例对象的引用地址,也就是说浅拷贝的对象,与被拷贝的对象指向的是同一个内存地址。
  2. 深拷贝:即会拷贝基本数据类型,也会针对实例对象的引用地址所只想的对象进行复制,深拷贝出来的独享,与被拷贝的对象指向的是不同地址。

引用某个类的静态变量是否会引发类的初始化

  1. 当静态变量被final修饰时不会引发类的初始化,因为在准备节点已经将final修饰的静态变量进行赋值放入常量池了。
    1. 如果值不是常量,比如获取时间戳System.currentTimeMillis(),此时也会进行类的初始化。
  2. 当静态变量没有被final修饰时会引发类的初始化

继承类调用顺序

  1. 父类静态代码块
  2. 子类静态代码块
  3. 父类普通代码块
  4. 父类构造方法
  5. 子类普通代码块
  6. 子类构造方法

创建对象的几种方式

  1. 通过new X()直接创建对象,调用构造方法
  2. 反射创建对象,X.class.newInstance(); ,会调用构造方法
  3. clone,不会调用构造方法
  4. 反序列化,不会调用构造方法

本文标签: JAVAJAVA