初识多线程
一文彻底搞懂多线程、高并发原理和知识点
📁Java并发体系
彻底理解Java中的各种锁-图灵课堂
进程和线程:
(1)什么是进程
1)例如记事本酷狗音乐等软件,程序启动运行后,操作系统会给其分配系统资源和内存空间;进程是系统层面的资源调度和分配单位;
2)进程就是在内存中运行的应用程序,进程都有自己独立的内存空间,该进程在执行过程中可以独享该块内存空间,所以一个进程出问题,不会影响其他进程;
3)大部分简单的应用程序只会对应一个进程;某些复杂的如杀毒软件、数据库软件等是由多个进程组成的;
4)早期的操作系统是单任务的,同时只能运行一个程序;现在的操作系统大多支持多程序同时执行,这就是进程在起作用。
(2)什么是线程
1) 线程是进程的一个执行单元,其负责当前进程中某部分程序的执行。一个进程中至少有一个线程,一个进程也可以运行多个线程,多个线程间可共享数据;
2.1)多个线程是共享某进程的内存等资源的,一个线程没有独立的运行空间,一个线程出问题,会影响其他线程的正常运行;所以,多进程的程序比多线程的程序更健壮;但是,因为多进程切换时,资源损耗较大,所以多进程比多线程效率低。
2.2)与进程不同的是同类的多个线程共享进程的 堆 和 方法区 资源,但每个线程有自己的 程序计数器 、 虚拟机栈 和本地方法栈 ,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
(3)进程和线程的区别
(摘自:https://blog.csdn.net/ThinkWon/article/details/102021274)
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
根本区别 :进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配 :同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系 :一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
(4)线程提升效率的原因
【多线程能提高运行效率的前提】是线程本身不会一直占用CPU且不占CPU的空闲时间大于线程切换时间,否则在线程时间运行效率变低,在线程需100%占用CPU时,CPU利用率也不会得到提升。即线程是一种压榨CPU的一种方式,使CPU尽量一刻不得闲,从而提升效率。如果,CPU已经达到了极限,再使用线程是起不到作用的。
Thread类创建线程:
(1)Thread类的构造方法:
●Thread(Runnable target),正是通过Runnable接口创建线程时需要用到的构造方法;
(2)Thread类常用方法
●不同功能的线程的区别就在于run()方法中的代码不同;
(3)Thread类也是实现了Runnable接口的
(4)Thread类创建线程示例程序
Java规定一个类必须继承Thread类(或实现Runnable接口)才能成为一个线程类,Java就是这样设计和规定的。
简单示例:其中通过 MyThread myThread = new MyThread(); 实例化线程类得到一个线程,主方法又是一个线程;
(1)创建一个类继承Thread类,得到线程类;(2)线程类添加run()方法,在run方法中添加线程相关代码;(3)实例化线程类对象,调用start()方法启动线程;
……………………………………………………
简单示例:同上;
(1)创建一个类继承Thread类,得到线程类;
(2)线程类添加run()方法;
(3)线程类添加有参构造方法,通过调用Thread类的有参构造,得线程类的有参构造,该有参构造方法可以指定线程的name;
(4)调用线程类的有参构造,MyThread mythread1 = new MyThread(“线程1”);创建线程;线程类每实例化一次就得到一个实际的线程;
(5)在线程类中可以直接调用getName()方法得到当前线程的name;
Runnable接口创建线程
这种方式更常用,因为Java不支持多继承。
(1)Runnable接口说明
(2)示例程序
本例是最容易理解的形式:
可以发现(
1)Printrunnable类实现Runnable接口,并实现run()方法,成为线程类;
(2)通过Thread.currentThread().getName()获取线程name,因为Runnable接口中没有getName()方法,getName()方法实在Thread类的,所以通过Thread类去获取线程名;
(3)实例化线程类对象,通过Thread的有参构造:Thread(Runnable target),得到最终的线程对象;
注:通过(3)可以发现,只有Thread类的对象,或者Thread子类的实例对象,才是最终的可以启动的进程对象;一个实现了Runnable接口的类的实例对象,可以理解为是具备了线程潜质的对象,但其并不是一个可以启动的线程对象,其还是要通过Thread thread = new Thread(Runnable target)来得到最终的线程对象。
输出结果:
……………………………………………………
对上示例改造如下:线程类只实例化一次,通过多次调用Thread thread = new Thread(Runnable target)得到多个线程;
输出结果:由结果也可发现,同一个线程类的实例化对象,如果多次调用Thread thread = new Thread(Runnable target),也会得到不同的线程
……………………………………………………
对上示例改造如下:把循环变量i转移到循环外
输出结果:多个线程处理同一个资源的情况,这是多个线程处理同一个资源的情况。之所以是多个线程处理同一个资源,是因为上面程序中,两个Thread t1 =new Thread(pr1)和Thread t2 = new Thread(pr1)使用了同一个Runnable类型的对象。
1.线程的状态
●新建(New):当创建一个Thread类或Thread子类对象时,线程就进入了新建状态;
●可运行(Runnable):当创建好的线程对象去调用start()方法后,线程就进入了可运行状态;具体线程运行与否要看CPU有没有空,即这个线程获取了CPU的使用权,这个处于可运行状态的线程才会实际执行;有时,也称可运行状态为“就绪状态”。
●正在运行(Running):一个处于可运行状态的线程,一旦获取了CPU的使用权,就可进入正在运行状态;
●阻塞(Blocked):当线程遇到干扰时,会进入阻塞状态,进程处于暂停状态;
●终止(Dead)
2.线程的生命周期
线程的生命周期是线程从创建到运行一直到运行结束的过程;也是线程的五个状态的转换过程;可以通过调用Thread类的一些方法,来影响线程的状态。
●I/O请求需要耗费一定的时间,此时就可以让线程进入阻塞状态(暂时暂停),等待I/O请求完成,再继续进行执行;
●stop()方法现在不推荐用了,JDK中已标注为版本过期;
1.sleep()方法
●sleep()方法使线程由正在运行状态转换到阻塞状态;线程暂时放弃CPU使用权;
●sleep()方法是Thread类中的方法;
●public static void sleep(long millis):静态方法,可直接用Thread类名字去调用;millis:毫秒,1000ms == 1s;
●sleep()方法:应用场景可能有(1)需要计时的情况,每隔一定时间显示一下时间等;(2)对于一些需要定期刷新数据而不是一直刷新数据的情况;
●需要注意一个线程到底能不能执行,或者说一个线程到底能不能“暂停”,不仅和sleep()方法有关,还与CPU是否可用有关;当一个线程sleep()方法执行完毕后,线程只是进入了可运行状态,该线程必须获得CPU的使用权才能真正开始执行。(所以,sleep()方法应用在严格的时钟计算时,可能会存在误差);
……………………………………………………
示例程序:
注:sleep()方法需要捕获异常;
sleep()方法的具体应用场景和应用技巧需要在实际开发中慢慢积累 ;
2.join方法
●join()方法使线程由正在运行状态转换到阻塞状态;线程暂时放弃CPU使用权;
●join()方法也是Thread类的方法;
●public final void join():是个final方法,字面理解join是加入、抢先进入的意思;
●一个线程调用了join()方法,那么这个线程获得了优先执行权,其他线程需要等待该线程执行完毕后才能执行;join()方法是一种抢占资源的方式;
……………………………………………………
示例程序:join(),无参形式,调用join()方法线程执行结束后,其他线程才会执行。
注:join()方法也需要捕获异常;
join()方法的具体应用场景和应用技巧需要在实际开发中慢慢积累;
输出结果:可以发现,因为mt线程调用了join()方法,所以mt线程获得了优先执行权;
若是,mt线程没有调用join方法:
输出结果:mt线程没有调用join()方法时,mt线程和主线程随机获取CPU使用权。
……………………………………………………
示例程序:join(long millis):调用join(long millis)方法的线程,其优先执行权只能维持一定时间,一定时间过后丧失优先权,和其他线程一样随机获取执行权
输出结果:输出结果截取部分,由执行结果可以发现join(long millis)方法作用。
3.线程的优先级
●Java为线程提供了10个优先级,优先级可以使用整数1-10表示,超过这个范围会抛出异常;数字越大,表示优先级越高;
●main()方法的线程(主线程)默认优先级是5;
●除了用数字表示优先级,还可以用优先级常量来表示优先级:
MAX_PRIORITY:线程的最高优先级10;
MIN_PRIORITY:线程的最低优先级1;
NORM_PRIORITY:线程的默认优先级5;
●Thread类提供了获取线程优先级的方法、和设置线程优先级的方法:
●线程的优先级只是提供了一个优先级别,优先级高的线程不一定非得先执行,是否先执行与CPU状态,系统状态等很多因素有关;即其执行还是有一定的随机性的;
……………………………………………………
示例程序:主要就是setPriority()方法、和getPriority()方法;
输出结果:可以发现,并不是高优先级的线程先执行完后低优先级的线程才执行,其任然存在一定的随机性;
换句话说,优先级高的程序并不一定会比优先级低的线程先运行;
线程同步
由前面几篇文章可知: 线程什么时候运行是不确定的 ,即:
●各个线程是通过竞争CPU的时间而获得运行机会的;
●各线程什么时候获得CPU时间,占用多久,是不可预测的;
●一个正在运行的线程在什么地方被暂停是不确定的;
●可能一个线程中一个方法(或一个代码块)刚执行到一半,这个线程就丧失了CPU的使用权,从而导致很多问题;
上面几条都透露着几个字:不确定。这个问题,在实际开发中也会带来不可预测的结果,问题;
……………………………………………………
如下面问题:银行账户的存款和取款。
(1)首先,存款和取款各是不同的线程在执行;
(2)存款操作和取款操作都是分很多步骤执行;
(3)不同的线程之间切换是随机的,即可能取款线程执行一半就停了,转去执行存款线程,而存款线程执行一般也可能暂停,转去执行取款线程;这种现象就会导致,最终的账户金额存在错误计算;
如下面代码:看个大概即可,程序很简单,主要是传达一种问题而已。
(1)Bank类,主要包括,存款方法和取款方法;(这儿用slee()方法,防止输出结果输出太快,看不清;而且实际中,一个方法的执行也需要一定的时间的);
(2)取款类,做成了一个线程类(因为取款操作是一个单独的线程)
(3)存款类,也做成了一个线程类
(4)测试类
解决办法:
●为了保证在存款或取款的时候,不允许其他线程对账户余额进行操作;需要将Bank对象进行锁定,即某个线程在操作Bank对象时,其他线程都不允许进行操作;
●使用关键字 synchronized(同步关键字)实现;该关键字,可以确保共享对象,在同一个时刻只能被一个线程访问,这种机制成为线程同步,或线程互斥。
● synchroniced具体的使用技巧,需要在以后的实际开发中逐步加深理解,逐步总结其应用场景,惯用应用技巧等。
synchroniced:
如上面Bank类改造如下:分别在存款方法和取款方法处,使用了synchroniced关键字。
如: saveAccount()方法添加synchronized后表示:这个方法中的所有代码在执行完毕之前,其他线程是不能把他打断的 。
线程间通信
可以设想这样一种情况:取款时,账务余额不够了;顺理成章的解决方案是:等待存入足够的钱以后再进行取款操作。(这种业务场景很多,不要仅仅局限于银行存取款喽)
这儿涉及线程间通信。
如下示例:
业务情况:
Queue类:该类相当于一个容器,其有一个变量n;
Producer类:充当生产者线程,该类负责对变量n的值进行加1运算;
Consumer类:充当消费者线程,该类负责对变量n的值进行减1运算;
或者认为Producer类负责数据的生产,Consumer类负责数据的取用;
业务要求:生产一个消费一个。
……………………………………………………
1.首先,是没有添加线程间通信的情况:
Queue类:
Producer类,生产类:
Consumer类,消费类:
Test类,测试类:
执行结果(只截取了前面几行):通过结果可以发现,第三行结果应该是生产1,即生产0,消费0之后,按照业务逻辑来说应该执行生产1,消费1…;但显然,第二行执行完后,生产和消费两个线程(在没有逻辑控制的)下一步该执行哪个是完全随机的,这种执行流程是不符合业务要求的。因此,有必要使得两个线程间能够彼此通信,从而使的线程间的执行顺序不再充满随机性,而是按照一定的逻辑执行;由此也是,线程间有点相互参照和依赖的味道。
2.其次,线程间通信:
●wait()方法:中断方法的执行,使线程等待;如果只有wait()方法,可能会出现:生产线程等待消费线程执行,消费线程等待生产线程执行,这样互相等待,谁都不会执行,就会处于死锁的状态;
●notify()方法:唤醒处于等待的某一个线程,使其结束等待;
●notifyAll()方法:唤醒所有处于等待的线程,使它们结束等待;这两个方法和wait()方法对应使用;
sleep() 和 wait() 的这 5 个区别,你知道几个?
(1)只使用wait()方法,而没有使用notify()和notifyAll()的情况:
Queue类:该类添加了flag变量,并在方法中添加了wait()方法和控制逻辑,但没有使用notify()和notifyAll();
执行结果:下面是所有的输出结果,发现,其实现了生产一个消费一个的逻辑;但当生产7后,并没有继续出现消费7,这就是出现了死锁的情况。
解决办法:添加notifyAll();时刻不忘唤醒线程。( 注:至于为什么会出现死锁,暂时还不明白 )
执行结果:只截取了部分,发现,程序的死锁情况解决了。
注: **(1) 具体线 程间通信,还需要通过实际业务来加深;这些可以应用哪些场景,应用时有哪些技巧等等都需要慢慢总结;
(2)线程间通信的场景也是千变万化,具体为了实现线程间通信,如何涉及逻辑代码,如何保证安全和效率的情况下的编写技巧和编写习惯做法,都需要慢慢积累;
Callable:创建线程的第三种方式
Callable接口是属于Executor框架(并发编程的一个框架)中的功能类,Callable接口和Runnable接口的功能相似,但功能比Runnable接口更强大。
(1)Callable接口可以在任务结束时提供一个返回值,Runnable接口没有这个功能;
(2)Callable接口的call()方法可以抛出异常,Runnable接口的run()方法不能抛出异常;
(3) Callable接口的深化,可等到接触并发编程的时候再了解 ;
Callable接口的示例程序如下:
创建Callable接口的实现类ThreadCallable,并实现call()方法;
实际调用:
输出结果: