多线程 @ Lin | 2023-06-01T14:21:26+08:00 | 12 分钟阅读 | 更新于 2023-06-01T14:21:26+08:00

1、线程实现的方式及其优缺点?

1)继承Thread类

2)实现Runnable接口

3)实现Callable(JDK>=1.5)

4)使用线程池方式创建

实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法返回值,可以声明抛出异常而已。这种实现Runnable接口和实现Callable接口归为一种方式。

a、采用实现Runnable、Callable接口的方式创建线程的优缺点

优点::线程类只是实现了Runnable或者Callable接口,还可以继承其他类,这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。

缺点::编程稍微复杂一些,如果需要访问当前线程,则必须使用 Thread.currentThread() 方法。

b、采用继承Thread类的方式创建线程的优缺点

**优点:**编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用this即可获取当前线程。

缺点::因为线程类已经继承了Thread类,Java语言是单继承的,所以就不能再继承其他父类了。

2、线程的run()和start()的区别?

start() 方法被用来启动新创建的线程,而且 start() 内部调用了 run() 方法,这和直接调用run()方法的效果不一样。

当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

3、进程、线程、协程之间的区别?

进程:进程时程序运行和资源分配的基本单位。一个程序至少有一个进程,一个进程至少有一个线程。

线程:线程是进程的一个实体,是cpu调度和分派的基本单位。

协程:协程比线程更轻量级,协程不是被操作系统内核管理,是完全有程序所控制(也就是用户态执行)。这样带来的好处就是性能提高,不会像线程切换那样消耗资源。

4、线程状态有哪些?(6个)

Java线程状态:新建、可运行、阻塞、等待、超时等待、终止。

5、说一下volatile关键字,还有怎么保证可见性,涉及内存屏障相关的?

volatile三特性:a、保证可见性 b、不保证复合操作的原子性 c、禁止指令重排

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主内存。

2)禁止进行指令重排序

3)volatile不保证原子性

保证部分有序性:当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结

果已经对后面的操作可见;在其后面的操作肯定还没有进行;

x = 2; //语句1

y = 0; //语句2

flag = true; //语句3

x = 4; //语句4

y = -1; //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

使用volatile 一般用于 状态标记量 和 单例模式的双检锁。

**补充:JMM(java内存)模型:**主内存+本地内存(线程),本地内存会复制主内存的共享变量副本。

6、如何死锁?(考察死锁的条件)

1)互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。 2)请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。 3)不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。 4)环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

7、如何加锁?(Synchronize关键字和Reentrantlock)

(synchronized在同一个类下修饰a、b两个方法,不会造成锁竞争,如果a、b方法是static修饰 的会,因为是锁的是同一个类。)

Synchronized的3种使用方式:

  • 普通同步方法,锁的是当前实例对象,this。
  • 静态同步方法,锁的是当前类的class对象。
  • 同步方法块,锁的是括号里面的对象

Synchronized 和 ReentrantLock有什么不同

相似处:都是可重入锁,都是阻塞式的同步加锁。 不同处

a、 synchronized依赖于JVM实现的,在经过编译,会在同步语句块的前后形成monitorenter和monitorexit这个两个字节码指令。ReentrantLock是基于JDK1.5之后提供的API层面的互斥锁,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成。

b、相比synchronized,ReentrantLock提供了一些高级的功能,主要有3点:

1)等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相对于synchronized来说可以避免出现死锁的情况。

2)**可实现公平锁,**多个线程等待同一个锁的时候,必须按照申请锁的时间顺序获得锁,Synchronized 是非公平锁,ReentrantLock默认创建构造函数时非公平锁,可以通过参数设置true设为公平锁。

3)锁可以绑定多个条件,,一个 ReentrantLock 对象可以同时绑定对个对象,例如ReentrantLock可与 wait() 和 notify()/notifyall() 方法相结合实现等待/通知机制,但需要借助Condition接口与newCondition()方法。

7.1、同步方法块和同步方法的区别?(monitorenter、exit和ACC_SYNCHRONIZED)

同步方法,锁是用当前实例对象,编译后,字节码有一个 ACC_SYNCHRONIZED 标识该方法是个同步方法。

同步方法块锁的是 synchronized关键字括号后的对象。 编译后,字节码有monitorentermonitorexit 指令.

7.2、锁升级过程(或者锁的优化机制)?(重点)

从 JDK1.6之后synchronized就在不断优化锁的机制,**锁状态从低到高:**无锁 - > 偏向锁 - > 轻量级锁 - > 重量级锁。。简单说就是,偏向锁就是通过对象头的偏向线程 ID 来对比,都不需要CAS了,而轻量级锁就是通过CAS修改对象头锁记录和自旋来实现的,重量级锁是除了拥有锁的线程其他全部阻塞。

偏向锁: 线程1访问代码块并获取锁对象,会偏向对象头和栈帧中记录偏向锁的线程 ID ,之后这个线程再次进入同步块代码就都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程。

轻量级锁:对象头中中包含有一些锁的标志位,代码进入同步块的时候,会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁

简要过程: 线程1访问代码块并获取锁对象,会在 Java 对象头和栈帧中记录偏向的锁的 线程ID,而且偏向锁不会主动释放锁,当线程2来获得锁对象,会查找对象头中记录的偏向锁偏向的线程ID(线程1),如果说还是要继续持有这个锁对象,暂停线程1,撤销偏向锁,升级为轻量级锁(如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程)。如果线程2获取轻量级锁时使用CAS获取失败,那么线程2会通过自旋来获得锁。如果这个时候线程3也来获取锁对象,线程2还在自选等待,没获得到锁,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

7.3、ReentrantLock优缺点?

image-20230710172118891

7.4、底层实现?(AQS、CAS) 

**CAS:**CAS叫做CompareAndSwap,比较并交换,如果主内存的值和期望值一样,那就会进行修改,否则会一直重试。它功能是判断内存某个位置的值是否是预期值,如果是则更改为新值,这个过程是原子的。(CAS是一条CPU的原子指令,不会造成数据不一致问题。)

AQS:AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。AQS是整个Java并发包的核心,AQS是一个用来构建锁和同步器的框架,比如ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH(虚拟的双向队列)队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS定义了两种资源获取方式:独占(只有一个线程能访问执行,又根据是否按队列的顺序分为公平锁和非公平锁,如ReentrantLock) 和共享(多个线程可同时访问执行,如Semaphore/CountDownLatch,Semaphore、CountDownLatCh、 CyclicBarrier )。

补充:Semaphore(信号量)-允许多个线程同时访问某一个共享资源、 CountDownLatch (倒计时器)让某一个线程等待直到倒计时结束,再开始执行

AQS底层使用了模板方法模式自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。

7.5、CAS会存在什么问题?如何避免?

存在ABA问题 :ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。

解决方法:使用时间戳 AtomicStampedReference , 这会维护一个版本号 stamp,在CAS操作过程中,不仅要比较当前值,还要比较版本号。

其他问题:a、循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。 b、只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行。

7.6、公平锁和非公平锁在AQS上是如何实现的?Synchonized是公平锁还是非公平锁?

AQS,抽线队列同步器(abstractQueuedSynchronizer),是可以实现锁和同步器的框架,内部实现的关键是维护了一个先进先出的队列已经state状态变量,先进先出队列存储的载体是Node节点,节点记录着当前状态值、是独占还是共享模式等信息,AQS定义了模板,具体实现由子类完成。

**公平锁:**公平锁就是把竞争的线程放在一个先进先出的队列上,如果持有锁的线程执行完了,唤醒队列的下一个线程去获得锁。

**非公平锁:**获得锁的顺序不会按照线程申请的先后顺序,后到的线程可能比临界区的线程获得锁,实现:线程先尝试能不能获得锁,如果获得不到锁,就把线程放到队列里面,与公平锁的区别是它会先尝试获得锁,而公平锁是直接放进队列,等待唤醒。(Synchonized是非公平锁)

7.7、 读写锁是怎么实现的?(也是基于AQS的)

基于AQS,Java读写锁用state的高16位表示读锁的线程数,低16位表示写锁的重入数。其中读锁类中的Sync实现共享获取、释放锁方法,写锁类中Sync实现互斥获取、释放锁方法。(当读取数据时用读锁,当没有线程获取到写锁,多个线程可同时获取到读锁;当没有线程获取到读锁时,可以获取到写锁,最多只有一个线程能获取到写锁)

8、线程池的理解

8.1、线程池创建的4中方法?

  • 继承Thread类
  • 实现Runnable接口
  • 实现callable接口通过FutureTask包装器来创建Thread线程
  • 通过线程池创建线程,使用线程池接口ExecutorService结合Callable、Future实现有返回结果的多线程。

8.2、任务加入的线程池的流程?

流程:

1)当我们提交任务,线程池会根据核心线程数(corePoolSize)大小创建若干任务数量线程执行任务

2)当任务数量大于核心线程数(corePoolSize)数量,后续的任务将会进入阻塞队列阻塞排队。

3)当阻塞队列也满了之后,那会继续创建最大线程数减去核心线程数(maximumPoolSize - corePoolSize)个数量线程来执行任务,如果任务处理完成,(maximumPoolSize - corePoolSize)额外创建的线程在达到活跃时间(keepAliveTime)之后就自动销毁了。

4)如果达到最大线程数maximumPoolSize,阻塞队列还是满的状态,那会根据不同的拒绝策略对应处理。

8.3、线程池的7个参数?拒绝策略?

核心参数:

1)最大线程数maximumPoolSize

2)核心线程数corePoolSize

3)活跃时间keepAliveTime

4)阻塞队列workQueue

5)拒绝策略RejectedExecutionHandler

6)时间单位unit

7)线程工厂threadFactory

四种拒绝策略:

1)AbortPolicy:直接丢弃任务,抛出异常,真是默认策略。

2)CallerRunsPolicy:只用调用者所在的线程来处理任务

3)DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务。

4)DiscardPolicy:直接丢弃任务,不抛出异常。

补充执行 execute()方法和 submit()方法的区别:

execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池所执行成功; submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型对象,通过这个 Future对象可以判断任务是否执行成功

8.4、线程池中如何拿到线程的执行结果?

执行任务的时候用submit() 方法,线程池会返回一个 Future 类型对象,通过这个 Future对象的 get() 方法获得返回值(get()方法会阻塞当前任务直到任务完成)。

9、ThreadLocal了解?

本地线程,每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,可以实现每个线程自己专属本地变量副本,可以避免线程安全问题。

**补充:乐观锁,悲观锁:**a、乐观锁,每次去拿数据的时候都认为别人不会修改,所以不会上锁。实现机制用CAS。 b、悲观锁,每次去拿数据的时候都认为别人会修改,例如synchronized。

© 2019 - 2024 Lin 的博客

Powered by Hugo with theme Dream.

avatar
关于我

Lin 的 ❤️ 博客

记录一些 🌈 生活上,技术上的事

职业是JAVA全栈工程师