一:面试造火箭,工作拧螺丝
近期将要去找工作面试了,以下是对Java基础,集合,多线程学习过程做的总结(以下知识点基本是通过整理网上知识点+自己总结)
二. Java基础面试题:
-
谈谈你对面向对象的理解?(2021.6.4)
答:面向对象是一个组织者的思维模式,而面向过程是一个从执行者角度的方向考虑问题;一提到面向对象,我们无疑先想到,万物皆对象,还有面向对象最主要的的有三大特性:封装,继承,多态。面向对象的优点:易维护、易复用、易扩展
封装:从狭义上来说就是属性私有化,对外提供get/set方法使用。说详细点就是将一类事物的属性和行为抽象成一个类,使其属性私有化,行为公开化,提高了数据的隐秘性的同时,使代码模块化。这样做使得代码的复用性更高。
继承:则是进一步将一类事物共有的属性和行为抽象成一个父类,而每一个子类是一个特殊的父类–有父类的行为和属性,也有自己特有的行为和属性。这样做扩展了已存在的代码块,进一步提高了代码的复用性。
多态:是父类引用指向子类对象,指向不同的子类对象有不同的表现。多态的一大作用就是为了解耦。 -
JDK 和 JRE 有什么区别?
答:JDK :Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。
JRE :Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境。
其中JDK里面包含了JRE,JRE里面又包含了JVM。(jvm的详细知识点在专属的篇章) -
JVM是什么?你对jvm了解多少?
答:JVM是可运行Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
(.java 文件通过编译器转成 .class 文件,.class文件经过jvm转成特定的机器码) -
== 和 equals 的区别是什么?
答:先说 == ,对于基本类型和引用类型 == 的作用效果是不同的;
基本类型:比较的是值是否相同;(注意:String 不属于基础类型,基础类型有 8 种:byte、boolean、char、short、int、float、long、double,而 String 属于对象。)
引用类型:比较的是引用是否相同;
例子:
String x = “string”;
String y = “string”;
String z = new String(“string”);
System.out.println(xy); // true
System.out.println(xz); // false
System.out.println(x.equals(y)); // true
System.out.println(x.equals(z)); // true
代码解读:因为 x 和 y 指向的是同一个引用,所以 == 也是 true,而 new String()方法则重写开辟了内存空间,所以 == 结果为 false,而 equals 比较的一直是值,所以结果都为 true。
一般情况下:equals 比较的一直是值;
但是:equals 本质上就是 ,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。
总结 : 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等
-
两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
答:hashCode()相等 即 两个键值对的哈希值相等,
然而哈希值相等,并不一定能得出键值对相等。 -
final 在 java 中有什么作用?
答:final修饰的类叫最终类,该类不能被继承。
final修饰的方法不能被重写。
final修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。 -
java 中操作字符串都有哪些类?它们之间有什么区别?
答:操作字符串的类有:String、StringBuffer、StringBuilder。
区别:
String: 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象。
StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的
情况下最好不要使用 String。
StringBuffer: 是线程安全的,多线程环境下推荐使用 StringBuffer。
StringBuilder:是非线程安全的,StringBuilder 的性能却高于 StringBuffer,在单线程环境下推荐使用 StringBuilder -
String 类的常用方法都有那些?
答: -
普通类,抽象类,接口的区别?
答:
普通类:不能包含抽象方法,一个类只能继承一个抽象类,但可以实现多个接口。普通类可以直接实例化。
抽象类:不能直接实例化,拥有抽象方法的类就是抽象类,抽象类要使用abstract关键字声明。抽象类也可以拥有非抽象的方法。抽象类的子类使用 extends 来继承,抽象类可以有构造函数。抽象类中成员变量默认 default,可在子类中被重新定义,也可被重新赋值,抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,如果一个类继承于抽象类,则该子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类。
接口:可以理解为一种特殊的类,里面全部是由全局常量和公共的抽象方法所组成。接口是解决Java无法使用多继承的一种手段,但是接口在实际中更多的作用是制定标准。接口必须使用 implements 来实现接口,接口不能有构造函数,接口中成员变量默认为 public static final 修饰,必须赋初值,不能被修改,其所有的成员方法默认使用 public abstract 修饰。
- java 中 IO 流分为几种?
答:按功能来分:输入流(input)、输出流(output)
按类型来分:字节流和字符流
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。
- BIO、NIO、AIO 有什么区别?
答:
BIO:(Block IO)同步阻塞式 IO,就是我们平常使用的传统 IO,数据的读取写入必须阻塞在一个线程内等待其完成。它的特点是模式简单使用方便,并发处理能力低。(jdk1.4之前)
服务器实现模式为一个连接一个线程
NIO :(New IO) 同步非阻塞 IO,N可以理解为Non-blocking,是传统 IO 的升级:客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。NIO 包含3个核心的组件,分别是Channel(通道),Buffer(缓冲区),Selector(选择器)(jdk1.4)
服务器实现模式为一个请求一个线程
AIO:(Asynchronous IO)异步非阻塞,在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型,异步 IO 的操作基于事件和回调机制,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
服务器实现模式为一个有效请求一个线程
-
重写和重载的区别?
答:重写:方法名、参数、返回值相同。
子类方法不能缩小父类方法的访问权限。
子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。
存在于父类和子类之间。
方法被定义为 final 不能被重写。
实现的是运行时的多态性。
重载:参数类型、个数、顺序至少有一个不相同。
不能重载只有返回值不同的方法名。
存在同类中。(存在争议:说是也可以存在于父类和子类当中)
实现的是编译时的多态性 -
java 容器都有哪些?
答:如下图:
-
Collection 和 Collections 有什么区别?
答:java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。 -
List、Set、Map 之间的区别是什么?
答:如下图:
List:List集合是继承collection接口的,我们经常用到的AbstractList,其子类有ArrayList, vector, LinkedList。List他可以允许元素重复,并且他是有序的,在线程安全方面,vector是线程安全的。其常用到的方法有add(),remove(),clear(),get(),size(),和contains().
Set:他也是继承collection接口,其下有HashSet,TreeSet,还要LinkedHashSet;他跟list的不同是,他是无序,而且不允许重复。其常用到的方法有add(),remove(),clear(),size(),和contains().
Map:其中主要有HashMap和TreeMap,还有Hashtable,其中Hashtable是线程安全的,其元素不可重复。
- HashMap 和 Hashtable 有什么区别?
答:HashMap:去掉了HashTable 的contains方法,但是加上了containsValue()和containsKey()方法。HashMap是非同步的,线程不安全,效率上比hashTable要高,hashMap允许空键值,底层是hash表,不保证有序(比如插入的顺序),其适用于在Map中插入、删除和定位元素这类操作,
hashTable同步的,线程是安全的,但是不允许空键值。
-
说一下 HashMap 的实现原理?
答:HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap的数据结构: 在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。(说白了:jdk1.7时候:数组+链表;jdk1.8后:数组+链表+红黑树)当我们往Hashmap中put元素时,首先根据key的hashcode重新计算hash值,根据hash值得到这个元素在数组中的位置(下标), 如果数组中该位置没有元素,就直接将该元素放到数组的该位置上,如果该数组在该位置上已经存放了其他元素,那么在这个位置上的元素将以链表的形式存放,jdk1.7时候是将新加入的放在链头,最先加入的放入链尾. Jdk1.8后,是将用尾插法,将值插在尾部。
需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过8个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn),当低于6时候,又转回链表 -
ArrayList 和 LinkedList 的区别是什么?
答: ArrayList是基于动态数组实现的,支持随机访问,他的特性就是可以使用索引来提升查询效率;插入和删除数组中某个元素,会导致其后面的元素需要重新调整索引,产生一定的性能消耗;
LinkedList是基于双向链表实现的,没有索引,所以查询效率不高,但是插入和删除效率却很高;因为链表里插入或删除某个元素,只需要调整前后元素的引用即可。 -
谈下HashMap中的Put是如何实现的?
答:先将key和value封装到Node节点,根据key的hashcode重新计算hash值,根据hash值得到这个元素在数组中的位置(下标), 如果数组中该位置没有元素,就直接将该元素放到数组的该位置上,如果该数组在该位置上已经存放了其他元素,那么就将节点中的元素和链表中的每个元素进行equals比对,如果,比对有值相等,也即是true,那么将覆盖这个值,如果一一比对完了,放回是false,那么根据JDK的本版,将元素加入链表(jdk1.7和jdk1.8是不一样的,dk1.7时候是将新加入的放在链头,最先加入的放入链尾. Jdk1.8后,是将用尾插法,将值插入尾部)
需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过8个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn),当低于6时候,又转回链表(红黑树知识点看专门的篇章) -
谈下HashMap中的get是如何实现的?
答; 1、底层会调用key的hashcode()方法,通过hash函数将hash值转换为数组下标,通过数组下标快速定位到数组的指定位置上,如果这个位置上没有任何元素,那么返回null。
2、如果这个位置上有单向链表(该位置上有元素),那么会拿着我们get(key)中的key和单向链表中的每个节点的key进行equals,如果说所有的equals都返回false,那么这个get方法返回false。
3、只要其中有一个节点的key和参数key的equals对比的结果返回true,那么这个节点的value就是我们想要找的value,get方法返回这个value. -
如何实现数组和 List 之间的转换?
答:List转换成为数组:调用ArrayList的toArray方法。
数组转换成为List:调用Arrays的asList方法。 -
Array 和 ArrayList 有何区别?
答:Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
Array是指定大小的,而ArrayList大小是固定的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。 -
在 Queue 中 poll()和 remove()有什么区别?
答:poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。 -
迭代器 Iterator 是什么?
答:迭代器是一种设计模式,它是 Java 中常用的设计模式之一。用于顺序访问集合对象的元素,而又无需知道集合对象的底层实现。迭代器通常被称为“轻量级”对象,因为创建它的代价小。
Iterator 是可以遍历集合的对象,为各种容器提供了公共的操作接口,隔离对容器的遍历操作和底层实现,从而解耦。缺点是增加新的集合类需要对应增加新的迭代器类,迭代器类与集合类成对增加。 -
并行和并发有什么区别?(2021.6.5)
答:并行:并行是指两个或者多个事件在同一时刻发生,并行是在不同实体上的多个事件。
并发:是指两个或多个事件在同一时间间隔发生。并发是在同一实体上的多个事件。 -
线程和进程的区别?
答:进程:进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程:是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的,能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。 -
创建线程有哪几种方式?
答:1. 继承Thread类创建线程类:
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
创建Thread子类的实例,即创建了线程对象。
调用线程对象的start()方法来启动该线程。 -
通过Runnable接口创建线程类:
定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动该线程。 -
通过Callable和Future创建线程:
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
使用FutureTask对象作为Thread对象的target创建并启动新线程。
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。 -
说一下 runnable 和 callable 有什么区别?
答:Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。 -
线程有哪些状态?
答:线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
创建状态:在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
就绪状态:当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
运行状态:线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
阻塞状态:线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
死亡状态:如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪 -
sleep() 和 wait() 有什么区别?
答:sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程
- 公平锁和非公平锁的原理以及区别?
答:
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。优点:所有的线程都能得到资源,不会饿死在队列中。缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,如果能获取到,就直接获取到锁,获取不到,再进入等待队列,优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
-
notify()和 notifyAll()有什么区别?
答:
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。 -
创建线程池有哪几种方式?
答:
第1 newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
第2 newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
第3 newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
第4 newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。 -
线程池中 submit()和 execute()方法有什么区别?
答:第1.接收的参数不一样,execute() 参数 Runnable ;submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable)
第2. execute() 没有返回值;而 submit() 有返回值
第3. submit() 的返回值 Future 调用get方法时,可以捕获处理异常 -
在 java 程序中怎么保证多线程的运行安全?
答:线程安全在三个方面体现:
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
理解:代码的执行顺序看起来是按照书写顺序执行的,但是虚拟机会对代码指令进行重排序,虽然进行了重排序,但是最后执行的结果是与程序顺序执行的结果一致的,他只会对不存在数据依赖性的代码进行重排序。因此在单个线程中,程序看起来是有序执行的,事实上,这个规则是为了保证程序在单线程中是有序执行的,但无法保证程序在多线程环境中执行的正确 -
多线程锁的升级原理是什么?
答:在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。 -
什么是死锁?
答:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。 -
怎么防止死锁?
答:互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源。
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放。
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放。
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系 -
ThreadLocal 是什么?有哪些使用场?
答:ThreadLocal,线程本地变量,顾名思义,它是每个线程私有的本地变量。通俗点讲,当你创建了一个ThreadLocal变量,每个线程在访问该变量时,都会拷贝一个副本至本地内存,所以多线程下操作ThreadLocal变量时,其实各自都是在操作自己拷贝的副本,互不影响,这样自然而然就避免了线程安全问题。
通过 ThreadLocal 的子类 InheritableThreadLocal 可以天然的支持多线程间的信息共享。
ThreadLocal 最典型的使用场景有两个:
第1. ThreadLocal 可以用来管理 Session,因为每个人的信息都是不一样的,所以就很适合用 ThreadLocal 来管理;
第2. 数据库连接,为每一个线程分配一个独立的资源,也适合用 ThreadLocal 来实现。 -
ThreadLocal为什么会发生内存溢出?如何解决呢?
答:ThreadLocal 造成内存溢出的原因:如果 ThreadLocal 没有被直接引用(外部强引用),在 GC(垃圾回收)时,由于 ThreadLocalMap 中的 key 是弱引用,所以一定就会被回收,这样一来 ThreadLocalMap 中就会出现 key 为 null 的 Entry,并且没有办法访问这些数据,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 并且永远无法回收,从而造成内存泄漏。
解决:可以使用代码 threadLocal.remove() ,使用完 ThreadLocal 之后,调用remove() 方法,清除掉 ThreadLocalMap 中的无用数据就可以避免内存溢出了。 -
谈谈你对Synchonized的认识?
答:synchronized是jvm实现的一种互斥同步访问方式,底层是基于每个对象的监视器(monitor)来实现的。被synchronized修饰的代码,在被编译器编译后 会在被修饰的代码前后加上了一组字节指令。
在代码开始加入了monitorenter,在代码后面加入了monitorexit,这两个字节码指令配合完成了synchronized关键字修饰代码的互斥访问。在虚拟机执行到monitorenter指令的时候,会请求获取对象的monitor锁,基于monitor锁又衍生出一个锁计数器的概念。
当执行monitorenter时,若对象未被锁定时,或者当前线程已经拥有了此对象的monitor锁,则锁计数器+1,该线程获取该对象锁。当执行monitorexit时,锁计数器-1,当计数器为0时,此对象锁就被释放了。那么其他阻塞的线程则可以请求获取该monitor锁。
1.若是对象锁,则每个对象都持有一把自己的独一无二的锁,且对象之间的锁互不影响 。
2.若是类锁,所有该类的对象共用这把锁。
3.一个线程获取一把锁,没有得到锁的线程只能排队等待;
4.synchronized 是可重入锁,避免很多情况下的死锁发生。
5.synchronized 方法若发生异常,则JVM会自动释放锁。
6.锁对象不能为空,否则抛出NPE(NullPointerException)
7.同步本身是不具备继承性的:即父类的synchronized 方法,子类重写该方法,分情况讨论:没有synchonized修饰,则该子类方法不是线程同步的。(PS :涉及同步继承性的问题要分情况)
8.synchronized本身修饰的范围越小越好。毕竟是同步阻塞。跑不快还占着超车道…
-
ThreadLocal和Synchonized的区别?
答:ThreadLocal 和 Synchonized 都用于解决多线程并发访问,防止任务在共享资源上产生冲突,但是 ThreadLocal 与 Synchronized 有本质的区别,Synchronized 用于实现同步机制,是利用锁的机制使变量或代码块在某一时刻只能被一个线程访问,是一种 “以时间换空间” 的方式;而 ThreadLocal 为每一个线程提供了独立的变量副本,这样每个线程的(变量)操作都是相互隔离的,这是一种 “以空间换时间” 的方式。 -
谈谈关键字volatile(是什么,原理,作用)(2021.6.6)
答:volatile 是一个类型修饰符。
volatile的作用:保证多线程共享变量的可见性和有序性,但volatile只能保证变量的可见性、有序性,但是不能保证原子性。
第1.保证内存可见性:一个线程对一个volatile变量的修改,对于其它线程来说是可见的。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
第2. 禁止指令重排序
volatile的原理:
线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作。这也是导致线程间数据不可见的本质原因。
第1. 可见性的实现:修改volatile变量时会强制将修改后的值刷新的主内存中。修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。
第2. 有序性的实现:volatile是通过编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序的。多核处理器需使用内存屏障指令来确保一致性。
第2.1:属性添加了volatile关键字之后,编译之后的属性会被添加ACC_VOLATILE访问标记。
第2.2:先执行内存屏障之前的代码,再执行内存屏障”之后的代码。
-
指令重排序
答:在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,jvm会对指令执行的顺序进行优化,这样是为了提高执行的效率。但是在单线程的情况下指令的执行先后没有关系,但是在多线程的情况下这些指令的执行顺序就是对其他线程产生很大的影响。重排序分3种类型,分别为:
编译器优化重排序(编译器在不改变单线程程序语义的前提下,可以重新安排语句 的执行顺序。)、
指令级并行重排序、(现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。)
内存系统重排序(由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。)。 -
synchronized 和 volatile 的区别是什么?
答: 第1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
第2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别
第3. volatile仅能实现变量的可见性,有序性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
第4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
第5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。 -
synchronized 和 ReentrantLock 区别是什么?
答:
java5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁的功能,它提供了与synchronized关键字类似的同步功能。既然有了synchronized这种内置的锁功能,为何要新增Lock接口?先来想象一个场景:手把手的进行锁获取和释放,先获得锁A,然后再获取锁B,当获取锁B后释放锁A同时获取锁C,当锁C获取后,再释放锁B同时获取锁D,以此类推,这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却显得容易许多。
ReentrantLock常常对比着synchronized来分析,我们先对比着来看然后再一点一点分析。
-
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。
-
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。AQS:AbstractQueuedSynchronizer的简称,是一个用于构建锁和同步容器的框架
-
可重入锁:可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
-
可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
-
公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。
-
说说你对Atomic的理解。
答:Atomic :指一个操作是不可中断的,即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。