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

来源:互联网 发布:工行个人网络抵押贷款 编辑:程序博客网 时间:2024/06/08 08:06


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


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

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

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

    关于进程与线程的讲解,这篇文章讲的挺好的-->进程与线程的一个简单解释 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 秒执行一次任务。

  • 原创粉丝点击