java每日八股分享

IO流基本认识

数据流是指⼀组有顺序的、有起点和终点的字节集合。 按照流的流向分,可以分为输⼊流和输出流。注意:这⾥的输⼊、输出是针对程序来说的。

输出:把程序(内存)中的内容输出到磁盘、光盘等存储设备中。

输⼊:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。

按处理数据单位不同分为字节流和字符流。字节流:每次读取(写出)⼀个字节,当传输的资源⽂件有中 ⽂时,就会出现乱码。

字符流:每次读取(写出)两个字节,有中⽂时,使⽤该流就可以正确传输显示中⽂。 1字符 = 2字节; 1字节(byte) = 8位(bit); ⼀个汉字占两个字节⻓度。

按照流的⻆⾊划分为节点流和处理流。节点流:从或向⼀个特定的地⽅(节点)读写数据。如 FileInputStream。

处理流(包装流):是对⼀个已存在的流的连接和封装,通过所封装的流的功能调⽤实现数据读写。如BufferedReader。处理流的构造⽅法总是要带⼀个其他的流对象做参数。⼀个流对象经过其他流的多 次包装,称为流的链接。

注意:⼀个IO流可以既是输⼊流⼜是字节流⼜或是以其他⽅式分类的流类型,是不冲突的。⽐如 FileInputStream,它既是输⼊流⼜是字节流还是⽂件节点流。

字节流和字符流的区别

1、要把⼀⽚⼆进制数据数据逐⼀输出到某个设备中,或者从某个设备中逐⼀读取⼀⽚⼆进制数据,不管输⼊输出设备是什么,我们要⽤统⼀的⽅式来完成这些操作,⽤⼀种抽象的⽅式进⾏描述,这个抽象描述⽅式起名为IO流,对应的抽象类为OutputStream和InputStream ,不同的实现类就代表不同的输⼊和输出设备,它们都是针对字节进⾏操作的。

2、在应⽤中,经常要完全是字符的⼀段⽂本输出去或读进来,⽤字节流可以吗?计算机中的⼀切最终都是⼆进制的字节形式存在。对于“中国”这些字符,⾸先要得到其对应的字节,然后将字节写⼊到输出流。读取时,⾸先读到的是字节,可是我们要把它显示为字符,我们需要将字节转换成字符。由于这样的需求很⼴泛,⼈家专⻔提供了字符流的包装类。

3、底层设备永远只接受字节数据,有时候要写字符串到底层设备,需要将字符串转成字节再进⾏写⼊。字符流是字节流的包装,字符流则是直接接受字符串,它内部将串转成字节,再写⼊底层设备,这为我们向IO设别写⼊或读取字符串提供了⼀点点⽅便。

4、字符向字节转换时,要注意编码的问题,因为字符串转成字节数组,其实是转成该字符的某种编码的字节形式,读取也是反之的道理。

字节流在操作时本身不会⽤到缓冲区(内存),是⽂件本身直接操作的;⽽字符流在操作时使⽤了缓冲区,通过缓冲区再操作⽂件。

使⽤场景

字节流⼀般⽤来处理图像,视频,以及PPT,Word类型的⽂件。字符流⼀般⽤于处理纯⽂本类型的⽂ 件,如TXT⽂件等。字节流可以⽤来处理纯⽂本⽂件,但是字符流不能⽤于处理图像视频等⾮⽂本类型的⽂件。

I/O模型

1. BIO(Blocking I/O阻塞I/O模型):

BIO 属于同步阻塞 IO 模型,读取或写⼊数据时,线程将⼀直等待,直到数据准备就绪或者写⼊操作完成,但在⾼并发环境下可能导致性能问题,因为线程在等待 I/O 操作完成时被阻塞,⽆法执⾏其他任务。

2. NIO(Non-blocking I/O):

在⾮阻塞 I/O 模型中,线程执⾏⼀个 I/O 操作时不会等待,⽽是继续执⾏其他任务, 这需要通过轮询(polling)或者使⽤回调函数等机制来检查 I/O 操作是否完成。

这种模型相对于阻塞 I/O 可以更好地⽀持并发,但轮询的⽅式可能会导致 CPU 资源的浪费。

3. IO多路复⽤

I/O 多路复⽤模型使⽤了操作系统提供的选择器(Selector)机制,例如 Java 中的 Selector 类。通过选择器,⼀个线程可以监听多个通道上的 I/O 事件,从⽽在单线程中处理多个连接。

4. AIO(Asynchronous I/O)

异步I/O允许程序在执⾏I/O操作时继续执⾏其他任务,⽽不需要等待I/O操作完成。在Java中,AIO主要是通过JavaNIO.2中的 AsynchronousChannel 和 CompletionHandler 接⼝来实现的。

AIO 允许程序发起⼀个I/O操作,并在操作完成时得到通知。在这个过程中,程序可以继续执⾏其他任务⽽⽆需等待I/O操作完成,当操作完成之后,进⾏回调

BIO/NI0/AIO的区别

1. BIO

⼯作原理: 阻塞 I/O 模型中,当⼀个线程执⾏ I/O 操作时,它会被阻塞,直到 I/O 操作完成。这会导致线程⽆法执⾏其他任务。

适⽤场景: 适⽤于连接数较少、并发不⾼的场景,例如传统的 Socket 编程。

2. NIO

⼯作原理: ⾮阻塞 I/O 模型中,⼀个线程可以处理多个连接,⽽不需要等待每个连接的 I/O 操作完成。但线程需要通过轮询(polling)或者选择器(Selector)来检查哪些连接已经准备好进⾏ I/O 操作。

适⽤场景: 适⽤于⾼并发、连接数较多的场景。Java NIO 提供了 Selector 、 Channel 等组件,可以更好地⽀持多连接的管理。

3. AIO

⼯作原理: 异步 I/O 模型中,程序发起 I/O 操作后,可以继续执⾏其他任务。当 I/O 操作完成时,系统会通知程序,并调⽤相应的回调函数。

适⽤场景: 适⽤于需要处理⼤量并发连接的场景,并且希望充分利⽤系统资源。Java NIO.2 提供了 AIO ⽀持,通过 AsynchronousChannel 和 CompletionHandler 实现异步 I/O 操作。

总结:

BIO: 适⽤于连接数较少、并发不⾼的情况,简单易⽤。

NIO: 适⽤于⾼并发、连接数较多的⽹络应⽤,通过选择器实现⾮阻塞 I/O。

AIO: 适⽤于需要处理⼤量并发连接、希望充分利⽤系统资源的情况,通过异步操作实现。

NIO的实现原理

Java NIO的实现原理主要涉及到以下⼏个核⼼概念和组件:

Channel(通道): Channel 是 NIO 中的⼀个抽象概念,它类似于传统的流,但更加灵活。 Channel 可以是读、写或者读写的,并且可以异步地进⾏ I/O 操作。Channel有好⼏种类型,其中⽐较常⽤的有FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel等,这些通道涵盖了UDP和TCP⽹络IO以及⽂件IO。

Buffer(缓冲区): Buffer 是 NIO 中⽤于存储数据的容器。 Channel 从 Buffer 中读取数据,将数据写⼊到 Buffer 中。 Buffer 提供了对数据的结构化访问,使得读写操作更为灵活和⾼效。Java NIO⾥关键的Buffer实现有CharBuffer、ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。这些Buffer覆盖了你能通过IO发送的基本数据类型,即byte、short、int、long、float、double、char。Buffer对象包含三个重要的属性,分别是capacity、position、limit。

Selector(选择器): Selector 是 NIO 的关键组件之⼀。它允许⼀个线程同时监控多个 Channel ,当其中的某个 Channel 发⽣读或写事件时,可以通过 Selector 得到通知。这样⼀个线程可以有效地管理多个⽹络连接。

⼯作原理可以分为以下⼏个步骤:

打开 Channel: 通过 FileChannel 、 SocketChannel 、 ServerSocketChannel 等类的静态⽅法open() 打开⼀个通道。

创建 Buffer: 创建⼀个或多个 Buffer ,⽤于读取或写⼊数据。

将数据写⼊ Channel: 将数据写⼊ Buffer ,然后将 Buffer 中的数据写⼊ Channel 。

从 Channel 读取数据: 将 Channel 中的数据读取到 Buffer 中。

注册 Channel 到 Selector: 通过 Selector 监听⼀个或多个 Channel ,当 Channel 上发⽣感兴趣的事件时, Selector 将通知程序。

处理事件: 在⼀个循环中调⽤ Selector 的 select() ⽅法,该⽅法会阻塞直到⾄少⼀个注册的 Channel发⽣了感兴趣的事件。然后通过迭代 selectedKeys() 获取 SelectionKey ,从⽽得知哪个 Channel 上发⽣了事件。

介绍⼀下Java的序列化与反序列化

序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在⽹络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中,对象的序列化(Serialize),是指将⼀个Java对象写⼊IO流中,对象的反序列化(Deserialize),则是指从IO流中恢复该Java对象。

若对象要⽀持序列化机制,则它的类需要实现Serializable接⼝,该接⼝是⼀个标记接⼝,它没有提供任何⽅法,只是标明该类是可以序列化的,Java的很多类已经实现了Serializable接⼝,如包装类、String、Date等。

若要实现序列化,则需要使⽤对象流ObjectInputStream和ObjectOutputStream。其中,在序列化时需要调⽤ObjectOutputStream对象的writeObject()⽅法,以输出对象序列。在反序列化时需要调⽤ObjectInputStream对象的readObject()⽅法,将对象序列恢复为对象。

多线程

什么是进程和线程

进程是系统运⾏程序的基本单位,在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main函数所在的线程就是这个进程中的⼀个线程,也称主线程。

线程是进程中的⼀个执⾏单元。⼀个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源。线程是操作系统调度的最⼩单位,它负责执⾏进程中的任务,但是线程的并发执⾏也可能导致⼀些问题,如竞态条件、死锁等。

可以用“工厂-车间-工人”的模型来生动形象地理解。进程相当于工厂中的车间,是系统资源分配和调度的基本单位;而线程则相当于车间内的工人,是系统调度的最小单位。

Java创建线程有⼏种⽅式

1. 继承⾃ Thread 类

通过继承 Thread 类,可以创建⼀个新的线程。为了实现线程的执⾏逻辑,需要重写 run() ⽅法。

  1. 实现 Runnable 接⼝

3. 使⽤ Executor 框架

Executor 框架是 Java 并发编程中的⾼级⼯具,它提供了⼀种更为灵活的⽅式来管理和执⾏线程。通过Executor ,可以将任务提交给线程池,由线程池来管理线程的⽣命周期和执⾏。

线程的⽣命周期

1. 新建( New )

线程对象被创建,但尚未启动。使⽤ new Thread() 创建线程对象后,线程处于新建状态。

2. 就绪( Runnable )

线程已经被启动,等待系统资源(如 CPU 时间)以便运⾏, 调⽤ start() ⽅法后,线程进⼊就绪状态。

3. 运⾏( Running )

就绪状态的线程获得 CPU 时间,开始执⾏ run() ⽅法中的代码。正在执⾏的线程处于运⾏状态。

4. 阻塞(Blocked):

线程因为某些原因放弃了 CPU 使⽤权,暂时停⽌运⾏。可能是等待某个资源、等待 I/O 操作完成、或者调⽤sleep() ⽅法等。

  1. 等待(Waiting):

线程进⼊等待状态,等待其他线程的通知唤醒。可以通过Object.wait() 、 Thread.join() 、 LockSupport.park() 等⽅式进⼊等待状态。

6. 超时等待(Timed Waiting):

线程等待⼀段时间,当时间到达或者其他条件满⾜时,线程会重新进⼊就绪状态。通过

Thread.sleep() 、 Object.wait(timeout) 、 Thread.join(timeout) 、 LockSupport.parkNanos() 等⽅式可进⼊超时等待状态。

7. 终⽌(Terminated):

线程执⾏完 run() ⽅法或者因异常退出后,进⼊终⽌状态。⼀个终⽌的线程不能再进⼊任何状态。

Java中将操作系统中的

就绪状态/运⾏状态转化为⼀个状态Runnable

阻塞状态 细分为了三种

BlOCKED

WAITING

TIMED_WAITING

什么是线程死锁

在典型的线程死锁情况下,每个线程都持有⼀些资源,并且正在等待获取其他线程持有的资源。由于每个线程都在等待,没有⼀个线程能够继续执⾏,这样整个程序就被阻塞了。

线程死锁的产⽣通常需要满⾜以下四个条件,称为死锁的必要条件:

  1. 互斥条件(Mutual Exclusion):指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。

2. 占有且等待条件(Hold and Wait):指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

3. ⾮抢占条件(No Preemption):指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

4. 循环等待条件(Circular Wait):指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。

如何预防和避免线程死锁

如何预防死锁? 破坏死锁的产⽣的必要条件即可:

破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁

破坏请求与保持条件:⼀次性申请所有的资源。采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其他的资源。

破坏不剥夺条件:占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件:靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。

你知道Java中有哪些锁吗

公平锁/⾮公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。 ⾮公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程⽐先申请的线程优 先获取锁。有可能,会造成优先级反转或者饥饿现象。

对于Java ReentrantLock ⽽⾔,通过构造函数指定该锁是否是公平锁,默认是⾮公平锁。⾮公平锁的 优点在于吞吐量⽐公平锁⼤。

对于 Synchronized ⽽⾔,也是⼀种⾮公平锁。由于其并不像 ReentrantLock 是通过AQS的来实现线 程调度,所以并没有任何办法使其变成公平锁。

可重⼊锁

可重⼊锁⼜名递归锁,是指在同⼀个线程在外层⽅法获取锁的时候,在进⼊内层⽅法会⾃动获取锁。说 的有点抽象,下⾯会有⼀个代码的示例。

对于Java ReentrantLock ⽽⾔, 他的名字就可以看出是⼀个可重⼊锁,其名字是 Re entrant Lock 重 新进⼊锁。对于 Synchronized ⽽⾔,也是⼀个可重⼊锁。可重⼊锁的⼀个好处是可⼀定程度避免死锁。

synchronized void setA() throws Exception{

 Thread.sleep(1000);

 setB();

}

synchronized void setB() throws Exception{

 Thread.sleep(1000);

}

上⾯的代码就是⼀个可重⼊锁的⼀个特点,如果不是可重⼊锁的话,setB可能不会被当前线程执⾏,可 能造成死锁。

独享锁/共享锁

独享锁是指该锁⼀次只能被⼀个线程所持有。共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock ⽽⾔,其是独享锁。但是对于Lock的另⼀个实现类 ReadWriteLock ,其读锁是共享锁,其写锁是独享锁。

读锁的共享锁可保证并发读是⾮常⾼效的,读写,写读 ,写写的过程是互斥的。 独享锁与共享锁也是通过AQS来实现的,通过实现不同的⽅法,来实现独享或者共享。

对于 Synchronized ⽽⾔,当然是独享锁。

互斥锁/读写锁

上⾯讲的独享锁/共享锁就是⼀种⼴义的说法,互斥锁/读写锁就是具体的实现。

互斥锁在Java中的具体实现就是 ReentrantLock

读写锁在Java中的具体实现就是 ReadWriteLock

乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,⽽是指看待并发同步的⻆度。

悲观锁认为对于同⼀个数据的并发操作,⼀定是会发⽣修改的,哪怕没有修改,也会认为修改。因此对于同⼀个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作⼀定会出问题。

乐观锁则认为对于同⼀个数据的并发操作,是不会发⽣修改的。在更新数据的时候,会采⽤尝试更新,不断重新的⽅式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

从上⾯的描述我们可以看出,悲观锁适合写操作⾮常多的场景,乐观锁适合读操作⾮常多的场景,不加 锁会带来⼤量的性能提升。

悲观锁在Java中的使⽤,就是利⽤各种锁。

乐观锁在Java中的使⽤,是⽆锁编程,常常采⽤的是CAS算法,典型的例⼦就是原⼦类,通过CAS⾃旋 实现原⼦操作的更新。

分段锁

分段锁其实是⼀种锁的设计,并不是具体的⼀种锁,对于 ConcurrentHashMap ⽽⾔,其并发的实现就是通过分段锁的形式来实现⾼效的并发操作。

我们以 ConcurrentHashMap 来说⼀下分段锁的含义以及设计思想, ConcurrentHashMap 中的分段锁 称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有⼀个 Entry数组,数组中的每个元素⼜是⼀个链表;同时⼜是⼀个ReentrantLock(Segment继承了 ReentrantLock)。

当需要put元素的时候,并不是对整个hashmap进⾏加锁,⽽是先通过hashcode来知道他要放在那⼀ 个分段中,然后对这个分段进⾏加锁,所以当多线程put的时候,只要不是放在⼀个分段中,就实现了真正的并⾏的插⼊。

但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计⽬的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的⼀项进⾏加锁操作。

偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对 Synchronized 。在Java 5通过引⼊锁升级的机制来实现⾼效 Synchronized 。

这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁是指⼀段同步代码⼀直被⼀个线程所访问,那么该线程会⾃动获取锁。降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另⼀个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过⾃旋的形式尝试获取锁,不会阻塞,提⾼性能。

重量级锁是指当锁为轻量级锁的时候,另⼀个线程虽然是⾃旋,但⾃旋不会⼀直持续下去,当⾃旋⼀定次数的时候,还没有获取到锁,就会进⼊阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进⼊阻塞,性能降低。

⾃旋锁

在Java中,⾃旋锁是指尝试获取锁的线程不会⽴即阻塞,⽽是采⽤循环的⽅式去尝试获取锁,这样的好 处是减少线程上下⽂切换的消耗,缺点是循环会消耗CPU。

谈谈你对volatile关键字的理解

volatile是一个java虚拟机提供的轻量级的同步机制,保证可见性和禁止指令重排,但不保证原子性。

volatile 通常被⽐喻成"轻量级的 synchronized",也是Java并发编程中⽐较重要的⼀个关键字。

和 synchronized 不同,volatile 是⼀个变量修饰符,只能⽤来修饰变量。⽆法修饰⽅法及代码块等。

被volatile修饰的共享变量,就具有了以下两点特性:

1保证了不同线程对该变量操作的内存可⻅性

2禁⽌指令重排序

volatile是java虚拟机提供的轻量级的同步机制具有以下特点:

保证可⻅性:可⻅性是指当多个线程访问同⼀个变量时,⼀个线程修改了这个变量的值,其他线程能够⽴即看得到 修改的值。如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使⽤它都到主存中进⾏读取。

不保证原⼦性

禁⽌指令重排

在讲volatile关键字之前,我需要先讲讲java的内存模型,我们的java的变量都存储在主内存当中,每当有一个线程需要读取内存中的变量的时候,java虚拟机会将主内存中的变量拷贝一份放入线程的工作内存中,多个线程之间并不可见,如果我们要保证可见性,就得使用volatile关键字,volatile可以保证变量的可见性,通过通知的方式让其他线程可见,但volatile并不保证变量的原子性,如果要保证变量的原子性,我们可以使用sync关键字或者atomic类,同时volatile可以禁止指令重排,因为编译器和cpu会对我们的代码进行优化,在一些数据没有依赖性的时候,我们的代码执行顺序可能不是我们想象的那样,在多线程情况下,可能会造成一些奇怪的错误,volatile实现指令重排的底层是使用了一个cpu指令叫内存屏障,通过内存屏障的前后指令会不进行指令重排,同时会强制刷新cpu缓存。

4种线程锁

1. .synchronized

在Java中synchronized关键字被常⽤于维护数据⼀致性。

synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对 共享资源的访问都是顺序的。

Java开发⼈员都认识synchronized,使⽤它来实现多线程的同步操作是⾮常简单的,只要在需要同步的 对⽅的⽅法、类或代码块中加⼊该关键字,它能够保证在同⼀个时刻最多只有⼀个线程执⾏同⼀个对象 的同步代码,可保证修饰的代码在执⾏过程中不会被其他线程⼲扰。使⽤synchronized修饰的代码具有 原⼦性和可⻅性,在需要进程同步的程序中使⽤的频率⾮常⾼,可以满⾜⼀般的进程同步要求。

synchronized (obj) { //⽅法

.......

}

synchronized实现的机理依赖于软件层⾯上的JVM,因此其性能会随着Java版本的不断升级⽽提⾼。 到了Java1.6,synchronized进⾏了很多的优化,有适应⾃旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提⾼。在之后推出的Java1.7与1.8中,均对该关键字的实现机理做了优化。 需要说明的是,当线程通过synchronized等待锁时是不能被Thread.interrupt()中断的,因此程序设计时必须检查确保合理,否则可能

会造成线程死锁的尴尬境地。

最后,尽管Java实现的锁机制有很多种,并且有些锁机制性能也⽐synchronized⾼,但还是强烈推荐在 多线程应⽤程序中使⽤该关键字,因为实现⽅便,后续⼯作由JVM来完成,可靠性⾼。只有在确定锁机 制是当前多线程程序的性能瓶颈时,才考虑使⽤其他机制,如ReentrantLock等。

2.ReentrantLock

可重⼊锁,顾名思义,这个锁可以被线程多次重复进⼊进⾏获取操作。

ReentantLock继承接⼝Lock并实现了接⼝中定义的⽅法,除了能完成synchronized所能完成的所有⼯作 外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的⽅法。

Lock实现的机理依赖于特殊的CPU指定,可以认为不受JVM的约束,并可以通过其他语⾔平台来完成底 层的实现。在并发量较⼩的多线程应⽤程序中,ReentrantLock与synchronized性能相差⽆⼏,但在⾼ 并发量的条件下,synchronized性能会迅速下降⼏⼗倍,⽽ReentrantLock的性能却能依然维持⼀个⽔ 准。因此我们建议在⾼并发量情况下使⽤ReentrantLock。

ReentrantLock引⼊两个概念:公平锁与⾮公平锁。

公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁。反之,JVM按 随机、就近原则分配锁的机制则称为不公平锁。

ReentrantLock在构造函数中提供了是否公平锁的初始化⽅式,默认为⾮公平锁。这是因为,⾮公平锁 实际执⾏的效率要远远超出公平锁,除⾮程序有特殊需要,否则最常⽤⾮公平锁的分配机制。

Lock lock = new ReentrantLock();

try {

lock.lock();

//...进⾏任务操作5

}

finally {

lock.unlock();

}

3. Semaphore

上述两种锁机制类型都是“互斥锁”,学过操作系统的都知道,互斥是进程同步关系的⼀种特殊情况,相当于只存在⼀个临界资源,因此同时最多只能给⼀个线程提供服务。但是,在实际复杂的多线程应⽤程序中,可能存在多个临界资源,这时候我们可以借助Semaphore信号量来完成多个临界资源的访问。

Semaphore基本能完成ReentrantLock的所有⼯作,使⽤⽅法也与之类似,通过acquire()与release()⽅法来获得和释放临界资源。

经实测,Semaphone.acquire()⽅法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作⽤效果⼀致,也就是说在等待临界资源的过程中可以被Thread.interrupt()⽅法中断。

此外,Semaphore也实现了可轮询的锁请求与定时锁的功能,除了⽅法名tryAcquire与tryLock不同,其 使⽤⽅法与ReentrantLock⼏乎⼀致。Semaphore也提供了公平与⾮公平锁的机制,也可在构造函数中 进⾏设定。

Semaphore的锁释放操作也由⼿动进⾏,因此与ReentrantLock⼀样,为避免线程因抛出异常⽽⽆法正 常释放锁的情况发⽣,释放锁的操作也必须在finally代码块中完成。

4. AtomicInteger

⾸先说明,此处AtomicInteger是⼀系列相同类的代表之⼀,常⻅的还有AtomicLong、AtomicLong等, 他们的实现原理相同,区别在与运算对象类型的不同。

我们知道,在多线程程序中,诸如++i或i++等运算不具有原⼦性,是不安全的线程操作之⼀。通常我们会使⽤synchronized将该操作变成⼀个原⼦操

作,但JVM为此类操作特意提供了⼀些同步类,使得使⽤更⽅便,且使程序运⾏效率变得更⾼。通 过相关资料显示,通常AtomicInteger的性能是ReentantLock的好⼏倍。

线程锁总结

1.synchronized:

在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译 程序通常会尽可能的进⾏优化synchronize,另外可读性⾮常好。

2.ReentrantLock:

在资源竞争不激烈的情形下,性能稍微⽐synchronized差点点。但是当同步⾮常激烈的时候, synchronized的性能⼀下⼦能下降好⼏⼗倍,⽽ReentrantLock确还能维持常态。

⾼并发量情况下使⽤ReentrantLock。

3.Atomic:

和上⾯的类似,不激烈情况下,性能⽐synchronized略逊,⽽激烈的时候,也能维持常态。激烈的时 候,Atomic的性能会优于ReentrantLock⼀倍左右。但是其有⼀个缺点,就是只能同步⼀个值,⼀段代 码中只能出现⼀个

Atomic的变量,多于⼀个同步⽆效。因为他不能在多个Atomic之间同步。

所以,我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进⼀步优化。ReentrantLock 和Atomic如果⽤的不好,不仅不能提⾼性能,还可能带来灾难。

并发容器的原理

Java的集合容器框架中,主要有四⼤类别:List、Set、Queue、Map,⼤家熟知的这些集合类

ArrayList、LinkedList、HashMap这些容器都是⾮线程安全的。

如果有多个线程并发地访问这些容器时,就会出现问题。因此,在编写程序时,在多线程环境下必须要求程序员⼿动地在任何访问到这些容器的地⽅进⾏同步处理,这样导致在使⽤这些容器的时候⾮常地不⽅便。

所以,Java先提供了同步容器供⽤户使⽤。同步容器可以简单地理解为通过synchronized来实现同步的容器,⽐如Vector、Hashtable以及SynchronizedList等容器。

同步容器⾯临的问题

可以通过查看Vector,Hashtable等这些同步容器的实现代码,可以看到这些容器实现线程安全的⽅式 就是将它们的状态封装起来,并在需要同步的⽅法上加上关键字synchronized。

这样做的代价是削弱了并发性,当多个线程共同竞争容器级的锁时,吞吐量就会降低。

例如: HashTable只要有⼀条线程获取了容器的锁之后,其他所有的线程访问同步函数都会被阻塞,因 此同⼀时刻只能有⼀条线程访问同步函数。因此为了解决同步容器的性能问题,所以才有了并发容器。

并发类容器是专⻔针对多线程并发设计的,使⽤了锁分段技术,只对操作的位置进⾏同步操作,但是其 他没有操作的位置其他线程仍然可以访问,提⾼了程序的吞吐量。

采⽤了CAS算法和部分代码使⽤synchronized锁保证线程安全。

并发容器分类

1.ConcurrentHashMap

对应的⾮并发容器:HashMap

⽬标:代替Hashtable、synchronizedMap,⽀持复合操作

原理:JDK6中采⽤⼀种更加细粒度的加锁机制Segment“分段锁”,JDK8中采⽤CAS⽆锁算法。

2.CopyOnWriteArrayList

对应的⾮并发容器:ArrayList

⽬标:代替Vector、synchronizedList

原理:利⽤⾼并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制⼀份新的集合,在新的 集合上⾯修

改,然后将新集合赋值给旧的引⽤,并通过volatile 保证其可⻅性,当然写操作的锁是必不可 少的了。

3.CopyOnWriteArraySet

对应的⾮并发容器:HashSet

⽬标:代替synchronizedSet

原理:基于CopyOnWriteArrayList实现,其唯⼀的不同是在add时调⽤的是CopyOnWriteArrayList的 addIfAbsent⽅法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有 则放⼊Object数组的尾部,并返回。

4.ConcurrentSkipListMap

对应的⾮并发容器:TreeMap

⽬标:代替synchronizedSortedMap(TreeMap)

原理:Skip list(跳表)是⼀种可以代替平衡树的数据结构,默认是按照Key值升序的。

5.ConcurrentSkipListSet

对应的⾮并发容器:TreeSet

⽬标:代替synchronizedSortedSet

原理:内部基于ConcurrentSkipListMap实现

6.ConcurrentLinkedQueue

不会阻塞的队列

对应的⾮并发容器:Queue

原理:基于链表实现的FIFO队列(LinkedList的并发版本)

7.LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue

对应的⾮并发容器:BlockingQueue

特点:拓展了Queue,增加了可阻塞的插⼊和获取等操作

原理:通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒

实现类:

LinkedBlockingQueue:基于链表实现的可阻塞的FIFO队列

ArrayBlockingQueue:基于数组实现的可阻塞的FIFO队列

PriorityBlockingQueue:按优先级排序的队列

什么是线程池

线程池是⼀种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后⾃动启动这些任务。线程池线程都是后台线程。每个线程都使⽤默认的堆栈⼤⼩,以默认的优先级运⾏,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插⼊另⼀个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的⼯作,则线程池将在⼀段时间后创建另⼀个辅助线程但线程的数⽬永远不会超过最⼤值。超过最⼤值的线程可以排队,但他们要等到其他线程完成后才启动。

为什么需要线程池

我们有两种常⻅的创建线程的⽅法,⼀种是继承Thread类,⼀种是实现Runnable的接⼝,Thread类其实也是实现了Runnable接⼝。但是我们创建这两种线程在运⾏结束后都会被虚拟机销毁,如果线程数量多的话,频繁的创建和销毁线程会⼤⼤浪费时间和效率,更重要的是浪费内存。那么有没有⼀种⽅法能让线程运⾏完后不⽴即销毁,⽽是让线程重复使⽤,继续执⾏其他的任务哪?这就是线程池的由来,很好的解决线程的重复利⽤,避免重复开销

线程池的优点?

1)重⽤存在的线程,减少对象创建销毁的开销。

2)可有效的控制最⼤并发线程数,提⾼系统资源的使⽤率,同时避免过多资源竞争,避免堵塞。

3)提供定时执⾏、定期执⾏、单线程、并发数控制等功能。

Java 提供了哪⼏种线程池?

Java 主要提供了下⾯4种线程池

FixedThreadPool:该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。 当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被暂存在 ⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

SingleThreadExecutor:⽅法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。

CachedThreadPool:该⽅法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数 量不确定,但若有空闲线程可以复⽤,则会优先使⽤可复⽤的线程。若所有线程均在⼯作,⼜有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执⾏完毕后,将返回线程池进⾏复⽤。

ScheduledThreadPoolExecutor: 主要⽤来在给定的延迟后运⾏任务,或者定期执⾏任务。

ScheduledThreadPoolExecutor⼜分为:ScheduledThreadPoolExecutor(包含多个线程)和

SingleThreadScheduledExecutor (只包含⼀个线程)两种。

4种线程池各⾃的使⽤场景是什么?

FixedThreadPool:适⽤于为了满⾜资源管理需求,⽽需要限制当前线程数量的应⽤场景。它适 ⽤于负载⽐较重的服务器;

SingleThreadExecutor:适⽤于需要保证顺序地执⾏各个任务并且在任意时间点,不会有多个线程是活动的应⽤场景;

CachedThreadPool: 适⽤于执⾏很多的短期异步任务的⼩程序,或者是负载较轻的服务器;

ScheduledThreadPoolExecutor:适⽤于需要多个后台执⾏周期任务,同时为了满⾜资源管理需 求⽽需要限制后台线程的数量的应⽤场景。

SingleThreadScheduledExecutor:适⽤于需要单个后台线程执⾏周期任务,同时保证顺序地执 ⾏各个任务的应⽤场景。

创建线程池的⽅式

(1) 使⽤ Executors 创建

我们上⾯刚刚提到了 Java 提供的⼏种线程池,通过Executors⼯具类我们可以很轻松的创建我们上⾯说的⼏种线程池。但是实际上我们⼀般都不是直接使⽤Java提供好的线程池,另外在《阿⾥巴巴Java开发⼿册》中强制线程池

不允许使⽤ Executors 去创建,⽽是通过 ThreadPoolExecutor 构造函数的⽅式,这样的处理⽅式让写的同学更加明确线程池的运⾏规则,规避资源耗尽的⻛险。

(2) ThreadPoolExecutor的构造函数创建

我们可以⾃⼰直接调⽤ ThreadPoolExecutor 的构造函数来⾃⼰创建线程池。在创建的同时,给BlockQueue 指定容量就可以了。这种情况下,⼀旦提交的线程数超过当前可⽤线程数时,就会抛出java.util.concurrent.RejectedExecutionException,这是因为当前线程池使⽤的队列是有边界队列,队列已经满了便⽆法继续处理新的请求。但是异常(Exception)总⽐发⽣错误(Error)要好。

(3) 使⽤开源类库

Hollis ⼤佬之前在他的⽂章中也提到了:“除了⾃⼰定义ThreadPoolExecutor外。还有其他⽅法。这个 时候第⼀时间就应该想到开源类库,如apache和guava等。”他推荐使⽤guava提供的 ThreadFactoryBuilder来创建线程池。

Cas

CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:

1、变量内存地址,V表示

2、旧的预期值,A表示

3、准备设置的新值,B表示

当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。

当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新变量值,其他线程均会失败。失败线程会重新尝试或将线程挂起(阻塞)

CAS的缺点主要有3点:

(1)ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。

(2)循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。

(3)只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。

JVM

请介绍⼀下JVM的内存结构

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、⽅法区、程序计数器、本地⽅法栈五个部分。

代码面试题

s1 和 s2 分别被初始化为字符串字面量 "a" 和 "b"。由于它们是字符串字面量,所以它们被存储在字符串常量池中。

s3 是通过连接两个字符串字面量 "a" 和 "b" 得到的,由于这种连接在编译时就能确定,所以 s3 也被直接存储在字符串常量池中,其值就是 "ab"。

s4 是通过连接 s1 和 s2 得到的。但是,这里的连接是在运行时进行的,所以 JVM 会创建一个新的 String 对象在堆内存中,其内容是 "ab",但 s4 引用的是这个新创建的堆内存中的对象。

s5 是直接字符串字面量 "ab",所以它也存储在字符串常量池中。

堆和栈的区别

堆是运⾏时确定内存⼤⼩,⽽栈在编译时即可确定内存⼤⼩

堆内存由⽤户管理( Java中由JVM管理),栈内存会被⾃动释放

栈实现⽅式采⽤数据结构中的栈实现,具有先进后出的顺序特点,堆为⼀块⼀块的内存

栈由于其实现⽅式,在分配速度上⽐堆快的多。分配⼀块栈内存不过是简单的移动⼀个指针

栈为线程私有,⽽堆为线程共享

JVM内存管理

私有数据:

1、程序计数器

程序计数器,记录的是正的是正在执⾏的虚拟机字节码指令的地址。字节码解释器⼯作时,就是通过改变这个计数器的值来选取下⼀条需要执⾏的字节码指令,完成程序的流程控制。

在多线程情况下,每个线程都需要有⼀个独⽴的程序计数器,以便于切换回来后可以恢复到正确的执⾏位置。

2、Java 虚拟机栈

Java 虚拟机栈由⼀个个栈帧组成,每个⽅法被执⾏时,Java 虚拟机都会同步创建⼀个栈帧。

⼀个⽅法从开始被调⽤到执⾏完毕,对应的就是⼀个栈帧在虚拟机中⼊栈到出栈的过程。

栈帧 Stack Frame,⽤于存储 局部变量表,操作数栈,动态连接,⽅法出⼝等信息。

3、局部变量表

存放编译期间可知的各种Java虚拟机基本数据类型,对象引⽤和returnAddress类型(指向了⼀条字节码指令的地址)。

这些数据类型在局部变量表中以局部变量槽来表示,局部变量表所需的内存空间在编译期间完成分配,⽅法运⾏期间,不会改变槽的数量,具体的内存空间还是由虚拟机来决定。

4、本地⽅法栈

为虚拟机使⽤到的本地⽅法native⽅法服务,其余的同 Java 虚拟机栈。

共享数据区

1、Java堆

Java堆在虚拟机启动时创建,是垃圾收集器管理的内存区域。

2、⽅法区

⽅法区⽤于存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。⽅法区与永久代,可以说 ⽅法区是接⼝,是规范,永久代是 HotSpot 虚拟机给出的实现

3、运⾏时常量池

运⾏时常量池是⽅法区的⼀部分,包括类的描述信息和常量池表,其中类的描述信息包括类的版本,字段,⽅法,接⼝。常量池表⽤于存储 编译期⽣成的各种字⾯量与符号引⽤。在类加载后,常量池表存放到⽅法区的运⾏时常量池中。

运⾏时常量池具有动态性,运⾏期间也可以将新的常量放⼊。

对象的创建

1、类加载检查

当 Java 虚拟机遇到⼀条字节码 new 指令时,⾸先将去检查这个指令的参数是否能够在常量池中定位到⼀个类的符号引⽤,并且检查这个符号引⽤代表的类是否已经被加载,解析和初始化过,如果没有,那么就要先执⾏相应的类加载过程。

2、分配内存

在类加载完成后,对象所需要的内存⼤⼩完全确定,可以对新⽣对象分配内存。

分配内存有两种⽅式:

(1)内存规整

如果内存规整,就是指针碰撞,将指针向空闲空间⽅向移动与对象⼤⼩相等的距离

(2)内存不规整

如果内存不规整,那就是空闲列表,维护⼀个列表,记录哪些内存块可⽤,在分配时,从列表中找到⼀块⾜够⼤的空间划分给对象实例,并更新列表上的记录。

划分空间时的并发问题的解决⽅法:

CAS +失败重试,另⼀种是 TLAB :先预先分配⼀块内存,称为本地内存缓冲 Thread Local Allocation Buffer,线程先在⾃⼰的 TLAB 中分配,⽤完了之后,分配新的缓冲区时,进⾏同步锁定。

3、初始化内存空间

分配到的内存空间(对象头除外)全部分配为 0 值,这步保证了对象的实例字段在 Java 代码中可以不赋值就直接使⽤,使得程序可以访问到这些字段的数据类型所对应的零值。

4、进⾏对象头的设置

在对象的对象头中保存⼀些必要的信息:这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC 分代年龄等信息。

5、执⾏构造函数

按照程序员的意愿初始化对象

什么是OOM

OOM,全称“Out Of Memory”,官⽅说明:当JVM因为没有⾜够的内存来为对象分配空间并且垃圾回 收器也已经没有空间可回收时,就会抛出这个error。

常⻅的内存溢出现象

堆内存溢出

在JVM可使⽤的最⼤堆内存可以在启动的时候通过-Xmx参数指定。堆内存溢出是最为常⻅的 内存溢出问题,发⽣堆内存溢出时,JVM会报告如下错误:java.lang.OutOfMemoryError : java heap space。

分析: 内存溢出顾名思义就是,堆内存不够⽤了,如果程序设计的最⼤对内存已经耗尽,那说 明程序设计存在问题,不该申请很多内存的逻辑申请了很多的内存,该释放的对象没有释放

解决⽅式:

检查程序,看是否有死循环或不必要地重复创建⼤量对象。找到原因后,修改程序和算 法

增加Java虚拟机中Xms(初始堆⼤⼩)和Xmx(最⼤堆⼤⼩)参数的⼤⼩。如:set JAVA_OPTS= -

Xms256m -Xmx1024m

对象访问定位

对象的访问的两种⽅式?区别和优缺点

Java 通过栈上的 reference 数据来操作堆上的具体对象。

对象访问的两种⽅式为句柄和直接指针

1、句柄⽅式

Java 堆中将可能会划分除⼀块内存来作为 句柄池,然后 reference 储存句柄池的地址,句柄中包含了指向对象实例数据的指针,和对象类型数据的指针。

2、直接指针

reference 中放的是对象地址

3、优缺点

句柄⽅式⽐较稳定:对象被移动时,只会改变句柄中的实例数据指针,不需要修改 reference 本身。

直接指针的话,少了⼀次指针定位的开销,所以好处就是访问速度更快。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值