java多线程-概念和作用
一:线程概念
线程是程序运行的基本执行单元。当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,
会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被称为主线程)来作为这个程序运行的入口点。
因此,在操作系统中运行的任何程序都至少有一个主线程
一个进程至少包含一个线程,如果一个进程包含2个以上,表示该进程是多线程操作,
那就存在资源共享的问题,多线程争夺资源的问题,
所以才有了锁机制的存在,防止多个线程打架
二:多线程的作用
1.为什么会有多线程:
1)由于早起的操作系统dos里,一个进程只有一个线程,随着机器的发展,发现一个进程如果存在多个线程来处理,这将大大利用期cup,
让cup充分利用,也可以让一个进程的程序让多个线程来跑,这将大大加快代码的执行时间。
2)实际上多线程的工作方式:表面上一个进程包含多个线程,代码由多个线程同时进行,但实际到了cup那里,
cpu的调度不会出现多个同时调度,
实际到了cup还是单个调度,只是中间有个时间间隔,这个时间间隔来调用不同的线程,eg:10ms执行这个线程,
然后执行别的线程,最后回来又重新执行现在的线程,通过时间差来保证多线程的工作,由于时间间隔非常短
,所以我们感觉多线程是并发进行的,这才有了多线程跑期来远比单线程快很多的道理。
3)多线程带来的困扰:上面都说了很多好处,那他有没有坏处呢,答案是肯定的,有坏处,
第一个问题:多线程共享一个进程的资源(主要还是内存的资源),资源的分配怎么保证大家都能共享,
第二个问题:如果程序里存在读写并存的情况,很有可能我的写和读也是并行的,这样就会出现数据的误差,
所以想了个办法,这样的情况,我把代码加个锁,只有保证我数据写完了,我才把锁交出来,
一个时间段里,我只有一把锁来维持,用完了再交给别人,防止数据的读和写被同时修改的可能。
4)多线程应该注意的地方:只有你充分理解了多线程在jvm里的工作方式,你才可以大胆的使用它的好处,否则,还是谨慎一点,别丢了西瓜捡了芝麻。
1、 线程概述
几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。2、 线程和进程
几乎所有的操作系统都有进程的概念,所有运行中的任务通常对应一条进程。当一个程序进入内存运行,就是一个进程了。进程是处于运行中的程序,具有一定的独立能力,进程是系统进行资源分配和调度的一个独立单位。
进程特征:
A、独立性:进程是系统中独立存在的实体,可以拥有自己独立的资源,每个进程都拥有自己的私有地址地址。在没有经过进程本身允许的情况下,一个用户进程不可以访问其他进程地址空间。
B、 动态性:进程和程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在程序中加入了时间概念,进程具有自己的生命周期和各种不同的状态,这些概念是程序不具备的。
C、 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程也被称为轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立、并发执行流。当进程被初始化后,主线程就被创建。对于绝大多数应用程序来说,通常仅要一个主线程,但我们也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程,每条线程也互相独立的。
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆、栈、程序计数器、局部变量,但不能拥有系统资源,它与父进程的其他线程共享该进程所有的全部资源。因为多个线程共享父进程的全部资源。
线程可以完成一定的任务,可与其他线程共享父进程中的变量和部分环境,相互之间协作共同完成进程所要完成的任务。
线程是独立运行的,它并不知道进程中是否还有其他进程存在。线程的执行是抢占方式的,也就是说,当前运行的线程在任何时候都可以被挂起,以便其他线程运行。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程可以并发执行。
综述:一个程序运行后至少有一个进程,一个进程可以包含多个线程。至少包含一个线程。
3、 并发和并行
并发性(concurrency)和并行性(parallel)是两个概念;
并行指在同一时刻,有多条指令(线程)在多个处理器上同时执行;
并发指在同一时刻只能有一个指令(线程)执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果。
4、 多线程的优势
线程划分尺度小于进程,使得多线程划分的并发性高。进程在执行时有自己独立的单元,多个线程共享内存,从而提高了运行效率。
线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性:多个线程将共享同一个进程的虚拟空间。线程共性的环境包括:进程代码段、进程共有数据等。线程很容易就利用共性的数据进行通信。
当操作系统创建一个进程时,必须给该进程分别独立的内存空间,并分配大量相关的资源;但创建一个线程则简单得多,因此多线程来实现并发要比多进程实现并发的性能高得多。
多线程优点:
A、进程之间不能共享内存,但线程之间共享内存非常容易。
B、 系统创建进程需要为该进程重新分配系统资源,但创建线程则代价要小得多,因此使用线程来实现多任务并发比多进程的效率高。
C、 Java语言内置多线程功能支持,而不是单纯的作为底层操作系统的调度方式,从而简化Java的多线程编程。
Java Thread 多线程 操作线程
创建、启动线程
线程的实现方式
线程的生命周期
线程的状态
控制线程
5、线程的创建和启动
A、继承Thread类或实现Runnable接口,重写或实现run方法,run方法代表线程要完成的任务
B、创建Thread子类或是Runnable的实现类,即创建的线程对象;不同的是接口实现线程,需要将接口的实现类作为参数传递给Thread类的构造参数
C、用线程对象的start方法启动线程
6、继承Thread和实现Runnable接口创建线程的区别
采用Runnable接口实现线程:
优势:
A、线程类只是实现了Runnable接口,还可以继承其他的类
B、在这种方式下,可以多个线程共享同一个目标对象,所以很合适多个线程来处理同一份资源的情况,
从而可以将CPU、代码和数据分开,形成清晰的模型,较好的面相对象思想。
劣势:编程稍微复杂,如果需要访问当前线程需要用Thread.currentThread方法来获取
采用继承Thread类的方式实现线程:
优势:编写简单,如果要获得当前线程直接this即可
劣势:线程类继承了Thread,不能在继承其他类
相对而言,用Runnable的方式更好,具体可以根据当前需要而定;
7、线程生命周期
线程被创建启动后,不并不是启动后就进入了执行状态,也不是一直处于的执行状态。
线程的生命周期分为创建(new)、就绪(Runnable)、运行(running)、阻塞(Blocked)、死亡(Dead)五种状态。
线程启动后不会一直霸占CPU资源,所以CPU需要在多条线程中切换执行,线程就会在多次的运行和阻塞中切换。
8、新建(new)和就绪(Runnable)状态
当new一个线程后,该线程处于新建状态,此时它和Java对象一样,仅仅由Java虚拟机为其分配内存空间,并初始化成员变量。
此时线程对象没有表现出任何的动态特征,程序也不会执行线程的执行体。
注意:run方法是线程的执行体,不能由我们手动调用。我们可以用start方法启动线程,系统会把run方法当成线程的执行体来运行,
如果直接调用线程对象run方法,则run方法立即会被运行。而且在run方法返回之前其他线程无法并行执行,
也就是说系统会把当前线程类当成一个普通的Java对象,而run方法也是一个普通的方法,而不是线程的执行体。
9、运行(running)和阻塞(Blocked)状态
如果处于就绪状态的线程就获得了CPU,开始执行run方法的线程执行体,则该线程处于运行状态。
单CPU的机器,任何时刻只有一条线程处于运行状态。当然,在多CPU机器上将会有多线程并行(parallel)执行,
当线程大于CPU数量时,依然会在同一个CPU上切换执行。
线程运行机制:一个线程运行后,它不可能一直处于运行状态(除非它执行的时间很短,瞬间执行完成),线程在运行过程中需要中断,
目的是让其他的线程有运行机会,线程的调度取决于底层的策略。对应抢占式的系统而言,系统会给每个可执行的线程一个小时间段来处理任务,
当时间段到达系统就会剥夺该线程的资源,让其他的线程有运行的机会。在选择下一个线程时,系统会考虑线程优先级。
以下情况会出现线程阻塞状态:
A、线程调用sleep方法,主动放弃占用的处理器资源
B、线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞
C、线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
D、线程等待某个通知(notify)
E、程序调用了suspend方法将该线程挂起。不过这个方法容易导致死锁,尽量不免使用该方法
当线程被阻塞后,其他线程将有机会执行。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态不是运行状态。
也就是被阻塞线程在阻塞解除后,必须重新等待线程调度器再次调用它。
针对上面线程阻塞的情况,发生以下特定的情况可以解除阻塞,让进程进入就绪状态:
A、调用sleep方法的经过了指定的休眠时间
B、线程调用的阻塞IO已经返回,阻塞方法执行完毕
C、线程成功获得了试图同步的监视器
D、线程正在等待某个通知,其他线程发出了通知
E、处于挂起状态的线程调用了resume恢复方法
线程从阻塞状态只能进入就绪状态,无法进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统调度所致的。
当就绪状态的线程获得资源时,该线程进入运行状态;当运行状态的线程事情处理器资源时就进入了就绪状态。
但对调用了yield的方法就例外,此方法可以让运行状态转入就绪状态。
10、线程死亡(Dead)状态
线程会在以下方式进入死亡状态:
A、run方法执行完成,线程正常结束
B、线程抛出未捕获的异常或Error
C、直接调用该线程的stop方法来结束线程—该方法易导致死锁,注意使用
注意:当主线程结束的时候,其他线程不受任何影响。一旦子线程启动后,会拥有和主线程相同的地位,不受主线程影响。
isAlive方法可以测试当前线程是否死亡,当线程处于就绪、运行、阻塞状态,该方法返回true,如果线程处于新建或死亡状态就会返回false。
不要试图对死亡的线程调用start方法,来启动它。死亡线程不可能再次运行。
11、控制线程
Java线程提供了很多工具方法,这些方法都很好的控制线程
A、join线程
让一个线程等待另一个线程完成的方法。当某个程序执行流中调用其他线程的join方法时,调用线程将会被阻塞,直到被join方法的join线程执行完成为止。
join方法通常有使用线程的程序调用,将大问题划分成许多小问题。每个小问题分配一个线程。当所有的小问题得到处理后,再调用主线程进一步操作。
join有三种重载模式:
一、join等待被join的线程执行完成
二、join(long millis)等待被join的线程时间最长为millis毫秒,如果在millis毫秒外,被join的线程还没有执行完则不再等待
三、join(long millis, int nanos)被join的线程等待时间长为millis毫秒加上nanos微秒
通常我们很少用第三种join,原因有二:程序对时间的精度无需精确到千分之一毫秒
计算机硬件、操作系统也无法做到精确到千分之一毫秒
B、后台线程
有一种线程,在后台运行,它的任务是为其他线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,有被称为“守护线程”或“精灵线程”。
JVM的垃圾回收器线程就是后台进程。
后台进程有个特征是:如果前台的进程都死亡,那么后台进程也死亡。(它为前台进程服务)
用Thread的setDaemon (true)方法可以指定当前线程为后台线程。
注意:前台线程执行完成死亡后,JVM会通知后台线程,后台线程就会死亡。但它得到通知到后台线程作成响应,需要一段时间,
而且要将某个线程设置为后台线程,必需要在该线程启动前设置,也就是说设置setDaemon必需在start方法前面调用。
否则会出现java.lang.IllegalThreadStateException异常
C、线程休眠sleep
如果需要当前线程暂停一段时间,并进入阻塞状态就需要用sleep,sleep有2中重载方式:
sleep(long millis)让当前线程暂停millis毫秒后,并进入阻塞状态,该方法受系统计时器和线程调度器的影响
sleep(long millis, int nanos)让当前正在执行的线程暂停millis毫秒+nanos微秒,并进入阻塞
当调用sleep方法进入阻塞状态后,在sleep时间段内,该线程不会获得执行机会,即使没有其他可运行的线程,处于sleep的线程不会执行。
D、线程让步yield
yield和sleep有点类似,它也可以让当前执行的线程暂停,但它不会阻塞线程,只是将该线程转入到就绪状态。
yield只是让当前线程暂停下,让系统线程调度器重新调度下。
当yield的线程后,当前线程暂停。系统线程调度器会让优先级相同或是更高的线程运行。
sleep和yield的区别
(1)、sleep方法暂停当前线程后,会给其他线程执行集合,不会理会线程的优先级。但yield则会给优先级相同或高优先级的线程执行机会
(2)、sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入到就绪状态;而yield则不会将线程转入到阻塞状态,它只是强制当前线程进入就绪状态。
因此完全有可能调用yield方法暂停之后,立即再次获得处理器资源继续运行。
(3)、sleep声明抛出了InterruptedException异常,所以调用sleep方法时,要么捕获异常,要么抛出异常。而yield没有申明抛出任何异常
E、改变线程优先级
每个线程都有优先级,优先级决定线程的运行机会的多少。
每个线程默认和它创建的父类的优先级相同,main方法的优先级是普通优先级,那在main方法中创建的子线程都是普通优先级。
getPriority(int newPriority)/setPriority(int)
设置优先级有以下级别:
MAX_PRIORITY 值是10
MIN_PRIORITY 值是1
NORM_PRIORITY 值是5
范围是1-10;
举例说明:两种创建的方式:
- package com.thread;
- public class TestThread1 {
- /**
- * 当直接调用 r.run()方法时,
- * 不加上:Thread t = new Thread(r);
- t.start();
- 就相当于是直接调用一个方法,两个线程直接没有交替执行。
- *
- * ========================================================================
- *
- *
- * 而 加上 : Thread t = new Thread(r);
- t.start(); 两个线程就会交替执行。
- *
- *
- * @param args
- */
- public static void main(String[] args) {
- Runner1 r = new Runner1();
- // r.run();
- // 下面两行是为了演示:直接调用run方法,不New 线程。
- <span style="color:#cc0000;">Thread t = new Thread(r);
- t.start();</span>
- for(int i=0; i<100; i++) {
- System.out.println("Main Thread:------" + i);
- }
- }
- }
- <span style="color:#ff0000;">class Runner1 implements Runnable {</span>
- public void run() {
- for(int i=0; i<100; i++) {
- System.out.println("Runner1 :" + i);
- }
- }
- }
第二种:
- package com.thread;
- public class TestThread2 {
- /**
- * @param args
- */
- public static void main(String[] args) {
- Runner r = new Runner();
- r.start();
- for(int i=0; i<100; i++) {
- System.out.println("Main Thread:------" + i);
- }
- }
- }
- <span style="color:#cc0000;">class Runner extends Thread {</span>
- public void run() {
- for(int i=0; i<100; i++) {
- System.out.println("Runner1 :" + i);
- }
- }
- }