1.介绍
JUC(Java Util Concurrent)是Java工具包中提供的并发编程工具集,它位于java.util.concurrent包下,主要用于简化多线程编程、提高程序性能和可维护性。JUC提供了一系列线程安全的数据结构、并发工具类和线程池等。
JUC中一些常用的并发工具和类:
- 并发集合类(Concurrent Collections):JUC提供了一系列线程安全的集合类,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet等,用于替代传统的非线程安全集合类,可以安全地在多线程环境中使用。
- 原子变量类(Atomic Variables):JUC提供了一系列原子操作类,如AtomicInteger、AtomicLong、AtomicReference等,用于在多线程环境中执行原子操作,避免了使用锁的开销,提高了程序的性能。
- 同步工具类(Synchronization Utilities):JUC提供了一些同步工具类,如CountDownLatch、CyclicBarrier、Semaphore等,用于在多线程之间进行协调和同步,实现线程间的等待、通知和同步操作。
- 线程池(Executor Framework):JUC提供了一套灵活强大的线程池框架,包括Executor、ExecutorService、ThreadPoolExecutor等,用于管理和调度线程的执行,提高了程序的性能和资源利用率。
- 并发工具类(Concurrent Utilities):JUC还提供了一些其他的并发工具类,如Lock、ReadWriteLock、Phaser、CompletableFuture等,用于实现更复杂的并发模式和解决特定的并发问题。
2.进程和线程有什么区别?
进程是程序的一次执行过程,拥有独立的地址空间和资源,是操作系统进行资源分配和调度的基本单位;而线程是进程中的一个执行单元,共享进程的地址空间和资源,是操作系统进行调度的基本单位,多个线程共享同一进程的内存空间和资源。
3.多线程和多进程有什么区别?
多线程是在同一进程内部并发执行多个线程,共享同一进程的地址空间和资源,而多进程是在不同进程之间并发执行多个进程,各自拥有独立的地址空间和资源。
4.并发编程中的三大特性(原子性、可见性、有序性)
在并发编程中,有三个重要的特性:原子性(Atomicity)、可见性(Visibility)、有序性(Ordering)。
原子性(Atomicity)
原子性是指操作不可被中断的特性,即要么全部执行成功,要么全部不执行,不存在部分执行的情况。在并发编程中,原子操作是一系列操作,要么全部执行成功,要么全部失败,不会被其他线程中断或干扰。
可见性(Visibility)
可见性是指一个线程对共享变量的修改能够被其他线程立即感知到的特性。在并发编程中,如果一个线程修改了共享变量的值,其他线程应该能够立即看到这个变化,而不是在很长一段时间内仍然保持旧值。
有序性(Ordering)
有序性是指程序执行的顺序与代码中的顺序一致的特性。在单线程环境下,程序按照代码中的顺序依次执行;但在并发编程中,由于指令重排等原因,可能会导致程序执行顺序与代码中的顺序不一致。在并发编程中,有序性需要通过同步机制来保证,例如使用锁、volatile关键字、原子操作等。
5.Java的内存模型和怎么保证三种特性?
Java的内存模型(Java Memory Model,JMM)定义了Java程序中线程之间如何访问共享内存的规范,保证了线程安全性、原子性、可见性和有序性等特性。
线程安全性(Thread Safety)
Java内存模型通过各种同步机制(如synchronized关键字、Lock接口、volatile关键字、原子类等)来保证线程安全性。
synchronized关键字可以对代码块或方法进行加锁,确保同一时间只有一个线程可以执行被锁定的代码,从而避免了多线程并发访问共享资源时的竞态条件。
volatile关键字用于声明变量,保证了变量的可见性和有序性,但不保证原子性。
原子类(如AtomicInteger、AtomicLong等)提供了一系列的原子操作,确保了对共享变量的操作是原子的,不会被中断。
原子性(Atomicity)
Java内存模型通过原子类、synchronized关键字等机制来保证对共享变量的操作是原子的。
原子类(Atomic类)提供了一系列的原子操作,如compareAndSet、incrementAndGet、decrementAndGet等,确保了对共享变量的操作是不可分割的,要么全部成功,要么全部失败。
可见性(Visibility)
Java内存模型通过volatile关键字来保证共享变量的可见性。
当一个变量被volatile修饰时,对这个变量的写操作会立即被其他线程所看到,而不会出现缓存一致性的问题,保证了共享变量的修改对其他线程的立即可见性。
有序性(Ordering)
Java内存模型通过volatile关键字和synchronized关键字来保证有序性。
volatile关键字保证了对volatile变量的读写操作都是按照程序中的顺序进行的,不会出现重排序的情况。
synchronized关键字不仅保证了临界区内代码的有序执行,还通过内存屏障(Memory Barrier)来保证了临界区前后的内存操作不会被重排序。
6.volatile关键字是什么?
volatile是Java中的一个关键字,用于修饰变量,其主要作用是保证变量的可见性和禁止指令重排序,但并不保证原子性。 因此,在多线程环境下,volatile适用于某个变量的写操作不依赖于当前值的情况,而且变量的写操作不会与其他变量的操作存在依赖关系。
可见性(Visibility)
当一个变量被volatile修饰时,对这个变量的修改会立即被其他线程所看到,而不会出现缓存一致性的问题。这是因为volatile变量的写操作会立即被刷新到主内存中,并且对volatile变量的读操作都是直接从主内存中读取的,而不是从线程的工作内存中读取。
禁止指令重排序(Prevent Instruction Reordering)
volatile关键字还可以防止指令重排序,即被volatile修饰的变量在读写操作时会被插入内存屏障(Memory Barrier),防止编译器和处理器对其进行重排序优化。这样可以保证变量的修改顺序和代码中的顺序一致,避免了可能导致数据不一致的重排序情况。
不保证原子性(No Atomicity)
尽管volatile可以保证变量的可见性和禁止指令重排序,但它并不保证对变量的操作是原子的。如果一个变量的操作需要保证原子性,例如自增或自减操作,需要使用synchronized关键字或java.util.concurrent.atomic包下的原子类来保证。
7.Synchronized关键字是什么?
synchronized关键字是Java中实现同步锁机制的一种方式,通过对共享资源的加锁和释放锁来保证了线程安全,避免了多线程并发访问时可能出现的竞态条件和数据不一致问题。
实现原理:
synchronized关键字可以用来修饰方法或代码块,在多线程环境下,当一个线程执行synchronized修饰的方法或代码块时,会尝试获取对象的锁(或者类的锁),如果获取成功,则可以执行对应的代码,如果获取失败,则会被阻塞,直到获取到锁为止。
当线程执行完synchronized修饰的方法或代码块时,会释放对象的锁,其他等待的线程可以继续竞争锁。
保证原子性
synchronized关键字可以保证被修饰的方法或代码块的原子性,即同一时间只有一个线程可以执行该方法或代码块,避免了多线程并发访问共享资源时可能出现的竞态条件和数据不一致问题。
通过synchronized关键字保护的代码块,在同一时间只允许一个线程执行,其他线程需要等待前一个线程执行完毕释放锁后才能执行,从而确保了对共享资源的安全访问。
保证可见性
除了保证原子性外,synchronized关键字还可以保证共享变量的可见性。当一个线程释放锁时,会将修改的共享变量的值刷新到主内存中,而其他线程在获取锁时会从主内存中重新读取该变量的值,确保了共享变量的修改对其他线程的可见性。
锁的粒度
synchronized关键字可以修饰方法、静态方法、代码块,以及类对象,因此可以根据实际需求选择合适的锁粒度。
方法级别的同步锁适用于整个方法需要同步的情况,代码块级别的同步锁适用于部分代码需要同步的情况,静态方法和类对象级别的同步锁适用于静态资源的同步访问。
8.Java中确保线程安全的方法?(Synchronized和Lock、thradlocal和同步,悲观锁和乐观锁CAS)
Java中确保线程安全的方法包括使用synchronized关键字、Lock接口、ThreadLocal类以及悲观锁和乐观锁CAS(Compare and Swap)等。
1.Synchronized和Lock
使用synchronized关键字可以实现简单的同步机制,通过对方法或代码块加锁来确保线程安全。
Lock接口提供了更灵活的锁机制,包括显示加锁和释放锁的操作,可以更精确地控制锁的粒度和获取锁的顺序。
2.ThreadLocal和同步
ThreadLocal类可以实现线程封闭,将变量与线程绑定,确保每个线程访问的是独立的变量副本,从而避免了多线程并发访问时的竞态条件和数据不一致问题。
使用ThreadLocal类可以减少同步的需求,提高程序的并发性能。
3.悲观锁和乐观锁CAS
悲观锁是一种悲观地认为数据会被其他线程修改的锁机制,需要在访问共享资源之前先获取锁,并且在整个操作过程中都持有锁。
乐观锁CAS是一种乐观地认为数据不会被其他线程修改的锁机制,先尝试进行操作,然后通过比较交换(Compare and Swap)来确保操作的原子性。
Java中的乐观锁CAS通常是通过Atomic类实现,例如AtomicInteger、AtomicLong等。
9.自旋锁是什么?
自旋锁是一种基于忙等待(busy-waiting)的锁机制,在尝试获取锁时,线程不会立即被阻塞,而是以循环的方式不断地尝试获取锁,直到获取成功或者达到一定的尝试次数。
自旋锁适用于锁竞争不激烈的情况下,可以减少线程的阻塞和唤醒开销,提高系统的并发性能。但在锁竞争激烈或持有锁时间较长的情况下,应该谨慎使用自旋锁,避免出现性能问题。
自旋锁的基本思想是,当一个线程尝试获取锁时,如果发现锁已经被其他线程持有,它并不立即进入阻塞状态,而是在循环中不断尝试获取锁,直到获取成功或者超过一定的尝试次数后才放弃。
自旋锁的优点
在锁竞争不激烈的情况下,自旋锁可以减少线程的切换次数,避免了线程进入阻塞状态带来的额外开销。自旋锁的等待时间通常很短,不会引起线程上下文切换,适用于锁竞争不激烈、且持有锁的时间较短的情况。
自旋锁的缺点
在锁竞争激烈的情况下,自旋锁可能会导致线程长时间的忙等待,占用CPU资源,降低系统的整体性能。自旋锁不适用于持有锁时间较长或者锁竞争激烈的场景,容易导致线程饥饿和性能下降。
10.线程的5种状态?
Java中的线程有五种状态:新建、就绪、运行、阻塞和死亡。这些状态描述了线程在执行过程中的不同阶段和状态。
1.新建(New): 当线程对象被创建但尚未启动时,线程处于新建状态。即线程对象被创建后,可以调用start()方法启动线程,使其进入就绪状态。
2.就绪(Runnable): 当线程启动后,它可能进入就绪状态,表示线程已经被创建,但尚未分配CPU执行时间。线程处于就绪状态时,可能正在等待系统资源,例如CPU时间片,一旦获取到CPU时间片,就会进入运行状态。
3.运行(Running): 当线程获得CPU时间片并开始执行时,线程处于运行状态。在运行状态下,线程正在执行它的任务代码。
4.阻塞(Blocked): 当线程被阻塞时,表示线程暂时无法执行,通常是由于某些原因导致线程无法继续执行。例如,线程可能因为等待某个输入/输出操作完成、等待获取锁、等待某个条件满足等原因而被阻塞。一旦等待的条件满足,线程就会进入就绪状态,等待系统重新调度。
5.死亡(Terminated): 当线程的run()方法执行完毕或者调用了线程的stop()方法使线程终止时,线程进入死亡状态。在死亡状态下,线程对象已经被销毁,不再存在。
11.sleep和wait有什么不同?
sleep()和wait()都可以用于线程的暂停,但是它们的使用场景和调用方式不同,sleep()主要用于简单的暂停执行,而wait()主要用于线程间的协作。
相同点
都可以暂停线程的执行: sleep()和wait()都可以使当前线程暂停一段时间的执行。
都会释放对象的锁: 调用sleep()或wait()方法时,都会释放对象的锁,让其他线程有机会获取该对象的锁。
不同点
方法来源不同:
sleep()方法来自于Thread类,而wait()方法来自于Object类。
使用场景不同:
sleep()方法通常用于让当前线程暂停执行一段指定的时间,是一种简单的暂停执行方式。例如,可以用于模拟程序中的等待或延迟操作。
wait()方法通常用于线程间的协作,让线程在等待某个条件满足时暂停执行,并释放对象的锁。通常结合条件判断和唤醒操作一起使用。例如,线程A等待某个条件的满足,线程B在条件满足时通过notify()或notifyAll()方法唤醒线程A。
调用方式不同:
sleep()方法是static方法,直接通过Thread.sleep()调用。
wait()方法必须在synchronized同步块或方法中调用,否则会抛出IllegalMonitorStateException异常。
唤醒方式不同:
调用sleep()方法后,线程会在指定时间后自动恢复执行,不需要其他线程的干预。
调用wait()方法后,线程会进入等待状态,需要其他线程调用相同对象上的notify()或notifyAll()方法来唤醒。
12.ThreadLocal是什么?
ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享。
13.ThreadLocal的底层原理
在ThreadLocal内部维护了一个ThreadLocalMap类型的成员变量,用来存储资源对象。
当我们调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中。
当调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值。
当调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值。
14.创建线程有几种方式
1.继承 Thread 类并重写 run 方法创建线程,实现简单但不可以继承其他类
2.实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。
3…实现 Callable 接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常。
4.使用线程池创建(使用 java.util.concurrent.Executor 接口)
15.Runnable 和 Callable 的区别?
Runnable 接口 run 方法无返回值;
Callable 接口 call 方法有返回值,支持泛型Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;
Callable 接口call 方法允许抛出异常,可以获取异常信息
16.如何启动一个新线程、调用 start 和 run 方法的区别?
线程对象调用 run 方法不开启线程。仅是对象调用方法。
线程对象调用 start 开启线程,并让 jvm 调用 run 方法在开启的线程中执行调用 start 方法可以启动线程,并且使得线程进入就绪状态,而 run 方法只是thread的一个普通方法,还是在主线程中执行。
17.线程有哪几种状态以及各种状态之间的转换?
- 第一是 new->新建状态。在生成线程对象,并没有调用该对象的 start 方法,这是线程处于创建状态。
- 第二是 Runnable->就绪状态。当调用了线程对象的 start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。
- 第三是 Running->运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。
- 第四是阻塞状态。阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(1)等待 – 通过调用线程的 wait() 方法,让线程等待某工作的完成。
(2)超时等待 – 通过调用线程的 sleep() 或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
(3)同步阻塞 – 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。 - 第五是 dead->死亡状态: 线程执行完了或者因异常退出了 run()方法,该线程结束生命周期.
18.线程相关的基本方法?
1.线程等待(wait)调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
2.线程睡眠(sleep)sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态.
3.线程让步(yield)yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到CPU时间片,但这又不是绝对的,有的操作系统对 线程优先级并不敏感。
4.线程中断(interrupt)中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)
5.Join 等待其他线程终止join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸.
6.线程唤醒(notify)Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
19.Java中synchronized 和 ReentrantLock 有什么不同?
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的.
区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指
令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于
Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,
ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性
能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。
20.SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步。而
ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要有一个线程访问
map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,
仍然可以对map执行某些操作。
所以,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优
势。同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修
改,也不会抛出ConcurrentModificationException。
21.说说自己是怎么使用 synchronized 关键字,synchronized关键字最主要的三种使用方式:
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
22.常用的线程池有哪些?
newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照
任务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达
到线程池的最大大小。
newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池
大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任
务的需求。
newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务
的需求。
23.简述一下你对线程池的理解
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。