多线程基础学习二:Runnable基础学习

梦里梦外; 2022-06-08 07:29 308阅读 0赞

前几天学习了Thread的基础知识,今天开始学习一下Runnable的基础知识。

Runnable与Thread的关系

Thread实现了Runnable,也就是说Thread是Runnable的一个实现类,并且扩展了很多有用的方法。
源码:

  1. public
  2. class Thread implements Runnable {
  3. `````
  4. }

可以确认的是,如果Runnable能实现一个功能,那Thread也能实现,如果Runnable无法实现二代功能,Thread可能能实现。

Runnable的使用

一个简单的例子:

  1. public class RunnableTest {
  2. public static void main (String[] args) {
  3. Thread thread = new Thread(new TestRunnable());
  4. thread.setDaemon(true);
  5. thread.start();
  6. SleepUtil.sleep(2000);
  7. }
  8. static class TestRunnable implements Runnable {
  9. @Override
  10. public void run () {
  11. System.out.println("这是一个简单的Runnable");
  12. }
  13. }
  14. }

从这里看到Runnable和Thread的区别,
– Thread需要继承,Runnable需要实现;
– Runnable没有start方法,没有办法启动线程,需要把实现类交给Thread处理。(当然如果实现Runnable时,像Thread一样,创建一个功能和Thread 的start方法一样的方法,也是可以的)

Runnable与Thread的区别

  • Thread需要继承,Runnable需要实现

Thread本身就是Runnable的实现类,两种实现方式的优缺点是有继承和实现这两个方式带来,Runnable能实习的功能,Thread都能实现,甚至能提供更多有用的方法

– 继承只能继承一个,实现可以实现多个接口;

这是两种实现方式造成的区别之一,不过根据我在项目中看到和自己使用的经历来看,基本没有遇到需要实现多个接口的场景,所以说这个区别大多数情况下,没什么影响。

以上是我认为的区别

下面是在网上查看博客学习时,看到的区别

– Runnable相比Thread,代码被多个线程共享,代码和数据独立
我不知道这种说法对不对,博客上很多人这么写,博客评论里也有很多人说这是错的,所以写代码测一下。
就用很多博客里写的那个卖票的例子。

Runnable:

  1. public class RunnableTest {
  2. public static void main (String[] args) {
  3. Runnable test = new TestRunnable();
  4. Thread first = new Thread(test, "first");
  5. first.setDaemon(true);
  6. first.start();
  7. Thread second = new Thread(test, "second");
  8. second.setDaemon(true);
  9. second.start();
  10. SleepUtil.sleep(50000);
  11. }
  12. static class TestRunnable implements Runnable {
  13. int i = 5;
  14. @Override
  15. public void run () {
  16. for (int j = 0; j < 10; j++) {
  17. if (i > 0) {
  18. //SleepUtil.sleep(1000);
  19. String name = Thread.currentThread().getName();
  20. System.out.println("线程" + name + "卖出了一张票" + i--);
  21. }
  22. }
  23. }
  24. }
  25. }

这是网上很多博客用来说明这个区别时,所举的例子。

运行结果:

  1. 线程first卖出了一张票5
  2. 线程second卖出了一张票4
  3. 线程first卖出了一张票3
  4. 线程second卖出了一张票2
  5. 线程first卖出了一张票1

只从结果上看,这个是对的,5张票被两个线程卖掉了,没多卖也没少卖。

多次运行(大概90多次)后出现其它结果:
第一种:

  1. 线程first卖出了一张票5
  2. 线程first卖出了一张票4
  3. 线程first卖出了一张票3
  4. 线程first卖出了一张票2
  5. 线程first卖出了一张票1

这是一个线程卖了5张票,另外一个线程无票可卖。

第二种(出现8-10次):

  1. 线程second卖出了一张票5
  2. 线程first卖出了一张票5
  3. 线程second卖出了一张票4
  4. 线程first卖出了一张票3
  5. 线程second卖出了一张票2
  6. 线程first卖出了一张票1

这是两个线程都卖了同一张票(票5)

第三种(出现2次):

  1. 线程second卖出了一张票5
  2. 线程first卖出了一张票4
  3. 线程second卖出了一张票3
  4. 线程first卖出了一张票2
  5. 线程second卖出了一张票1
  6. 线程first卖出了一张票0

有一个线程卖了一张不存在的票。

显而易见,大多情况正确,部分情况错误,i 的值在多线程的情况下不能保证原子性。如果加了同步关键字,执行结果有事什么样子呢?

代码修改:

  1. static class TestRunnable implements Runnable {
  2. int i = 5;
  3. @Override
  4. public synchronized void run () {
  5. for (int j = 0; j < 10; j++) {
  6. if (i > 0) {
  7. //SleepUtil.sleep(1000);
  8. String name = Thread.currentThread().getName();
  9. System.out.println("线程" + name + "卖出了一张票" + i--);
  10. }
  11. }
  12. }
  13. }

测试结果(执行40多次):
第一种(多数):

  1. 线程first卖出了一张票5
  2. 线程first卖出了一张票4
  3. 线程first卖出了一张票3
  4. 线程first卖出了一张票2
  5. 线程first卖出了一张票1

第二种(少数):

  1. 线程second卖出了一张票5
  2. 线程second卖出了一张票4
  3. 线程second卖出了一张票3
  4. 线程second卖出了一张票2
  5. 线程second卖出了一张票1

可以看到,没有出现两个线程一起卖的情况了,只有一个线程把所有的票卖完了。

这个可以这样解释,for遍历了10次,总共有5张票,又有关键字锁定了Runnable实例,导致只有会有一个线程把票卖完。

如果把for修改,让它在同步的情况下,不能把所有的票卖完,估计结果又不一样。

修改:

  1. public synchronized void run () {
  2. for (int j = 0; j < 3; j++) {
  3. if (i > 0) {
  4. //SleepUtil.sleep(1000);
  5. String name = Thread.currentThread().getName();
  6. System.out.println("线程" + name + "卖出了一张票" + i--);
  7. }
  8. }
  9. }

执行结果 (执行15次):

第一种:

  1. 线程first卖出了一张票5
  2. 线程first卖出了一张票4
  3. 线程first卖出了一张票3
  4. 线程second卖出了一张票2
  5. 线程second卖出了一张票1

第二种:

  1. 线程second卖出了一张票5
  2. 线程second卖出了一张票4
  3. 线程second卖出了一张票3
  4. 线程first卖出了一张票2
  5. 线程first卖出了一张票1

可以看到,只有两种结果,一个线程卖三张,另外一个线程卖两张。
这也很容易理解,同步的情况下,遍历三次,一个线程最多卖三张,线程执行结束,剩下两张有另外一个想成执行。

根据以上测试可知,为了能两个线程一起卖,就不能加synchronized或者在添加同步的情况控制遍历次数;没有添加同步的情况,这样的写法是有问题的,不能保证数据的原子性;添加同步的情况下,如果遍历的数量大于票数,使用多线程也是没有意义的(只会有一个线程卖完票);添加同步的情况下,如果遍历的数量小与票数,有可能票卖不完。
以上是往里例子的测试总结。

实际上Thread也可以做到多个线程处理同一个资源,一下是用Thread写的,

代码:

  1. public class ThreadTest {
  2. private static AtomicLong i = new AtomicLong(10);
  3. public static void main (String[] args) {
  4. Thread first = new TestThread();
  5. first.setDaemon(true);
  6. first.start();
  7. Thread second = new TestThread();
  8. second.setDaemon(true);
  9. second.start();
  10. SleepUtil.sleep(50000);
  11. }
  12. static class TestThread extends Thread {
  13. @Override
  14. public void run () {
  15. while (i.get() > 0) {
  16. //System.out.println(i.get());
  17. String name = Thread.currentThread().getName();
  18. System.out.println("线程" + name + "卖出了一张票" + i);
  19. i.decrementAndGet();
  20. }
  21. }
  22. }
  23. }

执行结果:
第一种:

  1. 线程Thread-0卖出了一张票10
  2. 线程Thread-0卖出了一张票9
  3. 线程Thread-0卖出了一张票8
  4. 线程Thread-0卖出了一张票7
  5. 线程Thread-0卖出了一张票6
  6. 线程Thread-0卖出了一张票5
  7. 线程Thread-0卖出了一张票4
  8. 线程Thread-0卖出了一张票3
  9. 线程Thread-0卖出了一张票2
  10. 线程Thread-0卖出了一张票1

第二种:

  1. 线程Thread-0卖出了一张票10
  2. 线程Thread-0卖出了一张票9
  3. 线程Thread-0卖出了一张票8
  4. 线程Thread-1卖出了一张票8
  5. 线程Thread-0卖出了一张票7
  6. 线程Thread-1卖出了一张票6
  7. 线程Thread-0卖出了一张票5
  8. 线程Thread-1卖出了一张票4
  9. 线程Thread-0卖出了一张票3
  10. 线程Thread-1卖出了一张票2
  11. 线程Thread-0卖出了一张票1

可能还有全是线程-1的,没有充分测试。
这样写也实现了代码复用,数据共享处理,也没有出现多卖的情况。

总而言之,网上很多博客里总结这个【代码被多个线程共享,代码和数据独立】区别,我认为不存在这种区别,网上很多博客里的demo,参考即可,对于我目前项目中遇到的使用场景,没什么太大的意义。

发表评论

表情:
评论列表 (有 0 条评论,308人围观)

还没有评论,来说两句吧...

相关阅读

    相关 java 线基础学习

    一、线程概念  1、进程:程序运行资源分配的最小单位,每个进程都有自己独立的代码和数据空间,操作系统为进程分配各种资源。  2、线程:CPU调度的最小单位,也叫轻量级进程,