1、线程与进程的区别

进程是所有线程的集合,每一个线程是进程中的一条执行路径。

比方:通过查看 windows 任务管理器中的列表,我们可以把运行在内存中的 exe 文件理解成进程,进程是受操作系统管理的基本运行单元。

2、为什么要使用多线程?

主要体现在多线程提高程序效率,但是需要注意,并不是使用了多线程就一定能提升性能,有的情况反而会降低性能。

多线程应用场景:

2.1、避免阻塞

我们知道,在我们单线程中,代码是顺序执行的,如果前面的操作发生了阻塞,那么就会影响到后面的操作,这时候可以采用多线程,可以理解成异步调用;其实前端里的 ajax 就是一个很好地例子,默认 ajax 是开启异步的,调用时浏览器会启一个新的线程,不阻塞当前页面的正常操作;

2.2、避免CPU空转

以http server为例,如果只用单线程响应HTTP请求,即处理完一条请求,再处理下一条请求的话,CPU会存在大量的闲置时间;

因为处理一条请求,经常涉及到RPC、数据库访问、磁盘IO等操作,这些操作的速度比CPU慢很多,而在等待这些响应的时候,CPU却不能去处理新的请求,因此http server的性能就很差;

所以很多web容器,都采用对每个请求创建新线程来响应的方式实现,这样在等待请求A的IO操作的等待时间里,就可以去继续处理请求B,对并发的响应性就好了很多 。

3、多线程常见的两种创建方式

3.1、继承Thread类,重写run方法
/**
 * author:  niceyoo
 * blog:    https://cnblogs.com/niceyoo
 * desc:
 */

public class ThreadDemo {
    public static void main(String[] args{
        System.out.println("-----多线程创建开始-----");
        /* 1.创建一个线程*/
        CreateThread createThread = new CreateThread();
        /* 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法*/
        System.out.println("-----多线程创建启动-----");
        createThread.start();
        System.out.println("-----多线程创建结束-----");
    }
}

class CreateThread extends Thread {

    /*run方法中编写 多线程需要执行的代码*/
    @Override
    public void run() 
{
        for (int i = 0; i< 10; i++) {
            System.out.println("i:" + i);
        }
    }
}

打印结果:

-----多线程创建开始-----
-----多线程创建启动-----
-----多线程创建结束-----
i:0
i:1
i:2
i:3
i:4
i:5
i:6
i:7
i:8
i:9

线程调用 start() 方法后,代码并没有从上往下执行,而是有一条新的执行分支。

注意:画图演示多线程不同执行路径。

并发编程快速入门_Java基础
3.2、实现Runnable接口,重写run方法
/**
 * author:  niceyoo
 * blog:    https://cnblogs.com/niceyoo
 * desc:
 */

class CreateRunnable implements Runnable {

    @Override
    public void run() 
{
        for (int i = 0; i< 10; i++) {
            System.out.println("i:" + i);
        }
    }
}

public class ThreadDemo2 {
    public static void main(String[] args{
        System.out.println("-----多线程创建开始-----");
        /* 1.创建一个线程 */
        CreateRunnable createThread = new CreateRunnable();
        /* 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法 */
        System.out.println("-----多线程创建启动-----");
        Thread thread = new Thread(createThread);
        thread.start();
        System.out.println("-----多线程创建结束-----");
    }
}

打印结果:

-----多线程创建开始-----
-----多线程创建启动-----
-----多线程创建结束-----
i:0
i:1
i:2
i:3
i:4
i:5
i:6
i:7
i:8
i:9
使用继承Thread类还是使用实现Runnable接口好?

使用实现Runnable接口好,继承方式的扩展性不强,java总只支持单继承,如果一个类继承Thread就不能继承其他的类了。

4、守护线程

java 中有两种线程,一种是用户线程,一种是守护线程。

  • 用户线程:指用户自定义创建的线程,主线程停止,用户线程不会停止。

  • 守护线程:当前进程不存在或主线程停止,守护进程也会被停止。

如何使用守护线程?

只需要调用 setDaemon(true) 方法即可设置为守护线程。

/**
 * author:  niceyoo
 * blog:    https://cnblogs.com/niceyoo
 * desc:
 */

public class DaemonThread {
    public static void main(String[] args{
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() 
{
                while (true) {
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                    }
                    System.out.println("我是子线程...");
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            System.out.println("我是主线程");
        }
        System.out.println("主线程执行完毕!");
    }
}

运行结果:

...
我是主线程
我是子线程...
我是主线程
主线程执行完毕!

从运行结果看到,main函数执行完了,守护线程也跟着停止了。

5、多线程运行状态

线程从创建、运行到结束,总是处于下面五个状态之一:

新建状态、就绪状态、运行状态、阻塞状态以及死亡状态。

并发编程快速入门_并发编程_02
5.1、新建状态

当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码

5.2、就绪状态

一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。

处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。

5.3、运行状态

当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.

5.4、阻塞状态

线程运行过程中,可能由于各种原因进入阻塞状态:

  1. 线程通过调用sleep方法进入睡眠状态;
  2. 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
  3. 线程试图得到一个锁,而该锁正被其他线程持有;
  4. 线程在等待某个触发条件;
5.5、死亡状态

有两个原因会导致线程死亡:

  1. run方法正常退出而自然死亡,
  2. 一个未捕获的异常终止了run方法而使线程猝死。

为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用 isAlive() 方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.

6、join()方法的作用

在多线程中也是有执行的优先级的,所谓的优先级,就是cpu是否格外关注这位小兄弟,优先级越大,自然获得的好处就越多。

当在主线程当中执行到 小弟.join() 方法时,就认为主线程应该把执行权让给 小弟。

举一个例子:

创建一个线程,如何让子线程执行完毕后,主线程才能执行呢?

public class ThreadDemo3 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() 
{
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {

                    }
                    System.out.println(Thread.currentThread().getName() + "i:" + i);
                }
            }
        });
        t1.start();
        /* 当在主线程当中执行到t1.join()方法时,就认为主线程应该把执行权让给t1 */
        t1.join();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {

            }
            System.out.println("main" + "i:" + i);
        }
    }
}

打印结果:

Thread-0i:0
Thread-0i:1
Thread-0i:2
Thread-0i:3
Thread-0i:4
Thread-0i:5
Thread-0i:6
Thread-0i:7
Thread-0i:8
Thread-0i:9
maini:0
maini:1
maini:2
maini:3
maini:4
maini:5
maini:6
maini:7
maini:8
maini:9

7、优先级

虽然上边在介绍 join 方法时提到了优先级,但是在使用 join() 方法后,该线程却变成了完全主导,这或许并不是你想要的结果。

现代操作系统基本采用时分的形式调度运行的线程,线程分配得到的时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。下面是源码(基于1.8)中关于priority的一些量和方法。

class PrioritytThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().toString() + "---i:" + i);
        }
    }
}

public class ThreadDemo4 {
    public static void main(String[] args) {
        PrioritytThread prioritytThread = new PrioritytThread();
        Thread t1 = new Thread(prioritytThread);
        Thread t2 = new Thread(prioritytThread);
        t1.start();
        /* 注意设置了优先级, 不代表每次都一定会被执行。 只是CPU调度会有限分配 */
        t1.setPriority(10);
        t2.start();
    }
}

打印结果:

Thread[t1,10,main]---i:0
Thread[t1,10,main]---i:1
Thread[t1,10,main]---i:2
Thread[t1,10,main]---i:3
Thread[t1,10,main]---i:4
Thread[t1,10,main]---i:5
Thread[t1,10,main]---i:6
Thread[t1,10,main]---i:7
Thread[t1,10,main]---i:8
Thread[t1,10,main]---i:9
Thread[t2,5,main]---i:0
Thread[t2,5,main]---i:1
Thread[t2,5,main]---i:2
Thread[t2,5,main]---i:3
Thread[t2,5,main]---i:4
Thread[t2,5,main]---i:5
Thread[t2,5,main]---i:6
Thread[t2,5,main]---i:7
Thread[t2,5,main]---i:8
Thread[t2,5,main]---i:9

7、常见的面试题

进程与线程的区别?

答:进程是所有线程的集合,每一个线程是进程中的一条执行路径。

为什么要用多线程?

答:提高程序效率

多线程创建方式?

答:继承Thread或Runnable 接口。

使用继承Thread类还是使用实现Runnable接口好?

答:实现Runnable接口好,继承方式的扩展性不强,java总只支持单继承,如果一个类继承Thread就不能继承其他的类了。

你在哪里用到了多线程?

答:主要能体现到多线程提高程序效率。

举例:分批发送短信。

8、最后总结

我们了解了什么是线程,线程是一条执行路径,每个线程互不影响;

了解了什么是多线程,多线程在一个线程中,有多条不同的执行路径,并行执行,目的为了提高程序效率。

了解了线程创建常见的两种方式:继承Thread类实现run方法,或者实现Runnable接口。

事实上,实际开发中这两种方式并不常见,而是使用线程池来进行管理。

了解了线程的几种状态,新建、就绪、运行、阻塞、死亡。

了解了线程里面也是有优先级的,用数值1-10来记录,默认是5,最大是10,通过调用 setPriority(10) 来设置优先级,需要一提的是,并不是优先级越大就一定要先执行完,只是优先执行完的概率要大。

我创建了一个java相关的公众号,用来记录自己的学习之路,感兴趣的小伙伴可以关注一下微信公众号哈:niceyoo

并发编程快速入门_Java基础_03