Java 多线程和线程同步学习笔记
多线程怎么用
1. Thread
直接重写 Thread 的 run 方法
kotlin
val thread = object : Thread() {
override fun run() {
println("直接重写 Thread 类的 run 方法")
}
}
thread.start()
2. Runnable
实现 Runnable 接口,重写 run 方法。在 Thread 的构造方法中传入 Runnable 对象
kotlin
val runnable = object : Runnable {
override fun run() {
println("重写 Runnable 接口的 run 方法")
}
}
val thread = Thread(runnable)
thread.start()
3. ThreadFactory
实现 ThreadFactory 接口,重写 newThread 方法。
kotlin
val factory = object : ThreadFactory {
val count = AtomicInteger(0) // int
override fun newThread(r: Runnable): Thread =
Thread(r, "Thread-${count.incrementAndGet()}") // ++count
}
val runnable = Runnable {
println("Thread ${Thread.currentThread().name} start!")
}
val threadA = factory.newThread(runnable)
threadA.start()
val threadB = factory.newThread(runnable)
threadB.start()
4. Executor
kotlin
val executor: ExecutorService = Executors.newCachedThreadPool()
val runnable = Runnable { println("Thread with Runnable started!") }
executor.execute(runnable)
executor.execute(runnable)
executor.execute(runnable)
newCachedThreadPool() 方法返回的是 ExecutorService 对象,而 ExecutorService 是 Executor 的子接口。
4.1 shutdown() & shutdownNow()
在 ExecutorService 接口中,我们可以看到有很多方法,比如 execute()、shutdown()、shutdownNow() 等等。其中 shutdown() 是一个保守的关闭线程池的方法,也就是说如果当前线程池中有任务正在执行,或者有任务在排队等待执行,那么 shutdown() 方法不会立即关闭线程池,而是等待所有任务都执行完毕后才关闭线程池。
shutdownNow() 方法是立即关闭线程池,它会尝试中断正在执行的任务,并且停止所有等待执行的任务的排队。它会调用每个任务的 interrupt() 方法来中断线程,但是不保证一定能停止任务的执行。
4.2 newCachedThreadPool()
java
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
/* corePoolSize = */ 0,
/* maximumPoolSize = */ Integer.MAX_VALUE,
/* keepAliveTime = */ 60L, /* unit = */ TimeUnit.SECONDS,
/* workQueue = */ new SynchronousQueue<Runnable>()
);
}
newCachedThreadPool 方法里面创建了 newCachedThreadPool 对象,也就是线程池。 第一个参数 corePoolSize 是线程池的核心线程数。也就是是创建线程池后,线程池会默认自带的线程数,即使这些线程没有任务执行,也不会被回收。当线程池中的线程数大于 corePoolSize 时,如果这些线程已经执行完任务(开始闲置),闲置时间超过 keepAliveTime 时,这些线程就会被回收,一直回收到只剩下 corePoolSize 个线程为止。
第二个参数 maximumPoolSize 是线程池的最大线程数。也就是当提交的任务数大于 corePoolSize 时,线程池会创建新的线程来执行任务,直到线程数等于 maximumPoolSize,这时继续提交任务,那么就会进入等待队列。
第三个参数 keepAliveTime 是线程闲置的时间。也就是多于 corePoolSize 的线程,闲置时间超过 keepAliveTime 时,就会被回收。
第四个参数 unit 是 keepAliveTime 的时间单位。
4.3 newSingleThreadExecutor()
java
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(
1,
1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())
);
}
创建一个单线程的线程池,这个线程池只有一个线程在工作,使用场景很少。
4.4 newFixedThreadPool()
java
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads,
nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
创建一个固定大小的线程池,也就是说即使你不用这些线程,它们也一直存在,当你相要更多的线程时,又扩展不了。那...这个线程池有什么用呢?这种线程池比较适合用来处理集中爆发式的任务,比如有这么一个需求,有 30 张图片,要对这些图片进行裁剪、压缩、加水印、上传等操作,这时候使用 newFixedThreadPool 就比较合适了。
kotlin
val images: List<Image> = ...
val executor: ExecutorService = Executors.newFixedThreadPool(30)
images.forEach { image ->
executor.execute(Runnable {
processImage(image)
})
}
executor.shutdown()
fun processImage(image: Image) {
// 裁剪
// 压缩
// 加水印
// 上传
}
注意这里把所有 Runnable 都提交到了线程池后就马上调用了 shutdown() 方法,会不会有问题呢?答案是不会,因为 shutdown() 方法只是关闭了线程池,但是不会影响已经提交到线程池的任务的执行。
4.5 newScheduledThreadPool() & newSingleThreadScheduledExecutor()
这两个方法都是创建一个定长的线程池,可以进行定时或者周期性的任务调度。
5. Callable
Callable 可以简单理解为"带返回值的 Runnable"。 这个 Callable 可能会让人感到疑惑,Runnable 就是后台任务嘛,而 Callable 是一个带返回值的后台任务...那你什么时候返回,把返回值返回给谁呢?
kotlin
val callable = object : Callable<String> {
override fun call(): String {
Thread.sleep(5000)
return "Hello from Callable!"
}
}
val executor : ExecutorService = Executors.newCachedThreadPool()
val future : Future<String> = executor.submit(callable)
future.get() // 阻塞
虽然 submit() 的时候不会阻塞,但是 future.get() 会阻塞,直到 callable 执行完毕,才会返回结果。那...tm 的不还是阻塞了吗? 如果我就是要一个带返回值的后台任务,我不想阻塞,怎么办呢?那只能主动去轮询检查 callable 是否执行完毕了,说实话挺鸡肋的。
kotlin
val callable = object : Callable<String> {
override fun call(): String {
Thread.sleep(5000)
return "Hello from Callable!"
}
}
val executor : ExecutorService = Executors.newCachedThreadPool()
val future : Future<String> = executor.submit(callable)
while (!future.isDone) {
// do something
}
val result = future.get()
// do something with result
线程同步与线程安全
Demo1
kotlin
object SynchronizedDemo1WithBug {
private var running = true
private fun stop() {
running = false
}
@JvmStatic
fun main(args: Array<String>) {
thread {
while (running) {
}
}
Thread.sleep(1000)
stop()
}
}
这个 Demo 的目的是想让新线程的 while 循环在主线程调用 stop() 方法后停止,但是实际上这个 Demo 会一直运行下去,因为 while 循环中的 running 变量没有被同步,主线程对 running 变量的修改对子线程来说是不可见的。
让我们暂停一下,回顾一下 Java 内存模型中的一些概念。
我们写的程序里声明了 1 个变量,这个变量会被存储在主内存中,直觉上我们会以为新线程里的操作会直接修改主内存中的变量,但是实际上不是这样的。
每个线程都有自己的工作内存,工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
在工作内存种修改了变量的值之后,需要将该变量的最新值刷新到主内存中,这个过程叫做变量的写回。 绝大多数情况下,JVM 会帮我们处理工作内存和主内存之间的同步,但是为了提高处理速度,工作内存和主内存之间的同步不一定是实时的。
在我们的 Demo 中,running 变量在主内存中的值是 true,当新线程开始执行 while 循环时,会将 running 变量的值拷贝到自己的工作内存中,然后在工作内存中执行 while 循环,这时候主线程修改了 running 变量的值, 但是新线程的工作内存中的 running 变量的值并没有被修改,所以新线程的 while 循环会一直执行下去。
我们先不探讨为什么 Java 要这么设计,我们先来看看怎么解决这个问题。我们可以使用 volatile 关键字来修饰 running 变量,这样就可以保证新线程的工作内存中的 running 变量的值会被及时更新。
kotlin
object SynchronizedDemo1Solution {
@Volatile
private var running = true
...
}
@Volatile
关键字能够把所修饰的变量的同步性强制打开,换句话来说,就是会以最高的积极性来同步/读写该变量在工作内存和主内存中的值。 每次读该变量的值都会去主内存中读取,每次写该变量的值都会立即同步到主内存中。并且在写之前也会先从主内存中读取该变量的值,防止其他线程的修改导致写入错误的值。
通过这种方式,我们程序的效率会降低,但也提高了程序的安全性。
Demo2
kotlin
object SynchronizedDemo2WithBug {
private var x = 0
private fun count() {
x++
}
@JvmStatic
fun main(args: Array<String>) {
thread {
repeat(1_000_000) {
count()
}
println("x = $x")
}
thread {
repeat(1_000_000) {
count()
}
println("x = $x")
}
}
}
新建两个线程,每个线程都会对 x 进行 100 万次自增操作,然后打印 x 的值,无论哪个线程先执行,最终 x 的值应该都是 200 万。可是实际上,我们运行这个 Demo 会发现每次运行打印的结果都不一样。
踩过了 Demo1 的坑,我们应该能猜到这个 Demo 的问题出在哪里了。没错,就是 x 变量没有被同步,那简单嘛,加上 @Volatile
关键字不就行了。 我们再运行一下,发现还是不行,这是为什么呢?这是因为 x++
并不是一个原子操作,它实际上是由三个步骤组成的:
kotlin
val temp = x
temp = temp + 1
x = temp
那么,在多线程的环境下,如果两个线程同时执行 x++
,那么就会出现这样的情况: 线程 1:
kotlin
val temp = x // temp = 0
temp = temp + 1 // temp = 1
// x = temp 这一步还没来得及执行,线程2 就开始执行了
线程 2:
kotlin
val temp = x // temp = 0
temp = temp + 1 // temp = 1
x = temp // x = 1
线程 2 执行完毕,线程 1 继续执行:
kotlin
x = temp // x = 1
这样,虽然执行了两次 x++
,但是 x 的值只增加了 1,这就是 Demo2 的问题所在。 所以,想要避免线程同步问题,除了要保证变量的同步性,还要保证那些对变量的操作的原子性。什么是原子操作呢?原子操作就是不可分割的操作,要么全部执行完毕,要么都不执行。
怎么解决呢?我们可以借助 synchronized
关键字。
kotlin
object SynchronizedDemo2WithBug {
private var x = 0
@Synchronized
private fun count() {
x++
}
@JvmStatic
fun main(args: Array<String>) {
thread {
repeat(1_000_000) {
count()
}
println("x = $x")
}
thread {
repeat(1_000_000) {
count()
}
println("x = $x")
}
}
}
给 count() 方法加上 @Synchronized
关键字,会有两个效果(同步性 & 互斥访问):
- 保证方法里面涉及到的所有变量在执行方法期间都具有同步性,这也是为什么上面的代码并没有给 x 变量加上
@Volatile
关键字,因为@Synchronized
关键字已经保证了 x 变量的同步性。 - 保证整个方法是原子操作。(我们暂且可以理解为某个线程调用执行该方法期间,其他线程无法调用该方法,看起来好像是 Synchornized 保护了该方法。)
AtomicXXX
说到这里,我们回头讲一下 AtomicInteger,它是一个原子类,它的 incrementAndGet()
方法就是原子操作,相当于 x++
,但是它是线程安全的。还有一个 getAndIncrement()
方法,也是原子操作,相当于 ++x
。 Atomic 就是原子的意思,AtomicInteger 就是具备原子性的 int 类型,除了原子性,它还具备同步性,所以我们可以使用 AtomicInteger 来解决 Demo2 的问题。
kotlin
object SynchronizedDemo2Solution2 {
private var x = AtomicInteger(0)
private fun count() {
x.incrementAndGet()
}
@JvmStatic
fun main(args: Array<String>) {
thread {
repeat(1_000_000) {
count()
}
println("x = ${x.get()}")
}
thread {
repeat(1_000_000) {
count()
}
println("x = ${x.get()}")
}
}
}
Atomic 相关的类还有 AtomicBoolean、AtomicIntegerArray、AtomicLong、AtomicLongArray、AtomicReference、AtomicReferenceArray 等等,它们都是线程安全的。
我们来看看 AtomicReference,原子型的引用?原子型的引用可以保证引用的对象的同步性,来看这么一种情况:
kotlin
data class User(val name: String)
var user = User("ABC")
假设我们有这么一个 User 类,我们在主线程中创建了一个 User 对象,线程 A 重新给 user 赋值 user = User("XYZ")
,有可能因为线程同步的问题,主线程中的 user 对象的值还是 ABC。
使用 AtomicReference 就可以解决这个问题。
kotlin
val user = AtomicReference<User>(User("ABC"))
总之 AtomicXXX 类可以保证变量的同步性和原子性,但是它的效率比较低,所以在不需要保证原子性的情况下,还是使用 @Volatile
关键字来保证变量的同步性更好。
Demo3
kotlin
object SynchronizedDemo3WithBug {
private var a = 0
private var b = 0
private var isPlaying = true
fun setValues(value: Int) {
a = value
b = value
}
fun setPlaying(isPlaying: Boolean) {
this.isPlaying = isPlaying
}
}
我们假设 setValues() 和 setPlaying() 都有可能被多个线程同时调用,那么为了保证 a 和 b 的值一致,我们可以给 setValues() 方法加上 @Synchronized
关键字,且为了 setPlaying() 方法也能保证同步性,我们也给它加上 @Synchronized
关键字。
kotlin
object SynchronizedDemo3WithBug {
private var a = 0
private var b = 0
private var isPlaying = true
@Synchronized
fun setValues(value: Int) {
a = value
b = value
}
@Synchronized
fun setPlaying(isPlaying: Boolean) {
this.isPlaying = isPlaying
}
}
实际上,@Synchronized
关键字是通过 Monitor 来实现的,Monitor 是 Java 中的一种同步机制,它可以保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而保证了线程安全性。 在这个例子里,我们给两个方法都加上了 @Synchronized
关键字,那么这两个方法会被同一个 Monitor 所保护,也就是说,在线程 A 执行 setValues() 方法期间,线程 B 是无法执行 setPlaying() 方法的,反之亦然。这...是不是有点不太合理?我们只是想保证 a 和 b 的值一致,为什么要把 setPlaying() 方法也锁住呢? 这是因为 @Synchronized
关键字是锁住了对象的,而不是锁住了某个方法,上面的写法相当于:
kotlin
object SynchronizedDemo3WithBug {
private var a = 0
private var b = 0
private var isPlaying = true
fun setValues(value: Int) {
synchronized(this) {
a = value
b = value
}
}
fun setPlaying(isPlaying: Boolean) {
synchronized(this) {
this.isPlaying = isPlaying
}
}
}
因为 this 是同一个对象,所以这两个方法会被同一个 Monitor 所保护。 为了解决这个问题,我们可以给两个方法分别加上不同的 Monitor,也就是说,我们可以给两个方法加上不同的锁。
kotlin
object SynchronizedDemo3Solution {
private var a = 0
private var b = 0
private var isPlaying = true
private val lock1 = Any()
private val lock2 = Any()
fun setValues(value: Int) {
synchronized(lock1) {
a = value
b = value
}
}
fun setPlaying(isPlaying: Boolean) {
synchronized(lock2) {
this.isPlaying = isPlaying
}
}
}
死锁
kotlin
object SynchronizedDemo3Solution {
private var a = 0
private var b = 0
private var isPlaying = true
private val lock1 = Any()
private val lock2 = Any()
fun setValues(value: Int) {
synchronized(lock1) {
a = value
b = value
synchronized(lock2) {
isPlaying = false
}
}
}
fun setPlaying(isPlaying: Boolean) {
synchronized(lock2) {
this.isPlaying = isPlaying
synchronized(lock1) {
a = 0
b = 0
}
}
}
}
在有些场景中,我们在一个代码块里面需要同时锁住多个对象,比如上面的代码,有这么一种场景,需要先修改 a 和 b 的值,然后再修改 isPlaying 的值,这两个操作都需要锁住不同的对象,另外还有一种场景我想反过来,先修改 isPlaying 的值,然后再修改 a 和 b 的值,这两个操作也需要锁住不同的对象,那么这个时候就有可能出现死锁的情况。 假设线程 A 执行 setValues() 方法,线程 B 执行 setPlaying() 方法,那么有可能在某个时刻,线程 A 锁住了 lock1,线程 B 锁住了 lock2,然后线程 A 试图去锁住 lock2,线程 B 试图去锁住 lock1,这时候就会出现死锁的情况。 所谓死锁就是在多重锁的情况下,你拿了别的的锁,别人也拿了你的锁,然后你们都想要对方手里的锁,这样就会造成死锁。
悲观锁 & 乐观锁
其实无论悲观锁还是乐观锁,它们都是数据库相关的管理思想,和线程同步没有直接关系,但是我们还是简单了解一下。 假设有这么一个场景:要从数据库里面读取一大堆数据,然后对这堆数据进行处理,最后再写回数据库。在这个过程中,别的线程可能已经对数据库里面的数据进行了修改,也就是在我们读完之后,写回之前,数据库里面的数据已经发生了变化,那么我们写回去的数据就是脏数据了,按理我们的处理结果应该作废,重新读取数据,然后再处理,再写回去。遇到这种情况,我们丢弃现有结果,读取新数据,再次处理并写入,这就是乐观锁的思想。因为我们"假设"了别的线程不会总是在我们读取数据的时候修改数据,所以我们"乐观"地认为自己的处理结果是最多被丢弃几次,而不是每次都会被丢弃。 相反,如果我们"悲观"地认为别的线程总是会在我们读取数据的时候修改数据,如果遇到这种情况,如果我们还是每次都丢弃现有结果,读取新数据,再次处理并写入,那这个计算工作一直在重复,永远到达不了结束的状态,这样就会造成资源的浪费,所以在这种"悲观"思想下,我们可以在读取数据的时候就锁住数据库,这样别的线程就无法修改数据库里面的数据,直到我们处理并写入完成,再把锁释放掉,这就是悲观锁的思想。
单例模式里面的线程同步
kotlin
class SingleBoy private constructor() {
companion object {
private var instance: SingleBoy? = null
fun getInstance(): SingleBoy {
if (instance == null) {
instance = SingleBoy()
}
return instance!!
}
}
}
如果有两个线程同时调用 getInstance() 方法,两者在 if (instance == null)
这一步都如果都判断 instance 为 null,那么两个线程都会执行 instance = SingleBoy()
,然后返回 instance,这样就会创建出两个 SingleBoy 对象,为了避免这种情况,我们可以直接给 getInstance()
方法加上 @Synchronized
关键字,但是这样做会降低程序的效率,因为每次调用 getInstance() 方法都会加锁,即使 instance 已经不为 null 了,也会加锁,所以我们可以先判断 instance 是否为 null,如果为 null,再加锁,这样就可以提高程序的效率。
kotlin
class SingleBoy private constructor() {
companion object {
private var instance: SingleBoy? = null
fun getInstance(): SingleBoy {
if (instance == null) {
synchronized(this) {
instance = SingleBoy()
}
}
return instance!!
}
}
}
但是现在我们的代码还是有问题的,因为我们只是在 instance = SingleBoy()
这一步加了锁,但是 if (instance == null)
这一步没有加锁,所以还是会有两个线程同时判断 instance 为 null,然后其中某个线程拿着锁去执行 instance = SingleBoy()
,另一个线程等待锁,等待第一个线程执行完毕,释放锁之后,第二个线程拿到锁,然后也执行 instance = SingleBoy()
,这样还是会创建出两个 SingleBoy 对象,那怎么办呢?我们可以在创建实际创建 SingleBoy 对象之前再判断一下 instance 是否为 null,如果为 null,再创建对象,这样就可以避免创建出两个 SingleBoy 对象了。
kotlin
class SingleBoy private constructor() {
companion object {
private var instance: SingleBoy? = null
fun getInstance(): SingleBoy {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = SingleBoy()
}
}
}
return instance!!
}
}
}
现在还有一个问题,外层的 if (instance == null)
的同步性是没有保证的,那么会出现什么问题呢?
类的初始化是一个比较复杂的过程,很有可能会出现 SingleBoy 对象已经被创建出来了(已经在虚拟机标记这个对象可用了),但是这个类的初始化过程(构造方法)还没有执行完毕,这个时候如果有另一个线程调用了 getInstance()
方法,外层的 if (instance == null)
会判断 instance 不是 null,那么这个线程就会拿到一个还没有初始化完毕的 SingleBoy 对象,这样就会出现问题。那怎么解决呢?我们可以使用 @Volatile
关键字来修饰 instance,这样就可以保证 instance 的同步性了。
kotlin
class SingleBoy private constructor() {
companion object {
@Volatile
private var instance: SingleBoy? = null
fun getInstance(): SingleBoy {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = SingleBoy()
}
}
}
return instance!!
}
}
}
用 Kotlin 语法糖来简化一下代码:
kotlin
class SingleMan private constructor() {
companion object {
@Volatile
private var instance: SingleMan? = null
fun getInstance(): SingleMan = instance ?: synchronized(this) {
instance ?: SingleMan().also { instance = it }
}
}
}
ReentrantLock
kotlin
object ReentrantLockDemo {
private var count = 0
private val reentrantLock = ReentrantLock()
fun count() {
reentrantLock.lock()
try {
count++
// some other operations
} finally {
reentrantLock.unlock()
}
}
}
ReentrantLock 就是 Java 中的可重入锁,它的用法和 @Synchronized
关键字是一样的,只不过它是以代码的形式来实现的,而不是以注解的形式来实现的。看上面的代码,因为操作过程中可能会出现异常,还必须 try...finally... 来保证锁一定会被释放,这...是不是有点麻烦?我为什么不直接用 @Synchronized
关键字呢?使用 ReentrantLock 的好处是,我们可以根据读和写的操作来分别加锁,从而提高程序的效率。
在继续往下之前,我们先来想一想,在不加锁的前提下,线程 A 对变量进行读或者写操作的时候,线程 B 能否也能对变量进行读或者写操作呢?
kotlin
var a = 0
var b = 0
fun setValues(value: Int) {
a = value
b = value
}
1.A 写,B 写
线程 A:setValues(1)
a = 1
// b = 0
线程 B:setValues(2)
a = 2
b = 2
线程 A:
a = 2
b = 1
2.A 写,B 读
线程 A:setValues(1)
a = 1
// b = 0
线程 B:
println(a) // a = 1
println(b) // b = 0
3.A 读,B 写
线程 A:
println(a) // a = 0
线程 B:setValues(1)
a = 1
b = 1
线程 A:
println(b) // b = 1
4.A 读,B 读
线程 A:
println(a) // a = 0
线程 B:
println(a) // a = 0
println(b) // b = 0
线程 A:
println(b) // b = 0
这么看下来,A 写的时候,B 既不能读也不能写,A 读的时候,B 也不能写,但是可以读。我们可以使用读写锁去更精细地保护变量。
kotlin
object ReentrantReadWriteLockDemo {
private var count = 0
private val readWriteLock = ReentrantReadWriteLock()
private val writeLock = readWriteLock.writeLock()
private val readLock = readWriteLock.readLock()
fun count() {
writeLock.lock()
try {
count++
// some other operations
} finally {
writeLock.unlock()
}
}
fun print() {
readLock.lock()
try {
println("count = $count")
} finally {
readLock.unlock()
}
}
}
通过对锁更精细化的控制,读的操作用读锁保护,写的操作用写锁保护,提高了程序的效率,避免了无意义的互斥(某个线程在读,其他线程不许读):
- 当多个线程同时访问读的操作时,不会被阻塞的;
- 当有线程访问读的操作时,写的操作会被阻塞;
- 当有线程访问写的操作时,无论是读的操作还是写的操作都会被阻塞。