一.引言
刷短视频看到有博主提到了睡眠排序这种排序方式,听了之后感觉很有意思,原文使用 java 进行编码,这里使用 scala 重新写一遍,顺带复习一下线程使用和线程安全相关的知识。
二.睡眠排序
1.实现思路
给定正整数数组 nums,针对数组中每一个 num 启动一个 thread,thread 内执行 Thread.sleep() 方法随后返回 num,这样 nums 的大小和 sleep 时间绑定在一起,从而实现 nums 数组的从小到大排序。
2.常规线程排序实现
// 常规线程操作
def commonThreadSort(nums: Array[Int], k: Int = 1): Unit = {
nums.foreach(num => {
new Thread() {
override def run(): Unit = {
try {
Thread.sleep(num * k)
println(num)
} catch {
case e: Exception =>
e.printStackTrace()
}
}
}.start()
})
}
val short_nums = Array[Int](3, 7, 2, 10, 62, 34, 5)
val k = 1
// 直接起线程
commonThreadSort(short_nums, k)
遍历 nums 针对每个给定的 num 起一个 thread,然后 sleep 并 print 输出,看到结果似乎并不完全达到从小到大排序的要求:
2 5 3 7 10 34 62
所以我引入了调节因子 k,😴 太短就多睡会提高区分度,设置 k=5 搞定,但是会多睡一段时间 :
2 3 5 7 10 34 62
3.常规线程 + Join 实现
上面已经基本满足了睡眠排序的功能,但是没有返回值,所以这次增加了数组保存存储结果,这里需要注意数组的线程安全问题,否则容易初始不一致的情况。由于需要等待全部线程结束并将 num 保存至 list,所以这里采用 join 保证所有线程对应的 num 都写入 list 任务才结束。
def commonThreadSortWithJoin(nums: Array[Int], k: Int = 1): Unit = {
val threadBuffer = new ArrayBuffer[Thread]()
val result = new java.util.Vector[Int]()
nums.foreach(num => {
val thread = new Thread() {
override def run(): Unit = {
try {
Thread.sleep(num * k)
result.add(num)
} catch {
case e: Exception =>
e.printStackTrace()
}
}
}
threadBuffer.append(thread)
thread.start()
})
threadBuffer.foreach(thread => {
thread.join()
})
println(result.toArray().mkString(","))
}
val array = (0 to 100).toArray.toList
val long_nums = random.shuffle(array).toArray
val k = 5
commonThreadSortWithJoin(long_nums, k)
这次采用长数组进行睡眠排序,打印前对所有 ThreadBuffer 里面的 Thread 调用 join 方法进行堵塞,等待全部子线程结束,所有 num 添加至 result 后执行打印并退出。join 的缺点是会总成堵塞且两个 For 循环不是很简洁。
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
4.常规线程 + Wait And Notify
def commonThreadSortWithNotify(nums: Array[Int], k: Int = 1): Unit = {
val count: AtomicInteger = new AtomicInteger(nums.length)
val waitObject: Object = new Object()
val result = new java.util.Vector[Int]()
nums.foreach(num => {
new Thread() {
override def run(): Unit = {
try {
Thread.sleep(num * k)
result.add(num)
waitObject.synchronized {
val cnt = count.decrementAndGet()
if (cnt == 0) {
waitObject.notifyAll()
}
}
} catch {
case e: Exception =>
e.printStackTrace()
}
}
}.start()
})
waitObject.synchronized {
while (count.get() != 0) {
waitObject.wait()
}
}
println(result.toArray().mkString(","))
}
使用 Object + Synchronized 关键字进行加锁,并初始化 AtomicInteger 保证计数的原子性即线程安全的计数,每执行完一个 Thread 都会调用 AtomicInteger.decrementAndGet 方法,当该数为 0 时,代表全部 thread 执行完毕,执行 notifyAll 停止 wait,执行后续打印逻辑。继续使用上述 demo 中的 long_nums 和 k=5。这里 NotifyAll + wait + AtomicInteger 其实和 join 的思想一致,就是等待全部子线程执行结束,解除阻塞,执行最终逻辑。
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,20,19
5.线程池 + CountDownLatch
def threadPoolSortWithLatch(nums: Array[Int], k: Int = 1): Unit = {
// Wait By CountDownLatch
val result = new java.util.Vector[Int]()
val THREADNUM = 100
val executor = Executors.newFixedThreadPool(THREADNUM)
val latch: CountDownLatch = new CountDownLatch(nums.size)
nums.foreach(num => {
executor.submit(new Runnable {
override def run(): Unit = {
try {
Thread.sleep(num * k)
println(num) // 打印
latch.countDown() // 计数器-1
result.add(num) // 添加结果
} catch {
case e: Exception =>
e.printStackTrace()
}
}
})
})
try {
latch.await()
} catch {
case e: Exception =>
e.printStackTrace()
} finally {
executor.shutdown()
}
}
相比起来 Join 的两个 For 循环和 Object 的 Sync 关键字, CountDownLatch 的写法相对最优雅且最好理解,这里 latch.await 方法等待 latch 减到 0 ,随后执行后续逻辑,同样适用线程安全的 Vector 存储排序结果。除此之外,本例尝试适用 ExecutorPool 进行睡眠排序。
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
三.总结
1.线程安全问题
上述 demo 我们采用了多线程执行任务,期间涉及到 num 的存储,示例中我们使用了线程安全的
java.util.vector,除此之外一下集合也是线程安全的:
- StringBuffer : StringBuffer 和 StringBuilder 很像,后者执行速度快适用于单线程,前者使用可变对象构造字符,执行速度慢,但线程安全所以适合多线程使用
-CopyOnWriteArrayList : CopyOnWriteArrayList 通过复制机制保证了线程安全问题,但随之而来的是较高的资源消耗和执行时间
除此之外 HashTable,CouncurentHashMap 等也是线程安全的,不过本场景下有线程安全的 List 使用即可,上述实例中如果我们尝试同时使用线程安全的 Vertor 和非线程安全的集合比如 ArrayBuffer 看下效果 :
Vector: ... 41,42,43,44,45,46,47,48,49,50
ArrayBuffer: ... 41,42,43,44,45,46,null,48,49,50
使用非线程安全的集合会导致 nums 中的元素丢失,不能完整实现排序。
2.线程执行顺序
上述 demo 尝试了直接使用 Thread 和 ThreadPool,为了获取全部返回值,我们必须等待子线程全部执行完毕才能打印最终结果,常用的方法上述给出了三种:
- Join : 主线程会在 join 位置堵塞,直到子线程都执行完毕才执行后续逻辑
- Notify + Wait : 与 join 思想类似但更底层,需配合 Object 和 synchronized 关键字使用
- CountDownLatch : 通过原子性的计数器实现阻塞和等待,和 NotifyAll 的 AtomicInteger 效果类似
除此之外也可以实现 CallBack 通过 Thread 返回 num,这里就不展开了
3.睡眠排序本身
这个排序也是刷手机看到的,觉得有意思就写了一下,这个排序的使用场景很苛刻,只有算力足够大的机器才支持这样精确的计算和足够多的 Thread,例如本机 mac-mini 长数组控制在 100 以内还好,再大一点如果不增加扩大因子 k 很难获取完整的正确结果,不过作为一种尝试还是很好玩,主要是可以通过该排序复习线程相关知识。👍