Kotlin Java多线程编程安全

在多线程编程里,放多线程会交叉访问共享的对象,如果我们不做些同步的工作,那些结果可能不是我们想要的。

    var sum = 0

    @Test
    fun addition_isCorrect(){
        
        for(i in 0..100){
            Thread{
                accumulate(1)
            }.start()
        }
        Thread.sleep(3000)
        println(sum)
    }

    fun accumulate(i: Int){
        sum += i
    }

上面的例子是多个线程去操作sum这个共享变量,每个线程都是让这个sum变加1,那么期待的结果应该是101,但是上面的程序可能不会让你得到101,结果可能是100,99,98等这些错误的结果。

再比如下面这个协程的例子

 @Test
    fun hello() = runBlocking {
        var coroutines = listOf<Job>()
        var shareSum = 0
        // 使用固定大小的线程池创建协程的执行上下文
        // @DelicateCoroutinesApi
        //public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher {
        //    require(nThreads >= 1) { "Expected at least one thread, but $nThreads specified" }
        //    val threadNo = AtomicInteger()
        //    val executor = Executors.newScheduledThreadPool(nThreads) { runnable ->
        //        val t = Thread(runnable, if (nThreads == 1) name else name + "-" + threadNo.incrementAndGet())
        //        t.isDaemon = true
        //        t
        //    }
        //    return executor.asCoroutineDispatcher()
        //}
        val scope = CoroutineScope(newFixedThreadPoolContext(8, "sizeFixedThreadPool"))
        // 在不阻塞当前线程的情况下启动一个新的协程,并将对该协程的引用作为Job返回。可以用job取消当前的协程
        val job = scope.launch {
            // 提醒 scope这个范围里有8条线程在执行
            coroutines = 1.rangeTo(100).map {
                // 创建100个协程
                launch {
                    for(i in 1..100){
                        shareSum += 1
                    }
                }
            }
            // 我们等待所以协程执行完成
            coroutines.forEach {
                // join() 会挂起协程,直到该作业完成
                it.join()
            }
        }.join()

        println(" 10000, $shareSum")   // 10000, 9952

    }

我创建一个有8个线程的协程执行上下文,然后在此执行上下文中创建一个100个协程,每个协程对shareSum进行加1操作。结果是有时正确,有时不正确。

上面两个例子为什么出现这些错误?这些线程的工作过程是这样的:

  1. 首先获得shareSum的当前值,
  2. 接着,将其保存到一个临时变量中,并对这个变量对行加1操作
  3. 最后,把这个临时变量赋给shareSum

在多线程的世界,可能会出现以下的情况:

  • 如果一个线程获取了shareSum的当前值的同时,也有另外的线程获取了shareSum,那么这些线程就都获取了一个相同的shareSum,所以这些线程加1后,都会将一个相同的值赋回给shareSum。
  • 再比如说有个线程刚好获取了shareSum,准备做后续的动作时,还没有将临时变量的值保存回shareSum时,另外一个线程进来了,获取好shareSum的值,并加好1,并将其值保存回shareSum,那么第一个线程完成加1,保存回shareSum的值是旧值(就是它应该拿最新的shareSum来加1)

还有很多类似的情况都会导致多线程操作共享变量出问题。为了解决这些问题,我们只要同步这些线程的工作就可以了。目的就是保证它这些线程操作shareSum时,一定是拿到最新的值去加1。

Volatile关键字

首先,volatile关键字并不能够解决上面遇到的问题,但是也顺便分享给大家。在Java中是volatile,在kotlin中是用@Volatile,这个东西是用在字段上。它的作用是提供内存可见性,保证这个正在被读取的字段的值一定是来自内存,而不是CPU的cache(就是CPU的高速缓存)。所以加了这个关键字的字段,CPU在读取时,它直接忽略在cache的值,直接重新从内存读取这个字段的值。这样就保证了CPU一定读取到这个字段最新的值。

我们上面的问题呢,有一程情况是多个线程都读取了相同的值造成了不正确的结果。volatile这个技术帮不了忙。它对单线程是有效的。这里就不展开了。

要解决上面的问题,就要保存它的操作是原子性,也就是每个线程获取了shareSum的值,将其保存到临时变量并加1,再保存回shareSum这些操作完成了,下一个线程才能开始操作。这样保证每个线程的操作都是原子性后,那么结果是正确的。

解决办法1:使用Synchronized

对一个例子的修复:

   @Synchronized
    fun accumulate(i: Int){
        sum += i
    }

对第二个例子的修复:


    @Test
    fun hello() = runBlocking {
        var coroutines = listOf<Job>()
        // 使用固定大小的线程池创建协程的执行上下文
        // @DelicateCoroutinesApi
        //public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher {
        //    require(nThreads >= 1) { "Expected at least one thread, but $nThreads specified" }
        //    val threadNo = AtomicInteger()
        //    val executor = Executors.newScheduledThreadPool(nThreads) { runnable ->
        //        val t = Thread(runnable, if (nThreads == 1) name else name + "-" + threadNo.incrementAndGet())
        //        t.isDaemon = true
        //        t
        //    }
        //    return executor.asCoroutineDispatcher()
        //}
        val scope = CoroutineScope(newFixedThreadPoolContext(8, "sizeFixedThreadPool"))
        // 在不阻塞当前线程的情况下启动一个新的协程,并将对该协程的引用作为Job返回。可以用job取消当前的协程
        val job = scope.launch {
            // 提醒 scope这个范围里有8条线程在执行
            coroutines = 1.rangeTo(100).map {
                // 创建100个协程
                launch {
                    for(i in 1..100){
                        accumulateShareSum()
                    }
                }
            }
            // 我们等待所以协程执行完成
            coroutines.forEach {
                // join() 会挂起协程,直到该作业完成
                it.join()
            }
        }.join()

        println(" 10000, $shareSum")   // 10000, 9952

    }

    var shareSum = 0

    @Synchronized
    fun accumulateShareSum(){
        shareSum += 1
    }

每次都结果都是正确的。感觉很棒!Synchronized保证每个线程完成了操作后,另一个线程才可以入场操作。这里提一个,我们的Synchronized是加在方法上的,因此整个方法的访问都被限制在一个线程。请看下面这种同步方法:

@Synchronized
fun accumulateShareSum(flag: Boolean){
    if(flag) {
       shareSum += 1
    }
}

上面这个方法,对任何调用者,不管它们是否需要加1操作都进行了同步,这其实不是很好,有一种同步语句比较适合,它会让真正需要操作共享变量的调用同步:

    @Synchronized
    fun accumulateShareSum(flag: Boolean){
        if(flag) {
            synchronized(this){
                shareSum += 1
            }
        }
    }

解决办法2:使用原子原语

其实,大家可能已经很熟悉了,比如AtomicInteger,AtomicReference,AtomicBoolean等都是常见的原子原语,它们提供了很多方法供开发者使用,都能达到原子性操作,保证线程安全。比如上面的例子:

    var shareSum = AtomicInteger(0)
    
    fun accumulateShareSum(){
        shareSum.incrementAndGet()
    }

解决办法3:锁

锁比Synchronized的同步方法和同步语要灵活。它可以出现在任何地方。我们现在用重入锁解决上面的问题:

    val reentrantLock = ReentrantLock()
    var shareSum = 0
    fun accumulateShareSum(){
        reentrantLock.lock()
        try {
            shareSum += 1
        } finally {
            reentrantLock.unlock()
        }

    }

解决办法4:信号量

我们直接上代码吧:

 val semaphore = Semaphore(1)
    var shareSum = 0
    fun accumulateShareSum(){
        try {
            semaphore.acquire()
            shareSum += 1
        } finally {
            semaphore.release()
        }

    }

另外Java还提供了很多并发的工具和集合(如HashTable,ConcurrentHashMap)等。大家有空可以去了解。CyclicBarrier和CountDownLatch是一些同步的工具,大家另外脑补吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin 是一种基于 JVM 的编程语言,因此它可以使用 Java 提供的多线程 API 实现多线程。除此之外,Kotlin 也提供了一些方便的语法糖来简化多线编程。 下面是使用 Kotlin 实现多线程的步骤: 1. 创建一个继承自 Thread 的线程类,重写 run() 方法,在该方法中编写线程执行的代码。 2. 在主线程中创建该线程的实例,并调用 start() 方法启动该线程。 3. 如果需要在子线程中更新 UI,需要使用 Handler 或者 runOnUiThread() 方法。 举个例子,下面是一个简单的多线程实现代码: ```kotlin class MyThread : Thread() { override fun run() { super.run() // 子线程执行的代码 Log.d("MyThread", "子线程执行中...") } } ``` 然后在主线程中创建该线程的实例并启动: ```kotlin val myThread = MyThread() myThread.start() ``` 除了使用 Java 的多线程 API,Kotlin 还提供了一些方便的语法糖,例如使用协程来简化异步编程。使用协程,可以在代码中使用类似于同步代码的方式来编写异步任务,这样可以避免回调地狱的问题。 举个例子,下面是使用协程实现多线程的代码: ```kotlin GlobalScope.launch { // 子线程执行的代码 Log.d("MyCoroutine", "子线程执行中...") } ``` 在上面的代码中,使用 `GlobalScope.launch` 可以创建一个新的协程,并在其中编写子线程执行的代码。需要注意的是,在使用协程时需要避免出现线安全问题,例如避免在不同的协程中同时修改同一个变量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值