ch01-多线程
一、概念
1、程序
程序是为完成特定任务、用某种编程语言编写的一组指令的集合;
我们可以理解为程序是一段静态的代码,如安装在硬盘上程序,如QQ、迅雷等
2、进程
正在运行的程序就是一个进程,如运行中的QQ、迅雷等;
进程是程序的一次动态执行过程,经历从代码加载、代码执行到执行完毕的一个完整过程(生命周期);
多进程操作系统能同时运行多个进程(程序),由于CPU具备分时机制,所以每个进程都能循环获得自己的CPU时间片;
每个进程在内存中有独占一个方法区和堆空间,被多个线程共享;
3、线程
进程可以进一步进行细化为线程,它是程序中的一条执行线路(路径);
每个Java程序都至少有三个线程——main主线程、gc()垃圾回收机制的运行线程和异常处理线程;
当一个Java程序启动时,JVM会创建主线程,并且在该线程中调用程序的main()方法。
1)单线程
之前接触的都是单线程程序,单线程的特点是,被调用的方法执行完毕后当前方法才可能完成,前一个方法完成后才进行下一个方法。这是一种顺序调用。
2)多线程
当程序同时用多个线程调用不同方法时,并不是像单线程一样的顺序调用,而是启动独立的线程之后立刻返回,各线程运行从run()方法开始,当run()结束后线程运行结束。
注意:真正的多线程指的是多CPU(即多核),如果只有单个CPU时,多线程是模拟实现的,在同一个时间点上,CPU只做一件事情。比如人虽然可以一边吃饭一边看电视,但...
线程的优点
- 省时(提高计算机系统CPU的利用率)
- 并行
3)并发与并行
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力;
- 并行(parallel)是同一时间动手做(doing)多件事情的能力;
- 例子
- 厨师洗菜、切菜、炒菜、上桌,客人点了菜,厨师一个人在同一时间轮流交替的完成,这就叫并发
- 配菜人员准备食材、砧板工切菜、厨师炒菜、服务员上菜,专人专职,各施其职,互不干扰,这就叫并发
- 厨师及其老婆共同完成洗菜、切菜、炒菜、上桌,此时夫妻二人即有并发也有并行。夫妻二人如果没有默契,则很有可能产生资源竞争问题,如妻子使用了锅,则厨师只能待。
4、进程与线程的区别
(1)一个应用程序就是一个进程,而线程是一个进程内部的多个运行单位;
(2)多进程的内部数据和状态都是完全独立的,而多线程是共享一块内存空间和一组系统资源(在同一进程内),在程序内部可以相互调用;
(3)线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担小。
二、线程的创建
1、继承java.lang.Thread类
// 第一:创建一个类,继承Thread类
public class Runner extends Thread{
public void run(){
// ...
}
}
public class Test{
public static void main(String[] args){
// 第二:创建线程对象
// Runner t = new Runner() ;
Thread t=new Runner();
// 第三:启动线程
t.start();
}
}
- Thread.currentThread():返回当前线程对象
- Thread:setName(String name):为线程取名
- Thread:getName():返回线程名
课堂练习:在RGB三原色中,使用多线程实现,分别随机打印输出红、绿、蓝三个随机值。(取值范围:0-255)
2、实现接口java.lang.Runnable
// 第一:实现Runnable接口
public class Runner implements Runnable{
public void run(){
// ...
}
}
public class Test{
public static void main(String[] args){
// 第二:创建Runner对象
Runner r=new Runner();
// 第三:创建线程
Thread t=new Thread(r);
// 第四:启动线程
t.start();
}
}
注意:线程必须通过start()方法启动。如果直接调用run()方法,只是方法调用,不是启动线程。
两种实现对比,实现Runnable接口有以下好处
1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码、数据有效的分离,较好地体现了面向对象的设计思想;
2)可以避免由于Java的单继承特性带来的局限;
3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码(run方法体)来自同一个类的实例(实现Runnable接口的对象)时,即称它们共享相同代码。
4)Thread
类实际上是Runnable
接口的实现类
3、实现Callable
接口
JDK5.0新特性。
- 第一:实现Callable接口 + 重写call()方法;
- 第二:创建实现Callable接口的对象
- 第三:创建
FutureTask
对象 Future
接口的唯一实现类
- 同时也实现了Runnable接口
- 能够接收 Callable 类型的参数,用来处理有返回结果的情况
- get():获取值
- 第四:创建线程对象
- 第五:启动线程
// 第一:实现Callable接口+重写call()方法
// 指定泛型,否则默认为Object
public class MyCallable implements Callable<T> {
@Override
public T call() throws Exception {
// 此线程执行需要执行的操作声明在call方法中
}
}
public class Test{
public static void main(String[] args){
// 第二:创建实现Callable接口的对象
MyCallable myCallable = new MyCallable() ;
// 第三:创建FutureTask对象
FutureTask futureTask = new FutureTask(myCallable);
// 第四:创建线程对象
Thread thread = new Thread(futureTask);
// 第五:启动线程
thread.start() ;
// 第六:获取结果(主线程阻塞,同步等待 task 执行完毕的结果)
T data = thread.get() ;
}
}
Callable接口类似于Runnable接口,实现Callable接口的类和实现Runnable接口的类都是可被其它线程执行的任务。
二者区别如下:
1)Callable接口定义的方法是call(),而Runnable接口定义的方法是run();
2)call()方法可抛出异常,而run()方法是不能抛出异常(只能在方法内部try);
3)Callable的任务执行后可以返回值,运行Callable任务可拿到一个Future对象,而Runnable的任务是不能返回值的。
4)Runnable
接口实现方法不支持泛型,Callable
接口实现方式支持泛型;
5)Callable
接口实现方式返回值需要借助FutureTask
类获取返回值(get方法)
**Future**
接口
Future表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果;
通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。
4、线程池
好处:;降低资源消耗;提高响应速度;提高线程的可管理性。
JDK5.0新特性。
- 第一:实现Callable接口+重写call()方法
- 第二:借助执行调度服务ExecutorService,获取Future对象
- ExecutorService ser = Executors.newFixedThreadPool(线程数) ;
- Future result = ser.submit(实现Callable接口的对象) ;
- Future result = ser.submit(实现Runnable接口的对象) ;
- 第三:获取值:result.get()
- 第四:停止服务:ser.shutdownNow() ;
// 第一:实现Callable接口+重写call()方法
// 指定泛型,否则默认为Object
public class MyCallable implements Callable<T> {
@Override
public T call() throws Exception {
// 此线程执行需要执行的操作声明在call方法中
}
}
// 第一:也可以实现Runnable接口+重写run()方法
public class MyRunnable implements Runnable{
public void run(){
// ...
}
}
public class Test{
public static void main(String[] args){
// 第二:工具类Executors创建线程池并返回ExecutorService对象
ExecutorService executorService = Executors.newFixedThreadPool(10) ;
// 第三:创建Future对象
Future future = executorService.submit(new MyCallable());
// Future future = executorService.submit(new MyRunnable());
// 第四:获取值
future.get() ;
// 第五:停止服务:
executorService.shutdownNow() ;
}
}
三、线程控制方法
1、start():启动线程
线程对象.start()
注意:
- 启动一个新的线程,在新的线程中运行run方法的代码;
- 不能直接调用run方法;
- start方法只能调用一次;
- start方法只是让线程进行就绪状态,不一定马上运行线程体,需要CPU分配时间片;
2、run():线程体
run() :线程的执行内容
3、currentThread():获取当前正在运行的线程
static Thread currentThread()
返回对当前正在执行的线程对象的引用
4、getName():获取线程名称
getName()
5、setName(String):设置线程名称
setName(String)
6、getState():获取线程的状态
getState()
线程状态通过Enum表示,分别为:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
7、sleep():线程休眠
static void sleep(long ms)
// 当前线程休眠1秒,单位为毫秒
Thread.sleep(1000) ;
注意:
- 休眠N毫秒,让出CPU时间片给其它线程;
- 调用sleep方法会使当前线程从
RUNNING
状态进入Timed Waiting
状态(阻塞); - 睡眠结束后的线程并不一定立刻执行;
- 睡眠一段时间之后重新加入CPU资源竞争,睡眠的时候仍然握锁;
- 建议使用TimeUnit的sleep方法,有更好的可读性
8、yield():线程让步
static void yield()
注意:
- 当前线程放弃cpu控制权,回到Runnable就绪状态(给其它线程一个执行机会);
- 具体的实现依赖于操作系统的任务调度器
9、interrupt():中断线程
interrupt()
注意:
- 如果被中断线程正在sleep,wait,join会导致被中断的线程抛出InterruptedException,并清除
中断标记
; - 如果中断正在运行的线程,则会设置
中断标记
; - park的线程被中断,也会设置
中断标记
10、isInterrupted():判断这个线程是否被中断
boolean isInterrupted()
不会清除中断标记
11、interrupted():判断当前线程是否被中断
static boolean interrupted()
会清除中断标记
12、 stop():终止线程
stop()
已取消,不建议使用。推荐终止线程的办法
public class Runner extends Thread{
private boolean flag = false;
public void shutDown(){
flag=true;
}
public void run(){
while(!flag){
// your business logic
}
}
}
13、线程优先级控制
1)线程优先级:优先级越高,获取CPU的执行时间就越长
2)设置/获取优先级
- public final void setPriority(int newPriority)
- newPriority取值范围是1-10,值越大,优先级越高
- public final int getPriority()
3)优先级的三个常量
- public final static int MIN_PRIORITY = 1 ; //最低优先级
- public final static int NORM_PRIORITY = 5 ; //缺省优先级
- public final static int MAX_PRIORITY = 10 ; //最高优先级
注意:
- 高优先级并不意味着先执行,它只是提示任务调度器优先调度此线程,但任务调度器很有可能会忽略它。
- 只是原则上具有更高概率先抢占CPU资源执行。
- 如果CPU比较忙,则优先级高的线程会获得更多的时间片;如果CPU闲时,优先级几乎无作用。
14、join():停止当前线程
- join() :等待当前线程运行结束
- join(long n):等待当前线程运行结束,最多等待n毫秒
停止当前线程(不是所有线程),直到调用join方法的线程结束(无限期等待线程死亡)
如:在线程A中调用线程B的join()
方法,相当于让线程B直接插入线程A方法中运行(线程A阻塞),直至线程B结束,继续线程A。
15、isAlive():是否处于活动状态
isAlive()
判断线程是否存活,即还没有执行完毕
四、生命周期
1、新建状态(New)
当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
2、就绪状态(Runnable)
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3、运行状态(Running)
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4、阻塞状态(Blocked)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、死亡状态(Dead)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
就绪状态转换为运行状态:当此线程得到处理器资源;
运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。
运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。
此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。
举个通俗一点的例子来解释上面五种状态,比如上厕所:
你平时去商城上厕所,准备去上厕所就是新建状态(new),上厕所要排队,排队就是就绪状态(Runnable),有坑位了,轮到你了,拉屎就是运行状态(Running),你拉完屎发现没有手纸,要等待别人给你送纸过来,这个状态就是阻塞(Blocked),等你上完厕所出来,上厕所这件事情结束了就是死亡状态了。
注意:便秘也是阻塞状态,你便秘太久了,别人等不及了,把你赶走,这个就是挂起,还有一种情况,你便秘了,别人等不及了,跟你说你先出去酝酿一下,5分钟后再过来拉屎,这就是睡眠。
https://blog.csdn.net/xiaosheng900523/article/details/82964768
课堂练习:设计一个线程类从0循环到50并打印,创建该类的三个线程,测试线程的创建、启动、停止、休眠、join、同步等现象。对照状态图理解运行结果。
五、线程同步
线程安全问题产生原因:当一个线程在执行操作共享数据的多条代码过程中,其他线程也参与了运算,就有可能导致线程安全问题的产生。也就是说,当多个线程操作相同的资源时,就有可能发出线程安全问题。
解决方案:同步机制(synchronized)
1、方法一:同步块
synchronized(object) {
// 代码段(临界区) ;
}
注:
- object可以是任意的一个对象,但必须是唯一的;
- 何意类型的对象都有一个标志位,该标志位具有0、1两种状态,其开始状态为1,当执行synchronized(object)语句后,object对象的标志位变为0状态,直到执行完整个synchronized语句中的代码块后又变回到1状态。
2、方法二:同步方法
1)成员方法
[修饰符] synchronized 数据类型 方法名() {
// 代码段 ;
}
等同于
[修饰符] 数据类型 方法名([参数列表]) {
synchronized(this) {
// 代码段 ;
}
}
2)静态方法
[修饰符] synchronized static 数据类型 方法名() {
// 代码段 ;
}
等同于
[修饰符] 数据类型 static 方法名([参数列表]) {
synchronized(当前类名.class) {
// 代码段 ;
}
}
注意:
- synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁;
- 以上成员方法和静态方法中的synchronized锁的不是方法,而是对象,分别为:
- 成员方法中的synchronized锁住的是:当前对象(this)
- 静态方法中的synchronized锁住的是:类对象(当前类名.class)
public void run(){
// 同步块
synchronized(this){
count++;
try{
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+":You are No."+count+" visitor!");
}
System.out.println("OK");
}
public void print(){
System.out.println("OK");
}
// 同步方法
synchronized public void run(){
...
}
3、方法三:lock(JDK5.0新增)
JDK5.0新特性,同步锁由Lock
对象充当。ReentrantLock
类实现Lock接口。
java.util.concurrent.locks.Lock
接口:控制多个线程对共享数据资源进行访问的工具。锁提供了对共享数据资源的独占访问,每次只有一个线程对Lock对象加锁,线程开始访问共享数据资源之前需要先获取Lock对象。
同样,使用方式一创建线程需要主要lock对象的静态问题。
ReentrantLock reentrantLock = new ReentrantLock();
try {
//加锁
reentrantLock.lock();
//需要同步的代码块
}finally {
//释放锁
reentrantLock.unlock();
}
两大类解决线程安全问题方法的不同之处(synchronized和lock(ReentrantLock))
synchronized
机制属于在执行完同步代码后,会自动释放锁(隐式锁);Lock
机制需要手动的启动和释放锁(显示锁),且只有代码块锁,没有方法锁;
六、线程通信
1、概念
2、线程通信常用方法
1)wait()
线程进入阻塞状态,释放锁(和sleep不同)
2)notify()
唤醒正在等待锁的线程,进入就绪状态(有优先级按优先级,没有随机唤醒一个);
3)notifyAll()
唤醒所有正在等待锁的线程;
说明
- 这三种方法只能出现在同步代码块或同步方法里,且不能用在lock里,否则会报错
java.lang.IllegalMonitorStateException: current thread not owner
; - 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,默认情况下是this或者类.class(当前类的对象)
sleep()和wait()的异同
- 同
- 一旦使用,均可使当前线程进入阻塞状态;
- 异
- 声明位置不同:sleep()声明在Thread类中,wait()声明在Object类中;
- 调用要求不同:sleep()可以使用在各种需要的地方,而wait()只能用在同步代码块或同步方法里;
- sleep()使用不释放锁,而wait()使用后会释放锁。
- [zing]()今天 08:22
- 引用原文:3、生产者与消费者
- 虚假唤醒问题
- 假设有多个女孩子线程吃完水果了,都进入了等待状态,此时它们已经跳过了if判断,如果有一个男孩子生产了一个水果并马上执行了nitifyAll,则会唤醒所有的线程,包括这些女孩子线程。此时,其中某一个女孩子获得锁且获得CPU时间片,则能正常消费男孩子生产的那一个水果,接着另一女孩子获得锁获得CPU时间片,则会继续往执行,不会再判断If条件了,LinkedList集合会空,没有水果了,调用removeLast就会有异常发生!
- 解决办法都把if改为while,不能让线程跳过判断条件,当线程往下执行时,先回while条件判断,如果满足条件则继续等待,等待下次通知