带你通俗易懂的理解——线程、多线程与线程池

作者 | wildma

地址 | http://www.jianshu.com/u/645342ca3cad

声明 | 本文是 wildma 原创,已获授权发布,未经原作者允许请勿转载



进程与线程

  • 进程:进程就是正在执行的程序。

  • 线程:是程序执行的一条路径, 一个进程中可以包含多条线程。
    通俗理解:例如你打开微信就是打开一个进程,在微信里面和好友视频聊天就是开启了一条线程。

  • 两者之间的关系
    一个进程里面可以有多条线程,至少有一条线程。
    一条线程一定会在一个进程里面。

关于进程与线程的讲解,这篇文章讲的挺好的-->进程与线程的一个简单解释 http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html

创建线程的三种方式

一、继承Thread

1、定义一个类MyThread继承Thread,并重写run方法。
2、将要执行的代码写在run方法中。
3、创建该类的实例,并调用start()方法开启线程。


代码如下:

public class MainActivity extends AppCompatActivity {
   private final String TAG = this.getClass().getSimpleName();
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       //3、创建该类的实例,并调用start()方法开启线程。
       MyThread myThread = new MyThread();
       myThread.start();
   }
   //1、定义一个类MyThread继承Thread,并重写run方法。
   class MyThread extends Thread {
       public void run() {
           //2、将执行的代码写在run方法中。
           for (int i = 0; i < 100; i++) {
               Log.d(TAG, "线程名字:" + Thread.currentThread().getName()  + "  i=" + i);
           }
       }
   }
}
二、实现Runnable接口

1、定义一个类MyRunnable实现Runnable接口,并重写run方法。
2、将要执行的代码写在run方法中。
3、创建Thread对象, 传入MyRunnable的实例,并调用start()方法开启线程。


代码如下:

public class MainActivity extends AppCompatActivity {
   private final String TAG = this.getClass().getSimpleName();
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       //3、创建Thread对象, 传入MyRunnable的实例,并调用start()方法开启线程。
       Thread thread = new Thread(new MyRunnable());
       thread.start();
   }
   //1、定义一个类MyRunnable实现Runnable接口,并重写run方法。
   class MyRunnable implements Runnable {
       public void run() {
           //2、将执行的代码写在run方法中。
           for (int i = 0; i < 100; i++) {
               Log.d(TAG, "线程名字:" + Thread.currentThread().getName() + "  i=" + i);
           }
       }
   }
}
三、实现 Callable 接口

Callable 是类似于 Runnable 的接口,实现 Callable 接口的类和实现 Runnable 的类都是可被其它线程执行的任务。

1、自定义一个类 MyCallable 实现 Callable 接口,并重写call()方法
2、将要执行的代码写在call()方法中
3、创建线程池对象,调用submit()方法执行MyCallable任务,并返回Future对象
4、调用Future对象的get()方法获取call()方法执行完后的值


代码如下:

public class MainActivity extends AppCompatActivity {
   private final String TAG = this.getClass().getSimpleName();
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       //3、创建线程池对象,调用submit()方法执行MyCallable任务,并返回Future对象
       ExecutorService pool = Executors.newSingleThreadExecutor();
       Future<Integer> f1 = pool.submit(new MyCallable());
       //4、调用Future对象的get()方法获取call()方法执行完后的值
       try {
           Log.d(TAG, "sum=" + f1.get());
       } catch (InterruptedException e) {
           e.printStackTrace();
       } catch (ExecutionException e) {
           e.printStackTrace();
       }
       //关闭线程池
       pool.shutdown();
   }
   //1、自定义一个类MyCallable实现Callable接口,并重写call()方法
   public class MyCallable implements Callable<Integer> {
       @Override
       public Integer call() throws Exception {
           //2、将要执行的代码写在call()方法中
           int sum = 0;
           for (int i = 0; i <= 100; i++) {
               sum += i;
           }
           return sum;
       }
   }
}


创建线程的三种方式对比

一、继承 Thread 类与实现 Runnable 接口的区别

我们都知道 java 支持单继承,多实现。实现 Runnable 接口还可以继承其他类,而使用继承 Thread 就不能继承其他类了。所以当你想创建一个线程又希望继承其他类的时候就该选择实现 Runnable 接口的方式。

二、实现 Callable 接口与 Runnable 接口的区别

Callable 执行的方法是 call() ,而 Runnable 执行的方法是 run()。
call() 方法有返回值还能抛出异常, run() 方法则没有没有返回值,也不能抛出异常。

多线程

一、概念

一个进程中开启了不止一个线程。

二、多线程的优缺点
  • 优点
    1、提高CPU的使用率
    例如朋友圈发表图片,当你上传9张图片的时候,如果开启一个线程用同步的方式一张张上传图片,假设每次上传图片的线程只占用了CPU 1%d的资源,剩下的99%资源就浪费了。但是如果你开启9个线程同时上传图片,CPU就可以使用9%的资源了。
    2、提高程序的工作效率
    还是拿朋友圈发表图片来说,假设开启一个线程上传一张图片的时间是1秒,那么同步的方式上传9张就需要9秒,但是你开启9个线程同时上传图片,那么就只需要1秒就完成了。

  • 缺点
    1、如果有大量的线程,会影响性能,因为CPU需要在它们之间切换。
    2、更多的线程需要更多的内存空间。
    3、多线程操作可能会出现线程安全或者死锁等问题。

三、多线程并行和并发的区别
  • 概念
    并行:多个处理器或者多核处理器同时执行多个不同的任务。
    并发:一个处理器处理多个任务。

  • 打个比喻
    并行就是一个人用他的左手喂女儿吃饭,同时用右手喂儿子吃饭。
    并发就是一个人先喂女儿吃一口饭,接着喂儿子吃一口,然后再喂女儿吃一口,轮流喂。

  • 举个多线程并发操作同一数据出现线程安全的例子
    利用多线程上传9张图片,并提示还剩几张图片未上传。代码如下:

public class MainActivity extends AppCompatActivity {
   private final String TAG = this.getClass().getSimpleName();
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       //开启2条线程上传图片
       MyRunnable myRunnable = new MyRunnable();
       new Thread(myRunnable, "线程1").start();
       new Thread(myRunnable, "线程2").start();
   }
   public class MyRunnable implements Runnable {
       private int imgNum = 9;//图片数量
       @Override
       public void run() {
           while (true) {
               if (imgNum == 0) {
                   break;
               }
               try {
                   Thread.sleep(1000);//模拟上传一张图片需要1秒钟的时间
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               Log.d(TAG, Thread.currentThread().getName() + "正在上传图片...,还剩" + imgNum-- + "张图片未上传");
           }
       }
   }
}

打印结果如下:

由图可知,图片数量出现了负数,显然是错误的。

原因:
出现这种错误的原因是有多个线程在操作共享的数据。即一个线程在操作共享数据的过程中CPU切换到其他线程又对该数据进行操作,这就是所谓的多线程并发。

解决:
把操作数据的那段代码用 synchronized 进行同步, 这样就能保证在同一时刻只能有一个线程能够访问。


代码如下:

public class MainActivity extends AppCompatActivity {
   private final String TAG = this.getClass().getSimpleName();
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       //开启2条线程上传图片
       MyRunnable myRunnable = new MyRunnable();
       new Thread(myRunnable, "线程1").start();
       new Thread(myRunnable, "线程2").start();
   }
   public class MyRunnable implements Runnable {
       private int imgNum = 9;//图片数量
       @Override
       public void run() {
           while (true) {
               synchronized (MyRunnable.class) { //加上synchronized进行同步,保证在同一时刻只能有一个线程能够访问。
                   if (imgNum == 0) {
                       break;
                   }
                   try {
                       Thread.sleep(1000);//模拟上传一张图片需要1秒钟的时间
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   Log.d(TAG, Thread.currentThread().getName() + "正在上传图片...,还剩" + imgNum-- + "张图片未上传");
               }
           }
       }
   }
}

打印结果如下:

线程池

关于线程池

前面举的朋友圈发表图片的多线程例子中,为了提高CPU的使用率和程序的工作效率就需要创建9个线程来上传图片。但是线程的创建和销毁是非常耗CPU和内存的,因为它涉及到要与操作系统进行交互。这样就可能导致创建与销毁线程的开销比实际业务还大,而线程池就能很好的解决这些问题。线程池里的每一个线程结束后,并不会销毁(可以设置超时销毁),而是回到线程池中成为空闲状态,等待下一个对象来使用。

使用线程池的优点

1、减少创建与销毁线程带来的性能开销。
2、可控制最大并发线程数,避免过多资源竞争而导致系统内存消耗完。
3、能更好的控制线程的开启与回收,并且能定时执行任务。

线程池中重要的几个类
  • Executor:java中线程池的顶级接口,可以称它为一个执行器,通过查看源码也知道,他只有一个简单的方法execute(Runnable command),就是用来执行提交的任务。源码如下:
    【Executor.java】

public interface Executor {
   void execute(Runnable command);
}
  • ExecutorService:Executor的子类,也是真正的线程池接口。它提供了提交任务和关闭线程池等方法。调用submit方法提交任务还可以返回一个Future对象,利用该对象可以了解任务执行情况,获得任务的执行结果或取消任务。

  • Executors:由于线程池配置比较复杂,自己配置的线程池可能性能不是最好的。Executors就是用来方便创建各种常用线程池的工具类。

  • ThreadPoolExecutor:ExecutorService的默认实现,Executors创建各种线程池的时候内部其实就是调用了ThreadPoolExecutor的构造方法。下面通过查看源码验证。
    例如随便创建一个线程池:

ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

点击newCachedThreadPool()进去,里面确实调用了ThreadPoolExecutor的构造方法,如下:
【Executor.java】

    public static ExecutorService newCachedThreadPool() {
       return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                     60L, TimeUnit.SECONDS,
                                     new SynchronousQueue<Runnable>());
   }
ThreadPoolExecutor构造函数参数说明

下面从源码中拿一个参数最完整的来讲解,如下:
【ThreadPoolExecutor.java】

    public ThreadPoolExecutor(int corePoolSize,
                             int maximumPoolSize,
                             long keepAliveTime,
                             TimeUnit unit,
                             BlockingQueue<Runnable> workQueue,
                             ThreadFactory threadFactory,
                             RejectedExecutionHandler handler)
{
   }
  • corePoolSize:核心线程数,如果运行的线程数少于corePoolSize,当有新的任务过来时会创建新的线程来执行这个任务,即使线程池中有其他空闲的线程。

  • maximumPoolSize:线程池中允许的最大线程数。

  • keepAliveTime:如果线程数多于核心线程数,那么这些多出来的线程如果空闲时间超过keepAliveTime将会被终止。

  • unit:keepAliveTime参数的时间单位。

  • workQueue:任务队列,通过线程池的execute方法会将任务Runnable存储在队列中。

  • threadFactory:线程工厂,用来创建新线程。

  • handler:添加任务出错时的策略捕获器,默认是ThreadPoolExecutor.AbortPolicy ,即添加任务出错就直接抛出异常 。

四种线程池
  • newFixedThreadPool:创建固定大小的线程池,这样可以控制线程最大并发数,超出的线程会在队列中等待。如果线程池中的某个线程由于异常而结束,线程池则会再补充一条新线程。
    例子:创建线程数为3的线程池

        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
       for (int i = 0; i < 10; i++) {
           final int finalI = i;
           newFixedThreadPool.execute(new Runnable() {
               @Override
               public void run() {
                   try {
                       Thread.sleep(1000);
                       Log.d(TAG, "线程名字:" + Thread.currentThread().getName() + "  i=" + finalI);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           });
       }

打印结果:

由打印结果可知,10个任务始终在3个线程中执行。

  • newSingleThreadExecutor:创建一个单线程的线程池,即这个线程池永远只有一个线程在运行,这样能保证所有任务按指定顺序来执行。如果这个线程异常结束,那么会有一个新的线程来替代它。


    例子:

        ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
       for (int i = 0; i < 10; i++) {
           final int finalI = i;
           newSingleThreadExecutor.execute(new Runnable() {
               @Override
               public void run() {
                   try {
                       Thread.sleep(1000);
                       Log.d(TAG, "线程名字:" + Thread.currentThread().getName() + "  i=" + finalI);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           });
       }

打印结果:

由打印结果可知,10个任务始终在3个线程中执行。

  • newCachedThreadPool:创建带有缓存的线程池,在执行新的任务时,当线程池中有之前创建的可用线程就重用之前的线程,否则就新建一条线程。如果线程池中的线程在60秒未被使用,就会将它从线程池中移除。
    例子:

        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
       for (int i = 0; i < 10; i++) {
           final int finalI = i;
           newCachedThreadPool.execute(new Runnable() {
               @Override
               public void run() {
                   Log.d(TAG, "线程名字:" + Thread.currentThread().getName() + "  i=" + finalI);
               }
           });
       }


由打印结果可知,线程1出现了很多次,说明有重用之前创建的线程。

  • newScheduledThreadPool:创建定时和周期性执行的线程池。

        ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
       /**
        * 延迟2秒执行任务
        */

       newScheduledThreadPool.schedule(new Runnable() {
           @Override
           public void run() {
               Log.d(TAG, "线程名字:" + Thread.currentThread().getName() + "定时任务");
           }
       }, 2, TimeUnit.SECONDS);
       /**
        * 延迟1秒后每2秒执行一次任务
        */

       newScheduledThreadPool.scheduleAtFixedRate(new Runnable() {
           @Override
           public void run() {
               Log.d(TAG, "线程名字:" + Thread.currentThread().getName() + "周期性任务");
           }
       }, 1, 2, TimeUnit.SECONDS);


由打印结果可知,定时任务是 2 秒后执行任务,周期性任务是延迟 1 秒后每 2 秒执行一次任务。


与之相关

6 天时间修改 1 行代码:现实中的软件开发流程

如何有效报告 bug


赞助商

优秀人才不缺工作机会,只缺适合自己的好机会。但是他们往往没有精力从海量机会中找到最适合的那个。100offer 会对平台上的人才和企业进行严格筛选,让「最好的人才」和「最好的公司」相遇。扫描下方二维码,注册 100offer,谈谈你对下一份工作的期待。一周内,收到 5-10 个满足你要求的好机会!

  • 13
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值