一.引言

刷短视频看到有博主提到了睡眠排序这种排序方式,听了之后感觉很有意思,原文使用 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 很难获取完整的正确结果,不过作为一种尝试还是很好玩,主要是可以通过该排序复习线程相关知识。👍