多线程快速入门

1.线程与进程

1.1何为进程

        进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。
在这里插入图片描述

1.2何为线程

        线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
        举个栗子 我们使用网易云音乐听歌 你能听到音乐的声音就是来自一个线程,你能看到歌词显示在屏幕上这是来自另一个线程
        再举个栗子 当我们使用迅雷下载小电影的时候 你可以同时下载几部小电影这就归功于多线程 每部电影至少有一个独立的线程在执行你的任务
如下图所示,打开迅雷的设置可以看到这些与线程相关的配置项
在这里插入图片描述

1.3 线程与进程的区别

        每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
        使用线程可以把占据时间长的程序中的任务放到后台去处理,程序的运行速度可能加快,在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下可以释放一些珍贵的资源如内存占用等等。
        如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换,更多的线程需要更多的内存空间,线程的中止需要考虑其对程序运行的影响。通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。
       &nbsp线程是进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反
        总结:进程是所有线程的集合,每一个线程是进程中的一条执行路径。

2.多线程应用场景

        个人觉得多线程有些场景下很像ajax异步请求可以处理很多互不影响的业务逻辑以加快程序运行速度
        a.图片上传业务
        b.java导出大量数据生成Excel表格 假如需要导出十万条数据 可以开启5个线程 将任务拆分成5个万条 这样时间缩短了5倍
        c.做批量发送短信、微信推送提醒、消息等需求的时候 可以开启多个线程去同时执行 同b
        d.一个非常庞大的后台管理系统(先不考虑用ElasticSearch优化) 当展示的数据来自多个库的多个表 你需要返回具体数据和总个数(用于分页) 当sql已经优化到了极致但查询仍然很慢 假设具体数据需要2s 总个数需要2s 相当于执行完至少4s 此时使用多线程 让主线程查询具体数据 并开启一个新的线程FutureTask去查询总个数
这样只需要2s左右就可以得到返回结果
        e.迅雷的断点续传功能
        f.还有很多很多…比如你要批量插入数据并且不需要等待插入任务执行完就可以返回前端插入成功 就可以开启多线程

3.线程创建的四种方式

参考文章:线程创建的四种方式
通过查看源码会发现,其实真正原理上只有一种方式,就是
Thread thread = new Thread();
其他方式都是基于它的

4.守护线程

        Java中有两种线程,一种是用户线程,另一种是守护线程。用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。守护线程当进程不存在或主线程停止,守护线程也会被停止。
        使用setDaemon(true)方法设置为守护线程

public class T {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                    System.out.println("我是子线程...");
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {
                // TODO: handle exception
            }
            System.out.println("我是主线程");
        }
        System.out.println("主线程执行完毕!");
    }
}

上述代码中thread线程会随着主线程结束而结束掉
如果将代码thread.setDaemon(true);注掉再去运行会发现主线程结束后thread线程仍会继续运行

5.多线程运行状态

在这里插入图片描述

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 阻塞状态

        线程运行过程中,可能由于各种原因进入阻塞状态:
a.线程通过调用sleep方法进入睡眠状态;
b.线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
c.线程试图得到一个锁,而该锁正被其他线程持有;
d.线程在等待某个触发条件;

5.5 死亡状态

有两个原因会导致线程死亡:
        (1)run方法正常退出而自然死亡,
        (2)一个未捕获的异常终止了run方法而使线程猝死。
        为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.

6.多线程有三大特性

6.1原子性

        即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
一个很经典的例子就是银行账户转账问题:
        比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
        我们操作数据也是如此,比如i = i+1;其中就包括,读取i的值,计算i,写入i。这行代码在Java中是不具备原子性的,则多线程运行肯定会出问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。
原子性其实就是保证数据一致、线程安全一部分,

6.2可见性

        当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
        若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

6.3有序性

        程序执行的顺序按照代码的先后顺序执行。
        一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如下:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
则因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4
但绝不可能 2-1-4-3,因为这打破了依赖关系。
显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

面试题

1.进程与线程的区别?
答:进程是所有线程的集合,每一个线程是进程中的一条执行路径,线程只是一条执行路径。
2.为什么要用多线程?
答:提高程序效率
3.多线程创建方式?
答:继承Thread或Runnable 接口。
4.是继承Thread类好还是实现Runnable接口好?
答:Runnable接口好,因为实现了接口还可以继续继承。继承Thread类不能再继承。
5.你在哪里用到了多线程?
答:主要能体现到多线程提高程序效率。
详见 多线程应用场景
6.说说并发与并行的区别?
并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
并行: 单位时间内,多个任务同时执行。
白话文:
并发等于一个人吃火锅点了多个配菜,一会吃虾滑,一会吃羊肉卷
并行等于多个人一起吃火锅点了多个配菜,A在吃虾滑和羊肉卷,B也在吃虾滑和羊肉卷
7.使用多线程可能带来什么问题?
        并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
8.说说线程的生命周期和状态?
详见5.多线程运行状态
9.说说 sleep() 方法和 wait() 方法区别和共同点?
        两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
        两者都可以暂停线程的执行。
        Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
        wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。
10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
        new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
        总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值