Java 多线程和线程同步学习笔记

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 关键字,会有两个效果(同步性 & 互斥访问):

  1. 保证方法里面涉及到的所有变量在执行方法期间都具有同步性,这也是为什么上面的代码并没有给 x 变量加上 @Volatile 关键字,因为 @Synchronized 关键字已经保证了 x 变量的同步性。
  2. 保证整个方法是原子操作。(我们暂且可以理解为某个线程调用执行该方法期间,其他线程无法调用该方法,看起来好像是 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()
        }
    }
}

通过对锁更精细化的控制,读的操作用读锁保护,写的操作用写锁保护,提高了程序的效率,避免了无意义的互斥(某个线程在读,其他线程不许读):

  1. 当多个线程同时访问读的操作时,不会被阻塞的;
  2. 当有线程访问读的操作时,写的操作会被阻塞;
  3. 当有线程访问写的操作时,无论是读的操作还是写的操作都会被阻塞。
相关推荐
一只柠檬新12 小时前
Web和Android的渐变角度区别
android
志旭12 小时前
从0到 1实现BufferQueue GraphicBuffer fence HWC surfaceflinger
android
_一条咸鱼_12 小时前
Android Runtime堆内存架构设计(47)
android·面试·android jetpack
用户20187928316712 小时前
WMS(WindowManagerService的诞生
android
用户20187928316712 小时前
通俗易懂的讲解:Android窗口属性全解析
android
openinstall12 小时前
A/B测试如何借力openinstall实现用户价值深挖?
android·ios·html
二流小码农12 小时前
鸿蒙开发:资讯项目实战之项目初始化搭建
android·ios·harmonyos
志旭13 小时前
android15 vsync源码分析
android
志旭13 小时前
Android 14 HWUI 源码研究 View Canvas RenderThread ViewRootImpl skia
android
whysqwhw14 小时前
Egloo 高级用法
android