Kotlin Java多线程编程安全
创始人
2024-05-24 07:04:59
0

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

    var sum = 0@Testfun 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等这些错误的结果。

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

 @Testfun hello() = runBlocking {var coroutines = listOf()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

对一个例子的修复:

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

对第二个例子的修复:

@Testfun hello() = runBlocking {var coroutines = listOf()// 使用固定大小的线程池创建协程的执行上下文// @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@Synchronizedfun accumulateShareSum(){shareSum += 1}

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

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

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

    @Synchronizedfun 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 = 0fun accumulateShareSum(){reentrantLock.lock()try {shareSum += 1} finally {reentrantLock.unlock()}}

解决办法4:信号量

我们直接上代码吧:

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

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

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...