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

进程与线程

  • 进程:进程就是正在执行的程序。
  • 线程:是程序执行的一条路径, 一个进程中可以包含多条线程。
    通俗理解:例如你打开微信就是打开一个进程,在微信里面和好友视频聊天就是开启了一条线程。
  • 两者之间的关系
    一个进程里面可以有多条线程,至少有一条线程。
    一条线程一定会在一个进程里面。

关于进程与线程的讲解,这篇文章讲的挺好的-->进程与线程的一个简单解释

创建线程的三种方式

一、继承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();
    //图片数量
    private       int    mImgNum;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void multiThread(View view) {
        mImgNum = 9;
        //开启3条线程上传图片
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
        new Thread(myRunnable).start();
        new Thread(myRunnable).start();
    }

    public class MyRunnable implements Runnable {

        @Override
        public void run() {
            while (true) {
                if (mImgNum == 0) {
                    Log.d(TAG, Thread.currentThread().getName() + "全部上传成功");
                    break;
                }
                try {
                    //模拟上传一张图片需要1秒钟的时间
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mImgNum--;
                Log.d(TAG, Thread.currentThread().getName() + "上传了一张图片...,还剩" + mImgNum + "张图片未上传");
            }
        }
    }
}

打印结果如下:

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

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

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

public class MainActivity extends AppCompatActivity {

    private final String TAG = this.getClass().getSimpleName();
    //图片数量
    private       int    mImgNum;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void multiThread(View view) {
        mImgNum = 9;
        //开启3条线程上传图片
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
        new Thread(myRunnable).start();
        new Thread(myRunnable).start();
    }

    public class MyRunnable implements Runnable {

        @Override
        public void run() {
            while (true) {
                //加上synchronized进行同步,保证在同一时刻只能有一个线程能够访问
                synchronized (MyRunnable.class) {
                    if (mImgNum == 0) {
                        Log.d(TAG, Thread.currentThread().getName() + "全部上传成功");
                        break;
                    }
                    try {
                        //模拟上传一张图片需要1秒钟的时间
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    mImgNum--;
                    Log.d(TAG, Thread.currentThread().getName() + "上传了一张图片...,还剩" + mImgNum + "张图片未上传");
                }
            }
        }
    }
}

打印结果如下:

image

由图可知,图片数量正常了,但是发现一秒钟只上传了一张图片,而且只有一个线程在上传(每次while循环, 哪个线程先访问是随机的,偶尔会出现3个线程都在上传的情况,线程休眠时间设置为100毫秒比较容易复现),并没有实现并发。其实这里的确解决了多线程并发操作同一数据出现线程安全的问题,但是因为这里要模拟上传图片,把休眠时间放到了synchronized中,所以导致没有并发,用线程池就不会有这个问题,这个放到后面去讲。

线程池

关于线程池

前面举的朋友圈发表图片的多线程例子中,为了提高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 ,即添加任务出错就直接抛出异常 。
四种线程池
  • FixedThreadPool:线程数量固定的线程池,空闲的线程不会被回收,超出的线程会在队列中等待。由于它只有核心线程并且不会被回收,所以能够快速响应外界的请求。
    例子:创建线程数为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个线程中执行。

  • SingleThreadExecutor:只有一个核心线程的线程池,这样能保证所有任务按指定顺序来执行,不需要处理线程同步的问题。
    例子:
        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个任务始终在1个线程中执行。

  • CachedThreadPool:线程数量不固定的线程池,它只有非核心线程。在执行新的任务时,当线程池中有之前创建的可用线程就重用之前的线程,否则就新建一条线程。如果线程池中的线程在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出现了很多次,说明有重用之前创建的线程。

  • ScheduledThreadPool:核心线程数量固定,非核心线程数量不固定的线程池,非核心线程闲置时会被立即回收,这种线程池主要用于执行定时任务和具有固定周期的重复任务。
    例子:
        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秒执行一次任务。

5.6 利用线程池实现多线程并发

前面的例子虽然解决了多线程操作同一数据出现线程安全的问题,但是并发却没了,这里利用多线程可以很好的解决。
代码如下:

public class MainActivity extends AppCompatActivity {

    private final String TAG = this.getClass().getSimpleName();
    //图片数量
    private       int    mImgNum;
    //开启3条线程上传图片
    ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void multiThread(View view) {
        mImgNum = 9;
        MyRunnable myRunnable = new MyRunnable();
        for (int i = 0; i < 9; i++) {
            //提交任务
            newFixedThreadPool.submit(myRunnable);
        }
    }

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            try {
                //模拟上传一张图片需要1秒钟的时间
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //加上synchronized进行同步,保证在同一时刻只能有一个线程能够访问
            synchronized (MyRunnable.class) {
                mImgNum--;
                Log.d(TAG, Thread.currentThread().getName() + "上传了一张图片...,还剩" + mImgNum + "张图片未上传");
                if (mImgNum == 0) {
                    Log.d(TAG, Thread.currentThread().getName() + "全部上传成功");
                }
            }
        }
    }
}

打印结果如下:

image

由图可知,每秒中上传了3张图片,确实实现了并发,而且线程安全问题也解决了。

相关源码:多线程并发 demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容