发布于2021-06-12 14:01 阅读(488) 评论(0) 点赞(2) 收藏(5)
1.请你谈谈你对volatile的理解?
volatile是JVM提供的轻量级的同步机制
1.保证可见性
2.不保证原子性
3.禁止指令重排
JMM(Java内存模型java memory model)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范。
JMM关于同步的规定:
由于JVM运行程序的实体是线程,而每个线程创建是JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有的线程都可以访问。线程对变量的操作(读取赋值等)都必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,不同的线程之间无法访问对方发的工作内存。
而这就可能存在 当一个线程AAA修改了共享变量X的值但还未写回主内存时,另一个线程BBB又对主内存中同一个变量X进行操作,但此时AAA线程工作内存中的变量X对线程BBB来讲并不课件,这种工作内存与主内存同步延迟现象就造成了可见性问题。
原子性:不可分割,完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者分割,需要整体完成,要么同时成功要么同时失败。
- class MyData2 {
- /**
- * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
- */
- volatile int number = 0;
-
-
- public void addPlusPlus() {
- number ++;
- }
- }
-
- public class VolatileAtomicityDemo {
-
- public static void main(String[] args) {
- MyData2 myData = new MyData2();
-
- // 创建10个线程,线程里面进行1000次循环
- for (int i = 0; i < 20; i++) {
- new Thread(() -> {
- // 里面
- for (int j = 0; j < 1000; j++) {
- myData.addPlusPlus();
- }
- }, String.valueOf(i)).start();
- }
-
- // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
- // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
- while(Thread.activeCount() > 2) {
- // yield表示不执行
- Thread.yield();
- }
-
- // 查看最终的值
- // 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
- System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
-
- }
-
- }
-
最后的结果总是小于20000。
number++
在多线程下是非线程安全的。
我们可以将代码编译成字节码,可看出number++
被编译成3条指令。
假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000。
问题解决:
- import java.util.concurrent.atomic.AtomicInteger;
-
- class MyData2 {
- /**
- * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
- */
- volatile int number = 0;
- AtomicInteger number2 = new AtomicInteger();
-
- public void addPlusPlus() {
- number ++;
- }
-
- public void addPlusPlus2() {
- number2.getAndIncrement();
- }
- }
-
- public class VolatileAtomicityDemo {
-
- public static void main(String[] args) {
- MyData2 myData = new MyData2();
-
- // 创建10个线程,线程里面进行1000次循环
- for (int i = 0; i < 20; i++) {
- new Thread(() -> {
- // 里面
- for (int j = 0; j < 1000; j++) {
- myData.addPlusPlus();
- myData.addPlusPlus2();
- }
- }, String.valueOf(i)).start();
- }
-
- // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
- // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
- while(Thread.activeCount() > 2) {
- // yield表示不执行
- Thread.yield();
- }
-
- // 查看最终的值
- // 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
- System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
- System.out.println(Thread.currentThread().getName() + "\t finally number2 value: " + myData.number2);
- }
- }
输出结果为:
- main finally number value: 18766
- main finally number2 value: 20000
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
单线程环境里确保程序最终执行结果和代码顺序执行的结果一致。
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线性中使用的变量能否保障一致性是无法确定的,结果无法预测。
- public class ReSortSeqDemo{
- int a = 0;
- boolean flag = false;
-
- public void method01(){
- a = 1;//语句1
- flag = true;//语句2
- }
-
- public void method02(){
- if(flag){
- a = a + 5; //语句3
- }
- System.out.println("retValue: " + a);//可能是6或1或5或0
- }
-
- }
多线程环境中线程交替执行method01()
和method02()
,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
volatile实现禁止指令重拍优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。
对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
2.CAS你知道吗?
CAS的全称为Compare-And-Swap,比较并交换,是一条CPU并发原语。
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
- public class CASDemo{
- public static void main(string[] args){
- AtomicInteger atomicInteger = new AtomicInteger(5);// mian do thing. . . . ..
- System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get());
- System.out.println(atomicInteger.compareAndset(5, 1024)+"\t current data: "+atomicInteger.get());
- }
- }
输出结果为
- true 2019
- false 2019
CAS底层原理?谈谈你对UnSafe的理解?
atomiclnteger.getAndIncrement();
源码
- public class AtomicInteger extends Number implements java.io.Serializable {
- private static final long serialVersionUID = 6214790243416807050L;
-
- // setup to use Unsafe.compareAndSwapInt for updates
- private static final Unsafe unsafe = Unsafe.getUnsafe();
- private static final long valueOffset;
-
- static {
- try {
- valueOffset = unsafe.objectFieldOffset
- (AtomicInteger.class.getDeclaredField("value"));
- } catch (Exception ex) { throw new Error(ex); }
- }
-
- private volatile int value;
-
- /**
- * Creates a new AtomicInteger with the given initial value.
- *
- * @param initialValue the initial value
- */
- public AtomicInteger(int initialValue) {
- value = initialValue;
- }
-
- /**
- * Creates a new AtomicInteger with initial value {@code 0}.
- */
- public AtomicInteger() {
- }
-
- ...
-
- /**
- * Atomically increments by one the current value.
- *
- * @return the previous value
- */
- public final int getAndIncrement() {
- return unsafe.getAndAddInt(this, valueOffset, 1);
- }
-
- ...
- }
-
1. UnSafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,而基于UnSafe类可以直接操作特定内存的数据,UnSafe类存在于sun.misc包中,其内部方法操作可以像c的指针一样直接操作内存,因为java中CAS操作的执行依赖于UnSafe类的方法。
2.变量valueOffset 表示该变量值在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的。
3.变量value用volatile修饰,保证了多线程之间的内存可见性。
CAS是什么?
CAS的全称为Compare-And-Swap,比较并交换,是一条CPU并发原语。
他的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。(原子性)
上面类似自旋锁
UnSafe.getAndAddInt()源码解释:
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :
CAS缺点:
1.循环时间长开销很大
- // ursafe.getAndAddInt
- public final int getAndAddInt(Object var1, long var2, int var4){
- int var5;
- do {
- var5 = this.getIntVolatile(var1, var2);
- }while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4));
- return var5;
- }
可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
2.只能保证一个共享变量的原子操作
当对一个共享变量执行操作是,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
3.CAS引来ABA问题
3.原子类AtomicInteger的ABA问题谈谈?原子类更新引用知道吗?
CAS会导致“ABA问题”(狸猫换太子).
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作变成了B,然后线程two又将V位置的数据变成了A,这时候线程one进行CAS操作的时候发现内存中仍然是A,然后线程one操作成功
尽管线程one的CAS操作成功,但不代表这个过程是没有问题的。
AtomicReference原子引用(自定义的类,原理和AtomicInteger差不多)
- import java.util.concurrent.atomic.AtomicReference;
-
- class User{
-
- String userName;
-
- int age;
-
- public User(String userName, int age) {
- this.userName = userName;
- this.age = age;
- }
-
- @Override
- public String toString() {
- return String.format("User [userName=%s, age=%s]", userName, age);
- }
-
- }
-
- public class AtomicReferenceDemo {
- public static void main(String[] args){
- User z3 = new User( "z3",22);
- User li4 = new User("li4" ,25);
- AtomicReference<User> atomicReference = new AtomicReference<>();
- atomicReference.set(z3);
- System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
- System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
- }
- }
-
输出结果
- true User [userName=li4, age=25]
- false User [userName=li4, age=25]
AtomicStampedReference版本号原子引用:
原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。
- import java.util.concurrent.TimeUnit;
- import java.util.concurrent.atomic.AtomicReference;
- import java.util.concurrent.atomic.AtomicStampedReference;
-
- public class ABADemo {
- /**
- * 普通的原子引用包装类
- */
- static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
-
- // 传递两个值,一个是初始值,一个是初始版本号
- static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
-
- public static void main(String[] args) {
-
- System.out.println("============以下是ABA问题的产生==========");
-
- new Thread(() -> {
- // 把100 改成 101 然后在改成100,也就是ABA
- atomicReference.compareAndSet(100, 101);
- atomicReference.compareAndSet(101, 100);
- }, "t1").start();
-
- new Thread(() -> {
- try {
- // 睡眠一秒,保证t1线程,完成了ABA操作
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 把100 改成 101 然后在改成100,也就是ABA
- System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
-
- }, "t2").start();
-
- /
- try {
- TimeUnit.SECONDS.sleep(2);
- } catch (Exception e) {
- e.printStackTrace();
- }
- /
-
-
- System.out.println("============以下是ABA问题的解决==========");
-
- new Thread(() -> {
-
- // 获取版本号
- int stamp = atomicStampedReference.getStamp();
- System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
-
- // 暂停t3一秒钟
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- // 传入4个值,期望值,更新值,期望版本号,更新版本号
- atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(),
- atomicStampedReference.getStamp() + 1);
-
- System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());
-
- atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(),
- atomicStampedReference.getStamp() + 1);
-
- System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());
-
- }, "t3").start();
-
- new Thread(() -> {
-
- // 获取版本号
- int stamp = atomicStampedReference.getStamp();
- System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
-
- // 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
- try {
- TimeUnit.SECONDS.sleep(3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
-
- System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:"
- + atomicStampedReference.getStamp());
-
- System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());
-
- }, "t4").start();
-
- }
- }
输出结果
- ============以下是ABA问题的产生==========
- true 2019
- ============以下是ABA问题的解决==========
- t3 第一次版本号1
- t4 第一次版本号1
- t3 第二次版本号2
- t3 第三次版本号3
- t4 修改成功否:false 当前最新实际版本号:3
- t4 当前实际最新值100
反射的实现与作用
Java语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类,类中的方法以及属性等。反射的实现主要借助以下四个类:
Class:类的对象
Constructor:类的构造方法
Field:类中的属性对象
Method:类中的方法对象
作用:反射机制指的是程序在运行时能够获取自身的信息。在java中,只要给定类的名字,那么就可以通过反射机制来获取类的所有信息。
注解的原理
注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。
Synchronized和lock
synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。JDK1.5以后引入了自旋锁、锁粗化、轻量级锁,偏向锁来有优化关键字的性能。
(1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
(2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
(3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
(4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
Integer和int=5的区别:
Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
Java 为每个原始类型提供了包装类型:
- 原始类型: boolean,char,byte,short,int,long,float,double
- 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
如:
class AutoUnboxingTest {
public static void main(String[] args) {
Integer a = new Integer(3);
Integer b = 3; // 将3自动装箱成Integer类型
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a自动拆箱成int类型再和c比较
}
}
请你解释一下类加载机制,双亲委派模型,好处是什么?
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
Java的容器:
容器是一个java所编写的程序,原先必须自行编写程序以管理对象关系,容器会自动帮您做好。
常用的容器:WebSphere,Weblogic,Resinm,Tomcat,Glassfish
Java容器类包含:list ,Arraylist,Vector及map, hashTable, HashMap, HashSet
wait和sleep:
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
hashSet的底层实现原理:
hashset是无序的,不可重复的。
Hashset底层使用了哈希表(哈希表是将数组和单向链表的优点集成在一起)实现的。特点是存储快。
往hashset添加元素的时候,hashset会先调用元素的hashcode方法得到元素的哈希值,然后通过与水泥素的哈希值经过异或或移位等运算,就可以算出该元素在哈希表中的存储位置。
运行原理:
如果算出的元素的存储的位置目前没有任何元素储存,那么该元素可以直接存储在该位置上,如果算出的元素的存储位置上目前已经有了其他的元素没那么还会调用该元素的equals方法,与该位置的元素进行比较一次,如果equals方法返回的是true,那么该位置上的元素就会被视为重复元素,不允许被添加,如果false,则允许添加。
实现原理:
Hashset是基于hashmap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75的hashmap。封装了一个hashmap对象来储存所有的集合元素,所有放在hashset中的集合元素实际上由hashmap的key来保存。
hashSet和treeset有什么区别:
hashset是由一个hash表来实现的,因此它的元素是无序的,add,remove,contains方法的时间复杂度是 O(1)
treeset是由一个树形结构来实现的,它里面的元素是有序的,因此,add,remove,contains方法的时间复杂度是 O(logn)
二叉树,多叉树,B树,B+树,B*树
二叉树的操作效率较高,但存在问题。
多叉树:在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树。多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。(2-3树,2-3-4树)
B树(B-tree/B-树)(就是多叉树的一种):B树通过重新组织节点,降低了树的高度,广泛应用于文件系统和数据库系统中。
B即Balanced,平衡的意思。
特点:在叶子节点和非叶子节点都存放数据。
B树为什么要设置成多路?
答:为了进一步降低树的高度。但不能设计成无限多路,因为如果不限制路数,B树就退化成一个有序数组了,而文件系统和数据库索引都是存在硬盘上的,并且数据量大的话,不一定能一次性加载到内存中。
B树做文件系统的索引比较多。
B+树(B树的变体)(也是多一种多路搜索树):
特点:(1)所有的数据(关键字)都放在叶子节点上。
B+树做数据库的索引比较多。
这是和业务场景相关的,数据库中select数据,不一定只选一条,很多时候会选多条,比如按照id排序后选10条。使用B树需要做局部的中序遍历,可能要跨层访问。而B+树由于所有数据都在叶子结点,不用跨层,同时由于有链表结构,只需要找到首尾,通过链表就能把所有数据取出来了。
面试题:B+树查询的时间和树的高度有关,大概是log(n),而使用hash存储索引,查询的平均时间是O(1),既然hash比B+树更快,为啥mysql还用b+树来存索引呢?
答:这和业务场景有关。如果只选一个数据,那确实是hash更快。但是数据库中经常会选择多条,这时候由于B+树索引有序,并且又有链表相连,它的查询效率比hash就快很多了。而且数据库中的索引一般是在磁盘上,数据量大的情况可能无法一次装入内存,B+树的设计可以允许数据分批加载,同时树的高度较低,提高查找效率。
B*树(B+树的变体)(在B+树的非根和非叶子结点再增加指向兄弟的指针):
1.怎么快速的把一个list集合中的元素去重?
(1)利用HashSet去重
- package com.ggqq;
-
- import java.util.ArrayList;
- import java.util.HashSet;
- import java.util.Iterator;
- import java.util.List;
-
- public class Test01 {
- public static void main(String[] args) {
- List list = new ArrayList();
- list.add("123");
- list.add("123");
- list.add("234");
- list.add("789");
- /*//遍历
- //方法一:for循环
- for(int i = 0; i<list.size();i++){
- System.out.println(list.get(i));
- }
- //方法二:iterator迭代器
- Iterator iterator = list.iterator();
- while(iterator.hasNext()){
- System.out.println(iterator.next());
- }*/
- //方法三:增强for
- for(Object s:list){
- System.out.println(s);
- }
-
- //list添加到HashSet中去重
- HashSet hashSet = new HashSet(list);
- //清空list
- list.clear();
- //将HashSet添加到list中
- list.addAll(hashSet);
- //遍历
- for(int i = 0; i<list.size();i++){
- System.out.println(list.get(i));
- }
- }
- }
(2)通过List的contains()方法去重
- package com.ggqq;
-
- import java.util.ArrayList;
- import java.util.HashSet;
- import java.util.Iterator;
- import java.util.List;
-
- public class Test02 {
- public static void main(String[] args) {
- List list = new ArrayList();
- list.add("123");
- list.add("123");
- list.add("234");
- list.add("789");
-
- //遍历
- Iterator iterator = list.iterator();
- while(iterator.hasNext()){
- System.out.println(iterator.next());
- }
- System.out.println("------------------------");
- //新建一个tempList,用于存放去重后的
- List tempList = new ArrayList();
- for(int i = 0 ; i <list.size();i++){
- if(!tempList.contains(list.get(i))){
- tempList.add(list.get(i));
- }
- }
- //遍历
- Iterator iterator2 = tempList.iterator();
- while(iterator2.hasNext()){
- System.out.println(iterator2.next());
- }
- }
- }
(3) 通过两层for循环判断
- package com.ggqq;
-
- import java.util.ArrayList;
- import java.util.Iterator;
- import java.util.List;
-
- public class Test03 {
- public static void main(String[] args) {
- List list = new ArrayList();
- list.add("123");
- list.add("123");
- list.add("234");
- list.add("789");
-
- //遍历
- Iterator iterator = list.iterator();
- while(iterator.hasNext()){
- System.out.println(iterator.next());
- }
- System.out.println("------------------------");
- //从list中索引为0开始往后遍历
- for(int i = 0 ; i <list.size()-1;i++){
- for(int j = list.size()-1; j>i; j-- ){
- if(list.get(i).equals(list.get(j))){
- //去重
- list.remove(j);
- }
- }
- }
- //遍历
- Iterator iterator2 = list.iterator();
- while(iterator2.hasNext()){
- System.out.println(iterator2.next());
- }
- }
- }
2.ThreadLocal是什么?有哪些使用场景?
ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
ThreadLocal 使用例子:
- package com.ggqq;
-
- public class TestThreadLocal {
-
- //线程本地存储变量
- private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
- @Override
- protected Integer initialValue() {
- return 0;
- }
- };
-
- public static void main(String[] args) {
- for (int i = 0; i < 3; i++) {//启动三个线程
- Thread t = new Thread() {
- @Override
- public void run() {
- add10ByThreadLocal();
- }
- };
- t.start();
- }
- }
-
- /**
- * 线程本地存储变量加 5
- */
- private static void add10ByThreadLocal() {
- for (int i = 0; i < 5; i++) {
- Integer n = THREAD_LOCAL_NUM.get();
- n += 1;
- THREAD_LOCAL_NUM.set(n);
- System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
- }
- }
-
- }
结果:
3.什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称。
4.分布式锁的实现方式
(1)为什么需要分布式锁?
首先我们应该先了解一下分布式锁的使用场景,然后再来理解为什么需要分布式锁。现我举两个例子来进行阐述:
应用场景:
1:银行转账问题(该场景不太好解释):A在上海,B在北京同时在建行转账给杭州C,A转账时,会修改C处服务器的表,B不能在此刻转账,同理,B转账时,A不能做处理,A,B的转账操作时同步,必须保证数据的一致性,这就需要分布式锁来进行处理。
2:取任务问题:某服务提供一组任务,A系统请求随机从任务组中获取一个任务;B系统请求随机从任务组中获取一个任务。 在理想的情况下,A从任务组中挑选一个任务,任务组删除该任务,B从剩下的的任务中再挑一个,任务组删除该任务。 同样的,在真实情况下,如果不做任何处理,可能会出现A和B挑中了同一个任务的情况。
(2)为什么分布式系统中不能用普通锁呢?那么普通锁和分布式锁有什么区别呢?
普通锁:单一系统中,同一个应用程序是有同一个进程,然后多个线程并发会造成数据安全问题,他们是共享同一块内存的,所以在内存某个地方做标记即可满足需求,例如synchronized和volatile+cas一样对具体的代码做标记,对应的就是在同一块内存区域作了同步的标记。
分布式锁:分布式系统中,最大的区别就是不同系统中的应用程序都是在各自机器上不同的进程中处理的,这里的线程不安全可以理解为多进程造成的数据安全问题,他们不会共享同一台机器的同一块内存区域,因此需要将标记存储在所有进程都能看到的地方。例如zookeeper作分布式锁,就是将锁标记存储在多个进程共同看到的地方,redis作分布式锁,是将其标记公共内存,而不是某个进程分配的区域。
(3)分布式锁的三种实现方式
a:zookeeper实现分布式锁(用的最多)
实现方式:
方案1:利用节点名称的唯一性来实现共享锁。
算法思路: 利用名称唯一性,加锁操作时,只需要所有客户端一起创建/test/Lock节点,只有一个创建成功,成功者获得锁。解锁时,只需删除/test/Lock节点,其余客户端再次进入竞争创建节点,直到所有客户端都获得锁。
方案2:利用临时顺序节点实现共享锁。(主要是用这种方式实现)
算法思路:对于加锁操作,可以让所有客户端都去/lock目录下创建临时顺序节点,如果创建的客户端发现自身创建节点序列号是/lock/目录下最小的节点,则获得锁。否则,监视比自己创建节点的序列号小的节点(比自己创建的节点小的最大节点),进入等待。
比如创建节点:/lock/0000000001、/lock/0000000002、/lock/0000000003。则节点/lock/0000000001会先获得锁,因为zk上的节点是有序的,且都是最小的节点先获得锁。
注:临时顺序节点比持久顺序节点的好处是:当zookeeper宕机后,临时顺序节点会自动删除,获取锁的客户端会释放锁,不会一直造成锁等待,而持久节点会造成锁等待。
两种方式的区别:
方案1会产生惊群效应:假如许多客户端在等待一把锁,当锁释放时候所有客户端都被唤醒,然后竞争分布式锁,仅仅有一个客户端得到锁。
方案2是按照创建顺序排队的实现,多个客户端共同等待锁,当锁释放时只有一个客户端会被唤醒,在zk上注册节点最小的客户端会被唤醒,避免了惊群效应。
b:redis实现分布式锁(用的次之)
redis实现分布式锁主要靠四个命令:
setnx(set if not exits 维护着是乐观锁):当不存在key的时候,才为key设置值为value。setnx与set的区别:set是存在key,则去覆盖value;setnx是不存在key,则重新给key和value赋值。
getset:根据key得到旧的值,并set新的值。
expire:设置过期时间。
del:删除
实现方式:
1:获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
2:获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
3:释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
c:数据库实现分布式锁(用的最少)
实现方式:利用的是乐观锁和悲观锁
乐观锁:在表中添加版本号的字段,每次更新前都先查询出带版本号的数据,然后再更新的时候where条件语句后带版本号条件,更新成功表示锁已占用,更新不成功表示锁没被占用。
悲观锁:利用select...for update(X锁)/select...lock in share mode(S锁),一般来说用X锁的较多,因为后续多会做写功能的实现。
注:当实现悲观锁的时候,需要关闭数据库的事务自动提交机制不然不会生效。因此java代码中应该选择主动关闭数据库的事务自动提交功能。
5.说一下HashMap的实现原理
hashset是无序的,不可重复的。
Hashset底层使用了哈希表(哈希表是将数组和单向链表的优点集成在一起)实现的。特点是存储快。
往hashset添加元素的时候,hashset会先调用元素的hashcode方法得到元素的哈希值,然后通过与水泥素的哈希值经过异或或移位等运算,就可以算出该元素在哈希表中的存储位置。
运行原理:
如果算出的元素的存储的位置目前没有任何元素储存,那么该元素可以直接存储在该位置上,如果算出的元素的存储位置上目前已经有了其他的元素没那么还会调用该元素的equals方法,与该位置的元素进行比较一次,如果equals方法返回的是true,那么该位置上的元素就会被视为重复元素,不允许被添加,如果false,则允许添加。
实现原理:
Hashset是基于hashmap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75的hashmap。封装了一个hashmap对象来储存所有的集合元素,所有放在hashset中的集合元素实际上由hashmap的key来保存。
6.在Queue中poll()和remove()有什么区别?
poll()和remove()都将移除并且返回队头,但是在poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常。
7.JDK1.8的新特性:
8.application和bootstrap的应用场景:
application:配置文件这个容易理解,主要用于SpringBoot项目的自动化配置。
bootstrap:配置文件有以下几个应用场景:
application.yml 是用户级别的配置,而bootstrap.yml 是系统级别的配置
9.HashMap和HashTable的key和value是否可以为null?
HashMap可以存储一个Key为null,多个value为null的元素,但是Hashtable却不可以存储
(看源码)
10.什么是Spring?
Spring是于2003年兴起的一个轻量级java开发框架,他是为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面编程(AOP)。
Spring的作用就是为代码解耦合,降低代码的耦合度。就是让对象和对象(模块和模块)之间关系不是使用代码关联,而是通过配置来说明,
IOC又称自动注入,注入即赋值,IOC使的主业务在相互调用的过程中,不用再自己维护关系了,即不用自己再创建要使用的对象了,而是右Spring容器统一管理。
AOP是动态代理的规范化,AOP是Spring框架中的一个重要内容,利用AOP可以对业务逻辑的各个部分进行隔离,从而使业务逻辑的各个部分之间的耦合度降低,提高程序的可重复性,同时提高了开发的效率。
SpringMVC谁调谁的问题
在SpringMVC中,强化了注解的使用,在Controller,Service,Dao层都可以使用注解。
使用@Controller注解的处理器的处理器方法,其返回值类型常用的有四种类型:
第一种:ModelAndView (传递数据+传递视图(即跳转到其他资源))
第二种:String (视图)
第三种: 无返回值void (处理Ajax)
第四种:返回自定义类型对象 (数据)
区分返回值String是数据还是视图,看有没有@ResponseBody注解,如果有,则是数据,否则是视图。
2、cookie和session的区别?
session是服务器端用于验证用户权限的一把钥匙,存于服务端,在进行数据交互时使用的,我们经常遇到的登录失效这一类场景,就是因为session在中间起到了作用。
cookie是保存在本地的数据(客户端),可以简单地理解,在页面中输入账号,自动弹出密码,这个密码之所以会弹出就是因为本地cookie的原因,包括历史记录这些,之所以会有记录,就是因为内容存储在本地的cookie文件中。
3、springmvc和springboot的区别
Spring 框架就像一个家族,有众多衍生产品例如 boot、security、jpa等等。但他们的基础都是Spring 的 ioc和 aop ioc 提供了依赖注入的容器 aop ,解决了面向横切面的编程,然后在此两者的基础上实现了其他延伸产品的高级功能。Spring MVC是基于 Servlet 的一个 MVC 框架 主要解决 WEB 开发的问题,因为 Spring 的配置非常复杂,各种XML、 JavaConfig、hin处理起来比较繁琐。于是为了简化开发者的使用,从而创造性地推出了Spring boot,约定优于配置,简化了spring的配置流程。
说得更简便一些:Spring 最初利用“工厂模式”(DI)和“代理模式”(AOP)解耦应用组件。大家觉得挺好用,于是按照这种模式搞了一个 MVC框架(一些用Spring 解耦的组件),用开发 web 应用( SpringMVC )。然后有发现每次开发都写很多样板代码,为了简化工作流程,于是开发出了一些“懒人整合包”(starter),这套就是 Spring Boot。
Spring MVC的功能
Spring MVC提供了一种轻度耦合的方式来开发web应用。
Spring MVC是Spring的一个模块,式一个web框架。通过Dispatcher Servlet, ModelAndView 和 View Resolver,开发web应用变得很容易。解决的问题领域是网站应用程序或者服务开发——URL路由、Session、模板引擎、静态Web资源等等。
Spring Boot的功能
Spring Boot实现了自动配置,降低了项目搭建的复杂度。
众所周知Spring框架需要进行大量的配置,Spring Boot引入自动配置的概念,让项目设置变得很容易。Spring Boot本身并不提供Spring框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于Spring框架的应用程序。也就是说,它并不是用来替代Spring的解决方案,而是和Spring框架紧密结合用于提升Spring开发者体验的工具。同时它集成了大量常用的第三方库配置(例如Jackson, JDBC, Mongo, Redis, Mail等等),Spring Boot应用中这些第三方库几乎可以零配置的开箱即用(out-of-the-box),大部分的Spring Boot应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。
Spring Boot只是承载者,辅助你简化项目搭建过程的。如果承载的是WEB项目,使用Spring MVC作为MVC框架,那么工作流程和你上面描述的是完全一样的,因为这部分工作是Spring MVC做的而不是Spring Boot。
对使用者来说,换用Spring Boot以后,项目初始化方法变了,配置文件变了,另外就是不需要单独安装Tomcat这类容器服务器了,maven打出jar包直接跑起来就是个网站,但你最核心的业务逻辑实现与业务流程实现没有任何变化。
所以,用最简练的语言概括就是:
Spring 是一个“引擎”;
Spring MVC 是基于Spring的一个 MVC 框架 ;
Spring Boot 是基于Spring4的条件注册的一套快速开发整合包。
5、你所知道的微服务技术栈有哪些?列举一二
微服务技术条目 | 落地技术 |
---|---|
服务开发 | SpringBoot、Spring、SpringMVC等 |
服务配置与管理 | Netfix公司的Archaius、阿里的Diamond等 |
服务注册与发现 | Eureka、Consul、Zookeeper等 |
服务调用 | Rest、PRC、gRPC |
服务熔断器 | Hystrix、Envoy等 |
负载均衡 | Ribbon、Nginx等 |
服务接口调用(客户端调用服务的简化工具) | Fegin等 |
消息队列 | Kafka、RabbitMQ、ActiveMQ等 |
服务配置中心管理 | SpringCloudConfig、Chef等 |
服务路由(API网关) | Zuul等 |
服务监控 | Zabbix、Nagios、Metrics、Specatator等 |
全链路追踪 | Zipkin、Brave、Dapper等 |
数据流操作开发包 | SpringCloud Stream(封装与Redis,Rabbit,Kafka等发送接收消息) |
时间消息总栈 | SpringCloud Bus |
服务部署 | Docker、OpenStack、Kubernetes等 |
6、 SpringCloud 和 Dubbo有那些区别?
对比结果:
Dubbo | SpringCloud | |
---|---|---|
服务注册中心 | Zookeeper | Spring Cloud Netfilx Eureka |
服务调用方式 | RPC | REST API |
服务监控 | Dubbo-monitor | Spring Boot Admin |
断路器 | 不完善 | Spring Cloud Netfilx Hystrix |
服务网关 | 无 | Spring Cloud Netfilx Zuul |
分布式配置 | 无 | Spring Cloud Config |
服务跟踪 | 无 | Spring Cloud Sleuth |
消息总栈 | 无 | Spring Cloud Bus |
数据流 | 无 | Spring Cloud Stream |
批量任务 | 无 | Spring Cloud Task |
最大的区别:SpringCloud抛弃了Dubbb的RPC通信,采用的是Http的REST方式
Rest和RPC对比:严格来说,这两种方式各有优劣,虽然从一定程度上说,Rest牺牲了服务调用的性能,但也避免了原生RPC带来的问题,而且REST相比RPC更为灵活,服务提供方和调用方只依靠一纸契约,不存在代码级别的强依赖,这个有点在当下强调快速演化的微服务环境下,显得更为合适。
文档质量和社区活跃度对比:SpringCloud社区活跃度远高于Dubbo,毕竟由于梁飞团队的原因导致Dubbo停止更新迭代五年,而中小型公司无法承担技术开发的成本导致Dubbo社区严重低落,而SpringCloud异军突起,迅速占领了微服务的市场,背靠Spring混的风生水起
Dubbo经过多年的积累文档相当成熟,对于微服务的架构体系各个公司也有稳定的现状
品牌机和组装机的区别:Dubbo是组装机,SpringCloud是品牌机
总结:二者解决的问题领域不一样,Dubbo的定位是一款RPC框架,而SpringCloud的目标是微服务架构下的一站式解决方案。
7、 SpringBoot 和 SpringCloud,请谈谈你对他们的理解
8、 Eureka和Zookeeper都可以提供服务注册与发现的功能,请说说两者的区别
RDBMS (MySQL\Oracle\sqlServer) ===> ACID
NoSQL (Redis\MongoDB) ===> CAP
CAP的三进二:要么CA、要么AP、要么CP(无法保证该三个性质都满足)
作为分布式服务注册中心,Eureka比Zookeeper好在哪里?
著名的CAP理论指出,一个分布式系统不可能同时满足C (一致性) 、A (可用性) 、P (容错性),由于分区容错性P在分布式系统中是必须要保证的,因此我们只能再A和C之间进行权衡。
Zookeeper保证的是CP(一致性,容错性,可用性差)
当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接收服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但zookeeper会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30-120s,且选举期间整个zookeeper集群是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因为网络问题使得zookeeper集群失去master节点是较大概率发生的事件,虽然服务最终能够恢复,但是,漫长的选举时间导致注册长期不可用,是不可容忍的。
Eureka保证的是AP(可用性,容错性,一致性差)
Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时,如果发现连接失败,则会自动切换至其他节点,只要有一台Eureka还在,就能保住注册服务的可用性,只不过查到的信息可能不是最新的,除此之外,Eureka还有之中自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:
因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪
11.关于数组的时间复杂度
算法 | 时间复杂度 |
---|---|
线性查找 | O(N) |
二分查找 | O(logN) |
无序数组的插入 | O(1) |
有序数组的插入 | O(N) |
无序数组的删除 | O(N) |
有序数组的删除 | O(N) |
12.线程启动的四种方式:
https://blog.csdn.net/Mcdull__/article/details/115444499
原文链接:https://blog.csdn.net/Mcdull__/article/details/117734159
作者:快起来搬砖啦
链接:http://www.javaheidong.com/blog/article/222295/6a3d77e50b9bbf48febd/
来源:java黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 java黑洞网 All Rights Reserved 版权所有,并保留所有权利。京ICP备18063182号-2
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!