🚩创建线程有几种方式?
创建线程有两种方式:
- 继承Thread类:重写run方法,它的缺点是无法继承其它的类
- 实现Runnable接口:重写run方法,将实现类作为参数传入Thread类的构造方法中,实现了解耦操作,适合多线程访问同一个共享资源。
🚩线程有哪几种状态?
线程总共有六种状态,分别是:
- 新建状态(New):表示新创建出的线程,此时线程还没有开始执行
- 可运行状态(Runnable):线程创建后,被调用了start方法启动后开始执行的状态,表示可以执行或正在执行,取决于CPU对其的调度。
- 阻塞状态(Blocked):如果线程在执行是遇到synchronized关键字的同步代码块,没有竞争到锁,就进入阻塞状态,或者调用sleep()方法后也会进入阻塞状态
- 等待状态(Waiting):当对象调用wait()方法时,拥有该对象锁的线程就进入等待状态。进入等待状态后,需要其它线程调用notify()或notifyAll()来对线程进行唤醒。或者调用Thread.join()方法也会进入等待状态
- 计时等待(Time_Waiting):同样也是进入了等待状态,但这种状态下,线程会有一个时间参数,超出时间或者接受到通知都会切换到可运行状态
- 终止状态(Terminated):当线程执行完毕后进入该状态
🚩sleep()和wait()的区别?
- sleep()是Thread类的静态方法,使得某一个线程调用后进入睡眠,状态变为阻塞状态。睡眠时间结束后,进入可运行状态。调用后会释放CPU,但不会释放持有对象的锁
- wait() 是Object类的方法,每一个对象有一个锁,对象调用该方法后,拥有锁的线程就进入等待状态,需要其它线程唤醒。调用后会释放CPU,也会释放持有的锁,只有再次竞争到锁,才可以进入可运行状态。
🚩yield()和join的区别?
- yield()是Thread类的一个静态方法,它的作用是让出CPU给其它线程,自己回到Runnable状态,等待重新分配CPU
- join()是Thread类的一个实例方法,它的作用是当前运行的线程进入阻塞状态,等待调用join()方法的线程执行完毕后再转化为就绪态等待重新分配时间片
🚩run()和start()方法有什么区别?
- start()方法用于启动多线程,调用start方法后,会在后台创建一个新的线程来执行,不需要等待run方法执行完毕就可以继续执行其它代码。
- run()方法也是线程体,包含了要执行的线程的逻辑代码,调用run方法后不会创建新的线程,而是直接运行run方法中的代码。
🚩怎么中断一个线程呢?
- 每一个线程都有一个中断标志位,初始为false表示没有被中断,使用interrupt方法会向线程发送一个终止信号,改变线程的标志位为true
- 使用
isInterrupted()
方法可以检查这个线程是否被中断,不会影响标志位。interrupted()
方法也可检测线程是否中断,再调用该方法后,如果线程标志位是true会重置为false - 对一个处在time-waiting状态的线程,使用中断会抛出异常,抛出异常后会重置标志位为false
🚩什么是线程安全问题?如何解决?
在多线程的情况下,由于每个线程都有个工作内存,操作数据时会首先将数据从主内存拷贝到工作内存中。所以如果多个线程都要对同一个变量进行修改时,就会产生数据不一致的问题。
如果要解决这种问题可以对数据进行加锁操作,使用synchronized关键字,或使用显式的Lock锁来保证每次只有一个线程对变量进行修改。
🚩线程池了解吗?
- 线程池是管理一组线程的资源池,我们可以每次将任务加入线程池,让线程池统一分配执行任务。
- 使用线程池可以对线程进行统一的分配和监控,大大的减少了创建和销毁线程的次数,每一个工作线程都可以重复使用,同时由于不需要创建线程所以也提高了任务的响应时间
- 线程有几个核心参数分别是:
(1)核心线程数:当线程数小于核心线程数,加入的任务都会创建一个新的线程
(2)最大线程数:线程池最大允许有的线程数,任务队列满后还没到达最大线程数时就可以创建该线程执行任务
(3)最大等待时间:线程数大于核心线程时,空闲的线程等待的最大时间,超过就终止线程
(4)任务队列:当核心线程都在工作是,再次加入的任务就会加入到任务队列来等待执行
(5)拒绝策略:当任务队列已满,线程池线程数以达到最大线程数就会启用拒绝策略,总共有四种,分别是:直接抛出异常(默认)、直接丢弃、丢弃任务队列中最旧的任务、交给调用任务的线程进行执行。
🚩创建线程池有几种方式?
常见的创建线程池的方式有四种,分别是:
new FixedThreadPool(int nThreads)
:创建一个固定数量的线程池,每次提交一个任务就创建一个工作线程,如果工作线程达到初始数量就放入任务队列new CachedThreadPool
:创建一个可缓存线程池,可灵活回收空闲线程,若无可回收,则新建线程。创建数量几乎没有限制,如果长时间没有往线程池中提交任务,工作线程将自动终止。new SingleThreadExecutor()
:创建一个只有一个线程的线程池,可以保证所有的任务都能按顺序执行到new ScheduleThreadPool()
:创建一个定长的线程池,支持定时、延时的执行任务
这样创建线程要注意使用的任务队列是LinkedBlockingQueue,很容易导致OOM的发生
🚩执行execute()和submit()方法区别是什么呢?
- execute()方法用于提交不用返回值的任务,所以无法判断任务是否被线程池执行成功。它的参数是一个Runnable的实例。
- submit()方法用于提交需要返回值的任务。线程会返回一个future类型的对象,通过这个对象可以判断任务是否执行成功。
🚩了解synchronized关键字吗?
- synchronized 主要是用在多线程并发访问时的一个关键字,它是对对象进行上锁操作使得在某一时刻只有一个线程获取到对象锁而执行共享代码块。它保证了线程间的原子性、可见性、互斥性
- synchronized关键字可以修饰对象、方法和代码块。对于普通同步方法,锁的是当前实例对象;对于静态同步方法,锁的是对应的类对象;对于代码块,锁的是括号中配置的对象
- 修饰代码块时采用的是一个monitor机制,在代码块开始和结束分别加上monitorenter和monitorexit指令,因为synchronized是一个可重入锁,所以还会有一个count计数,避免自己锁自己的情况。修饰方法时会将方法中的
access_flags
中设置ACC_SYNCHRONIZED
标志,如果线程竞争锁,发现已经设置了这个标志,就表示该对象锁已经有线程持有。
🚩了解synchronized锁优化机制吗?
在JDK1.6中新增了轻量级锁和偏向锁的来提高锁的效率。锁会从偏向锁升级到轻量级锁再升级到重量级锁。
- 在只有一个线程访问共享变量时,使用的就是偏向锁,仅仅是在第一次进入同步代码块时使用CAS将线程ID记录到对象头中,下次再访问时,发现对象头中有线程ID就直接获取锁。
- 如果多个线程不在同一时刻访问同步代码块时,锁一般情况下就是轻量级锁。每次没有得到锁时使用CAS竞争替换对象头的指向,但如果始终得不到锁,竞争的线程会自旋消耗CPU
- 最后就是重量级锁,如果多个线程同一时刻访问同步代码块,锁就膨胀为重量级锁,此时没有竞争到锁的线程就会进入阻塞状态。
🚩偏向锁、轻量级锁和重量级锁的区别?
- 偏向锁的优点是加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距,缺点是如果线程间存在锁竞争会带来额外锁撤销的消耗,适用于只有一个线程访问同步代码块的场景。
- 轻量级锁的优点是竞争的线程不会阻塞,提高了程序的响应速度,缺点是如果线程始终得不到锁会自旋消耗CPU,适用于追求响应时间和同步代码块执行非常快的场景。
- 重量级锁的优点是线程竞争不使用自旋不会消耗CPU,缺点是线程会被阻塞,响应时间很慢,适应于追求吞吐量,同步代码块执行较慢的场景
🚩了解volatile关键字吗?
- volatile 关键字是轻量级的synchronized,它使得多线程并发访问时共享变量时直接与主内存进行读写,保证了每个线程读到的都是最新的值,也即保证了可见性。
- 同时使用volatile关键字还可以禁止指令重排序
- volatile关键字的实践作用就是同CAS结合保证原子性,atomic包下的类就是这样使用的
🚩volatile的底层是怎么实现的?
有volatile修饰的共享变量在进行写操作时的汇编代码是具有lock前缀的指令,lock前缀的指令在多核处理器下会引发两件事:
- 将当前处理器缓存行的数据写回到系统内存。
- 处理器将缓存回写到内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
多处理器下,为了保证各个处理器的缓存是一致的就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
🚩说说synchronized和volatile的区别?
- synchronized可以锁代码块,也可以锁同步方法,而volatile只能修饰变量
- volatile不会进行加锁操作也不会阻塞线程,synchronized会进行加锁操作和阻塞
- volatile不保证线程的原子性,而synchronized可以保证原子性
🚩生产者消费者模型作用是什么?
- 解决了忙闲不匀:通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的效率
- 解耦:生产者和消费者之间不直接进行沟通也就减少了相互之间的制约
🚩实现一个生产者消费者模型?
public class ProduceAndConsume {
//设置共享变量COUNT
private static volatile int COUNT;
public static void main(String[] args) {
//生产者
//创建三个线程,每一个线程就为一个生产者,生产者执行一次使得COUNT + 1
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
//每个生产者可以连续工作十次
for (int j = 0; j < 10; j++) {
synchronized (ProduceAndConsume.class) {
//如果生产者生产后,COUNT>10就开始等待
//注意:此处不使用if判断是为了防止wait方法返回后不能及时再次判断COUNT
while (COUNT + 1 > 10) {
//当前线程释放锁,等待唤醒
ProduceAndConsume.class.wait();
}
//库存不满就生产
produce();
System.out.println(Thread.currentThread().getName() + "生产,库存总量为" + COUNT);
//每次生产后睡眠一段时间
Thread.sleep(500);
//生产后有库存,就唤醒所有等待的消费者
ProduceAndConsume.class.notifyAll();
}
//睡眠一段时间,留出时间让其他线程竞争锁
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
//消费者
//创建三个线程,表示有三个消费者
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
while(true) {
//三个消费者均可一直消费
synchronized (ProduceAndConsume.class) {
//库存为0,则停止消费
while (COUNT == 0) {
ProduceAndConsume.class.wait();
}
consume();
System.out.println(Thread.currentThread().getName() + "消费,库存总量为" + COUNT);
Thread.sleep(500);
ProduceAndConsume.class.notifyAll();
}
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
//生产消费的同步方法
public static synchronized void produce(){
COUNT ++;
}
public static synchronized void consume(){
COUNT --;
}
}
🚩单例模式了解吗?
单例模式就是某个类的实例在多线程的环境下只会被创建一次。
单例模式有两种安全写法:饿汉模式和基于双重校验锁的懒汉模式
🚩写一个单例模式?
饿汉式:调用方法前就已经创建好了实例
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return instance;
}
}
懒汉式:在第一次调用方法时才开始创建
public class Singleton {
private static volatile Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里要注意变量用volatile修饰变量保证可见性,还有使用双重校验锁的方式保证多线程访问的安全性
🚩为什么要使用双重校验锁以及volatile关键字?
双重校验锁是指使用了两次if条件判断。如果只使用一次判定,那么当多个线程同时判定为null后只有一个线程竞争到锁其它线程阻塞,获取到锁的线程开始初始化变量,当初始化结束后释放锁。此时其它线程继续竞争到锁后又会开始创建实例,如果加上校验,那就避免了这一情况的发生提高了效率。
使用volitile修饰保证了变量的可见性和禁止指令重排序。在new一个对象时,虚拟机会将整个步骤分为三步:分配内存空间、初始化对象、将内存空间赋值给变量。在这三步骤中,虚拟机为了了提高效率可能会对指令进行重排序。也就有可能出现对象还没被初始化但是instance确指向该内存空间。如果此时有一个线程来访问变量,就会得到错误的值,所以为了解决这个问题,可以使用volatile关键字禁止指令的重排序。
🚩Java锁有什么作用?有哪些分类?
Java锁是用于保障在多线程并发情况下的数据一致性,线程必须获得锁才可以对共享变量进行操作。
- 从乐观悲观的角度分为:乐观锁和悲观锁
- 从获取资源的公平性上可以分为:公平锁和非公平锁
- 从是否共享资源可以分为:共享锁和排他锁
- 从锁的状态可以分为:偏向锁、轻量级锁和重量级锁
🚩什么是乐观锁和悲观锁?
- 乐观锁采用乐观心态,每次操作数据时都认为别人不会修改数据,所以不会上锁。CAS就是一种乐观锁
- 悲观锁采用悲观心态,每次操作数据都要进行加锁。synchronized就是一种悲观锁
🚩什么是公平锁和非公平锁?
- 公平锁指的是多个线程按照申请锁的顺序去获取锁,线程会进入队列排序,只有队列的第一个才可以获得到锁。
- 非公平锁是多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。不需要按照顺序获得。
🚩什么是CAS?有什么问题?
- CAS(Compare and swap)比较并交换,包括三个参数分别是:预期值、实际值、修改值,只有预期值=实际值时才进行交换将变量设置为修改值。它是一条从硬件方面实现的原子操作,采用了乐观锁的思想,同时多个线程使用CAS只有一个可以执行成功,失败的线程不会被阻塞,允许再次操作或放弃操作。
- CAS会产生ABA问题,无法察觉出改变可能会造成一些问题,为了解决这个问题可以使用版本号,加入版本号后,每一次修改都对版本号加一,这样即使数值一样但版本号也不同。
- 利用CAS实现的自旋锁,可以减少阻塞线程带来的开销,适合同步块代码执行较快的情况。如果同步块代码执行时间较长或线程竞争过于激烈时,就会使线程长时间消耗CPU。
🚩如何进行锁优化?
- 减小锁持有的时间:只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间
- 减小锁的粒度:将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争
- 锁粗化:为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但如果锁分的太细反而会影响性能提升,这种情况下建议将关联性强的锁操作集中处理
🚩atomic包下的原子类有了解吗?
atomic包是是java.util.concurrent
包中的一个原子类集合,其中包含了不同类型的多个原子类都是基于CAS机制的基础下实现。使得原子类型的变量都可以实现原子的自增和自减操作。主要包括4类,原子更新基本类型、原子更新数组、原子更新引用类型和原子更新属性。在原子更新属性类中有支持带有版本号的更新方法,可用于解决CAS操作时出现的ABA问题。
🚩Lock 了解吗?
Lock是一个接口可以实现锁的功能,提供了与synchronized相似的同步功能。但是在使用时需要显式的获取锁和释放锁,提高了锁的获取、释放的可操作性。使用时要将释放锁的操作放到finally代码块中保证锁可以释放成功。
🚩什么是AQS?
AQS 全称为AbstractQueuedSychronizer,就是抽象队列同步器。它是java.util.concurrent.locks
包下的一个抽象类,是用来构建锁的一个基础框架,子类通过继承同步器并实现它的抽象方法来管理同步状态,它使用了一个volatile的int变量来表示同步状态,通过内置的FIFO队列来完成资源的获取线程的排队工作。
总的来说,锁式面向使用者的,定义了使用者与锁交互的接口,隐藏了细节;同步器是面向锁的实现者的,简化了锁的实现,线程排队唤醒的操作。
🚩synchronized和ReentrantLock有什么区别?
synchronized是Java中的一个关键字,ReentrantLock是Java中的一个类
- ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁
- ReentrantLock 可以获取各种锁的信息
- ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized操作的应该是对象头中mark word
- ReentrantLock 需要调用方法显示的加锁解锁,而synchronized是会自动加锁
欢迎点赞收藏!如果想加一波关注那就再好不过了😁