疯狂Java讲义:第16章:多线程复习(一)

来源:互联网 发布:魔侠传 网络异常 编辑:程序博客网 时间:2024/05/16 00:36

16.1线程概述

16.1.1 进程和线程

单线程:只有一个顺序执行流,即程序从MAIN开始执行,自上而下执行每行代码,遇到阻塞时会停滞。这样的功能很有限,下面的例子指出了其中的弊端。

例子:开发一个服务器程序,如果服务器程序要向不同的用户提供服务,各个用户间不该互相干扰,某一个用户出现问题不该影响其他用户。

多线程:包含多个顺序执行流,各个顺序执行流之间不互相干扰。

每个运行中的程序被称为进程,程序与进程是不同的,程序仅仅是一个静态的代码块,进入内存运行后,变成进程。进程是运行中的程序,有一定的独立功能,是系统进行资源分配和调度的一个独立单位。

进程三个特征:
1.独立性:独立存在,可以拥有自己的独立资源,每个进程都有自己私有的地址空间,没经过进程本身允许的情况下,一个用户进程不能直接访问其他进程的地址。

2.动态性:程序是一个静态的指令集合,进程是一个正在系统中活动的指令集合,进程具有自己的生命周期和各种不同的状态。

3.并发性:多个进程可以在单个处理器上并发执行,多个进程间不会互相影响。

注意:并发性与并行性的区别:并行:同一时刻,多条指令在多个处理器上同时运行;并发:同一个时刻只能有一条指令执行,但多个进程指令被快速执行,宏观上有多个进程执行的效果。

多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程也被称作轻量级进程,线程是进程的执行单元,,就像进程在操作系统中的地位一样,线程在程序中是独立,并发的执行流。当进程被初始化后,主线程就被创建了。(比如运行一个有main函数的程序,程序运行后就会产生一个main相关的主线程),一般的程序可以只有一个main函数,这样就有只有一个主线程,但可以在主线程中创建多条顺序执行流,这些顺序执行流就是线程,并相互独立, 这样就是多线程。

线程是进程的组成部分,一个进程可以有多个线程,一个线程必须有一个父进程。线程可以有自己的堆栈,自己的程序计数器和自己的局部变量,但不拥有系统资源,他与父进程的其他线程共享父进程的全部资源,这是一把双刃剑,一方面共享资源编程更加方便,但也容易线程之间造成影响。

总结:一个程序运行后至少有一个进程,可以有多个进程(可以看电脑上的软件:资源管理器后,一个程序可以有多个进程),一个进程可以有多个线程,至少有一个线程,即主线程。

16.1.2多线程的优势

多个线程共享同一个父进程的资源,包括进程的内存空间,相关资源,也包括进程代码段,进程的公有数据等。
多线程编程的特点:
1.进程之间不能共享内存,但线程之间共享内存。并且可以共享同一进程的代码块和共有数据
2.系统创建进程时需要为进程分配系统资源。由于进程的独立性,资源之间不共享,会使得资源耗费极大,但建线程资源消耗则小的多,因为同一个进程中的线程共享资源。因此使用多线程来实现多任务并发比多进程要高。

多线程应用:java虚拟机在后台提供了一个超级线程来进行垃圾回收。

16.2线程的创建和启动

四种方式:继承Thread类创建线程类;实现Runnable()接口创建线程类;实现Callable()接口

16.2.1 继承Thread类创建线程类

通过继承Thread类来创建并启动多线程的步骤如下:
1.定义Thead类的子类,并重写改类的run()方法,该run()方法的方法体就代表了线程的需要完成的任务,run()方法称为线程执行体;
2,创建Thread子类的实例,即创建线程对象。
3.调用线程对象的start()方法来启动该线程。
通过看JDK源码可知,Thead类实现了Runnable()接口,这样就感觉能好理解第二种通过实现Runnable接口的方法了。可见Runnable接口是很重要的。
publicclass Thread implements Runnable {
}
需要注意的是使用继承Thread类的方法创建线程类时,多个线程之间无法共享线程类的实例变量

16.2.2 实现Runnable接口创建线程类

1.定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2.创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
对应的jdk源码:
 public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
3.调用线程对象的start()方法来启动该线程。
目前到这,我们发现这两种实现线程的方法有相同之处:都要重写run()方法,是因为继承Thread时,因为Thread本身实现了Runnable接口,与第二种实现Runnable接口很像,重写的都是Runnable接口中的方法。

需要注意的是与使用继承Thread类的方法创建线程类不同,多个线程之间可以共享线程类的实例变量

16.2.3使用Callable和Future创建线程

只做了解,未深入,Callable未实现Runnable接口,所以Callable不能直接作为Thread的target,Callable中提供了一个call()方法,来作为线程执行体,作用与run()相同,但功能更加强大。
两个区别:1,call可以有返回值。2,call可以抛出异常



16.2.4 创建线程的三种方式对比


因为Runnable与Callable差不多,两者放在一起讨论。
Thraed的优点:编写简单,如果需要访问线程无须使用用Thread.CurrentThread方法,直接用this即可。
缺点,线程只能继承一个父类,继承Thread实现后,无法继承其他父类。
Runnable的优点:实现接口后,可以实现其他接口,并且多个线程可以共享一个target对象,适合多个相同线程处理一份资源的情况。
缺点:编程略复杂。



16.3线程的生命周期

线程的生命周期中,它要经过新建(New),就绪(Runnable),运行(Running),阻塞(Blocked)和死亡(Dead)5种状态。尤其是线程启动以后,不能一直“霸占”CPU独自运行,所以CPU需要在多条线程间切换,线程状态多次在运行,就绪之间切换。

16.3.1新建和就绪状态

程序使用New关键字新建进程后,线程处于新建状态,此时它仅仅由Java虚拟机分配内存,并初始化其成员变量的值,此时线程没有动态特征,也不会执行线程执行体。
调用Start()后,线程处于就绪状态,Java虚拟机会为其创建方法调用栈,但并未开始运行,仅仅表示线程可以运行了,但什么时候运行取决于JVM中线程调度器的调度。
注意:启动线程使用STRAT()方法,而不是run()方法!永远不要调用线程对象的run()方法!调用start()方法来启动线程,系统会把该run()方法当做线程执行体处理。如果直接调用线程的run()方法,run方法会立即执行,此时run方法会被认为是主线程中的方法。并且只能对处于新建状态的线程调用start()方法,否则引发IllegalThreadStateException.
因为start()后线程仅仅是就绪状态,不一定会立刻执行,如果希望立刻执行,使用Thread.sleep(时间参数)使当前运行的线程休眠,那么线程调度器会自动执行另一个就绪状态的线程。

16.3.2 运行和阻塞状态

如果处于就绪状态的线程获得CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。至于是否会有线程并行,要看线程数与cpu的数量,如果线程数大于CPU数还是会有并发的情况,小于会并行。
线程中采用抢占式策略来让线程处理任务,会给每个线程一小段时间,执行完那就死了,没执行完就给其他线程让步,而选择下一个线程时会根据优先级选择。
要注意线程只能从就绪进入运行状态,其他的情况都不行。
引发线程从运行到阻塞的几种情况:
1.线程调用sleep()方法。如果过了指定时间,线程就会从阻塞进入就绪状态。
2.线程调用了一个阻塞式IO方法,在该方法返回之前,线程阻塞。
3.线程试图获得一个同步监视器,但该同步监视器正被其他线程持有。(以后再总结)。
4.线程在等待某个通知(notify).
5.程序调用了线程的suspend()方法将该线程挂起。但这个容易导致死锁,应该尽量避免使用。
线程进入阻塞后,其他线程就能执行。
阻塞的进程进入就绪状态的情况:
1.调用sleep()到了指定的时间。
2.线程调用的阻塞式IO方法已经返回。
3.线程成功获得了试图取得的同步监视器。
4.线程正在等待某个通知时,其他线程发出了一个通知。
5.处于挂起状态的线程被调用了resume()恢复方法。
就绪与运行状态之间的相互转换:这两种状态之间的转换不受程序控制,受线程调度器控制,处于就绪状态的线程获得处理器资源就进入运行状态,失去处理器资源就就如就绪状态。但可以是用yield()方法让运行状态线程转入就绪状态。

16.3.3 线程死亡


线程会以如下三种方式结束:
1.run或call方法执行完成,正常结束死亡
2.线程抛出一个未捕获的异常
3.直接使用stop()方法,容易导致死锁。

注意:当主进程结束时,其他线程不受任何影响,并不会随之结束,一旦子线程启动后,就和主线程有相同的地位,不受主线程影响。

关于start()函数要注意:1.不要对新建的线程重复使用 2.不要对死亡的线程使用。会保存线程状态非法。

检测线程是否死亡可以使用isAlive()方法。处于就绪,运行,阻塞返回true,新建,死亡返回false。



16.4 控制线程

16.4.1 join线程

Thread提供了一个让线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
join()有三种重载形式:(控制等待时间)
1.join()等待被join的线程执行完成。
2.join(long mills):等待被join的线程的时间最长为millis毫秒。若没执行结束,则不再等待。

16.4.2 后台线程

在后台运行的线程,为其他线程提供服务,JVM的垃圾回收线程就是典型的后台线程。如果所有的前台线程都死亡,后台线程会自动死亡。若果程序中有一个前台线程,一个后台线程,后台线程时间比前台线程的长,那么有可能因为前台进程已经结束,那样后台进程也结束,后台进程可能不能完全执行。
调用Thread的setDaemon(true)设置线程为后台线程。用isDaemon判断线程是否为后台线程。

如果想把某个线程设置成后台线程,thread.setDaemon(true)必须在thread.start()之前。

16.4.3 线程睡眠:sleep

如果让当前执行的线程暂停段时间,由运行到阻塞,通过调用thread的静态方法类sleep();millis单位是毫秒。
当前进程进入sleep后,在其睡眠时间内不会有执行机会,即使系统中没有其他线程,sleep也不会执行,sleep用来暂停程序的执行。

16.4.4 线程让步:yield

yield()是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但不会阻塞线程,会让线程释放占用资源,进入就绪状态,等待线程调度器的重新调度,重新调度时需要根据线程优先级进行调度,优先调度与优点级比暂停线程高的线程,或者是同级的线程。
yield()方法与sleep()有相同之处:都是暂停当前线程,但有很大不同:
1.线程状态上:yield()由运行状态转到就绪状态,可能马上会被再次调用。sleep由运行状态转为阻塞状态,不会被再次调用,只有当超过sleep的时间后,线程由阻塞进入就绪状态才能被再次调用。
2.sleep()的构造函数中必须有参数指定时间。yield()无参数。

进一步深入sleep与yield的区别:
1.sleep方法暂停当前线程后,会给其他线程执行机会,不理会线程的优先级;但yield不同只会给优先级高于或更高的线程执行机会。有可能一个线程yield后,因为其与线程优先级不高,还会再次被调用。
2.sleep会将线程转入阻塞状态,直到经过阻塞时间转入就绪状态后,才能被继续调用;yield会将线程转入阻塞状态。
3.sleep方法比yield方法一致性好,不建议用yield执行并发性。


16.4.5 改变线程优先级

每个线程都有一定优先级,每个线程创建时默认与创建它的父线程优先级相同,默认时main线程具有普通优先级(即NORM_PRIORITY,5),由main创建的子线程也有普通优先级。如果在子线程创建之前,修改了父线程的优先级,那么子线程优先级也会随之改变。
Thread类有setPriority和getPriority来设置和返回指定线程优先级,优先级范围1~10.设置时除了使用1~10,也可以使用MAX_PRIORITY=10;MIN_PRIORITY=1;NORM_PRIORITY=5.设置。
优先级高的线程会获得更多的执行机会,但这不代表优先级低的不会执行,只是说执行的次数少。


16.5线程同步


多线程编程中线程之间的调度是受JVM线程调度器控制的,有一定的随机性,那就存在一些偶然的问题。如果多个线程对同一个变量进行操作,会使得结果有一定概率出现误差。这是不允许的,引入线程同步来使得线程变得安全。
经典问题——银行取钱问题。
思想:对多个线程操作的那个变量,加上一个同步监视器,使用同步监视器的方法就是同步代码块,语法如下:
synchronized(Object )
{
..同步代码块
}
其中的object就是同步监视器,代码的含义是:线程开始执行同步代码块之前,必须获得对同步监视器的锁定,任何时刻只能有一个线程可以获得对同步监视器的锁定,同步代码块执行完成后,线程会释放对该同步监视器的锁定。这就使得执行过程中只有一个线程可以同步监视器进行修改,其他线程不可以,保证了线程安全。
通常使用可能被并发访问的共享资源当做同步监视器。


阅读全文
0 0
原创粉丝点击