Java精选笔记_多线程(创建、生命周期及状态转换、调度、同步、通信)

时间:2022-04-30 05:03:09

线程概述

在应用程序中,不同的程序块是可以同时运行的,这种多个程序块同时运行的现象被称作并发执行
多线程可以使程序在同一时间内完成很多操作
多线程就是指一个应用程序中有多条并发执行的线索,每条线索都被称作一个线程

进程Process

在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”

线程Thread

一个程序至少有一个进程,一个进程至少有一个线程进程在执行过程中拥有独立的内存单元
在java中一个正在执行的程序。程序运行中至少有两个线程在运行,一个是主函数的主线程,另一个是垃圾回收的线程
让程序运行的更为顺畅,达到了多任务处理的目的。
优缺点
提升性能改善用户体验,对系统运行的其他进程/线程不友好。

线程的创建

继承Thread类创建多线程

继承Thread类.并重写Thread类中的run()方法,调用线程的start方法,启动新线程,线程启动后,系统会自动调用run()方法,如果子类重写了该方法便会执行子类中的方法

作用

1.启动新线程 start()方法
2.运行run方法。目的是将自定义的代码存储在run方法中,让线程运行
cpu每次只执行一个程序,只是在快速的不同线程间切换,表现了多线程的随机性

Thread类的方法

对象方法
start()-启动线程
setPriority(int)/getPriority()-设置/获取优先级
setDaemon(boolean) - 设置守护线程(后台线程)
interrupt() - 中断线程
join() - 等待该线程结束
静态方法
sleep(long) - 线程休眠
yield() - 让出CPU
currentThread() - 获得正在执行的线程
class demo extends Thread {
  public void run() {
  }
}
run方法用于存储线程要运行的代码。
demo demo=new demo();  创建对象就创建了一个线程。
run方法和 start方法
run方法  仅仅是对象调用方法,并没有运行线程
start方法   开启线程并且执行线程中的run方法

实现Runnable接口创建多线程

Thread类提供了另外一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法

步骤

1,定义类实现Runnable接口。
2,覆盖接口中的run方法(用于封装线程要运行的代码)。
3,通过Thread类创建线程对象;
4,将实现了Runnable接口的子类对象作为实际参数传递给Thread类中的构造函数。
    为什么要传递呢?因为要让线程对象明确要运行的run方法所属的对象。
5,调用Thread对象的start方法。开启线程,并运行Runnable接口子类中的run方法。


  Ticket t = new Ticket();
  直接创建Ticket对象,并不是创建线程对象。
  因为创建对象只能通过new Thread类,或者new Thread类的子类才可以。所以最终想要创建线程。既然没有了         Thread类的子类,就只能用Thread类。
  Thread t1 = new Thread(t); //创建线程。
  只要将t作为Thread类的构造函数的实际参数传入即可完成线程对象和t之间的关联
  为什么要将t传给Thread类的构造函数呢?其实就是为了明确线程要运行的代码run方法。

实现Callable<T>接口重写call方法

Future<T>
可以在线程执行技术之后得到一个结果

两种实现多线程方式的对比分析

继承Thread类:线程代码块存放在Thread子类的run方法中
实现Runnable,线程代码存放在接口的子类的run方法中,可以被多实现
继承方式有局限性。要被实现多线程的一个类 如果继承了父类 就不能再继承Thread类。
实现方式就改变了单继承的局限性。
适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效地分离,很好的体现出面向对象的编程思想
大多数的应用程序都会采用实现Runnable的方式实现多线程的创建

线程运行状态

新建:start()
运行:具备执行资格,同时具备执行权;
冻结:sleep(time),wait()—notify()唤醒; 线程释放了执行权,同时释放执行资格;
临时阻塞状态:线程具备cpu的执行资格,没有cpu的执行权;
消亡:stop() run方法结束

后台线程

只要有一个前台线程在运行,这个进程就不会结束。如果一个进程中只有后台线程在运行,这个进程就会结束。

新创建的线程默认都是前台线程,如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成一个后台线程。

线程的生命周期及状态转换

线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated),线程的不同状态表明了线程当前正在进行的活动。

1、新建状态(New)

创建一个线程对象后且在调用start方法之前,该线程对象就处于新建状态,此时它不能运行,和其它Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。

2、就绪状态(Runnable)

当线程对象调用了start()方法后,该线程就进入就绪状态(也称可运行状态)。处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件(具备了除CPU之外所有运行所需资源),能否获得CPU的使用权开始运行,还需要等待系统的调度。

3、运行状态(Running)

如果处于就绪状态的线程获得了CPU的使用权,开始执行run()方法中的线程执行体,则该线程处于运行状态。当一个线程启动后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就结束了),当使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其它线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。

4、阻塞/等待状态(Blocked)
一个正在执行的线程在某些特殊情况下,如执行耗时的输入/输出操作时,会放弃CPU的使用权,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
当线程试图获取某个对象的同步锁时,如果该锁被其它线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其它线程所持有的锁。

当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回。

当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程。

当线程调用了Thread的sleep(long millis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。

当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。

5、死亡状态(Terminated)
线程的run()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其它状态,线程的整个生命周期结束。

线程的方法

获取线程名称和对象
线程都有自己默认的名称:
获取线程名称的方法:Thread.currentThread().getName()
currentThread() 获取当前线程对象
getName()   获取线程名称 
设置线程名称 SetName(); 构造方法传参

public final String getName() 返回线程的名称
public final void setName() 设定线程名称
public final void join()throws InterruptedException
等待该线程终止
public final native boolean isAlive() 判断线程是否在活动,如果是返回true,否则返回false
public final void join(long millis)
throws InterruptedException 等待该线程终止,并指定等待的时间。其中millis的单位是毫秒
public final void setPriority(int newPriority)
修改线程的优先级
public final int getPriority() 取得线程的优先级
public static void sleep(long millis) throws InterruptedException
暂停执行当前正在执行的线程并让该线程休眠一段时间。其中millis表示休眠的时间,其单位为毫秒
public static void yield() 暂停执行当前的线程对象,并执行其他线程

public static boolean interrupted()
中断线程,可以通过使用类名直接调用

线程的调度

线程在整个生命周期中始终处于某种状态,从一种状态到另一种状态的转换由线程调度方法实现

线程的优先级

SetPriority(1-10)设置优先级。
Thread.MAX_PRIORITY 10
Thread.MIN_PRIORITY 1
Thread.NORM_PRIORITY 5
程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级
线程优先级不是固定不变的,可以通过Thread类的public final void setPriority(int newPriority);方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。

线程休眠/睡眠

如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法public static void sleep(long millis)throws InterruptedException;,该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。

当前线程调用sleep(long millis)方法后,在指定时间(参数millis)内该线程是不会执行的,这样其它的线程就可以得到执行的机会了。
sleep()方法是一个静态的方法,所以sleep()方法不是依赖于某一个对象的,位置比较随意,当在线程中执行sleep()方法,则线程就进入睡眠状态。sleep()方法是可能发生捕获异常的,所以在使用sleep()方法时必须进行异常处理。

线程让步

线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。
public static void yield();没有声明抛出任何异常

线程插队

当在某个线程中调用其它线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行

停止线程

run方法结束,就会停止线程,开启多线程运行,运行代码通常是循环结构。只要控制住循环,就可以让线程结束。
方法:改变标记。
特殊情况,改变标记也不会停止的情况。
将处于冻结状态的线程恢复到运行状态。interrupt();  中断线程。

守护线程

SetDaemon将线程标记为守护线程或用户线程。在启动线程前调用 。当线程都为守护线程后,JVM退出。
Join方法:
t.join();抢过cpu执行权。
当A线程执行到了B线程的join方法时,A就会等待,等B线程执行完,A才会执行。Join可以用来临时加入线程执行。
yield方法:暂停当前正在执行的线程对象,并执行其他线程。

new Thread() {
for(int x=0;x<100;x++) {
sop(Thread.currentThread().getName())
}
}.start();
for(int x=0;x<100;x++) {
sop(Thread.currentThread().getName())
}
Runnable r=new Runnable() {
public voud run() {
for(int x=0;x<100;x++) {
sop(Thread.currentThread().getName())
}
}
};
new Thread(r).start();

多线程同步

线程安全

安全产生的原因:当多条语句在操作同一个共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,
另一个线程参与进来执行。导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。

什么时候需要同步

存在临界资源(多个线程访问一个对象),对资源的访问是互斥的(排他的)

同步代码块

当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用synchronized关键字来修饰,被称作同步代码块。
    synchronized(lock对象) {  火车卫生间案例
        需要被同步的代码。(共享数据)

}

lock是一个锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。

对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程,即使获取cpu的执行权,也进不去,因为没有获取锁。
同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。

同步的前提

1.必须要有两个或者两个以上的线程
2.必须多个线程必须使用同一个锁
  必须保证同步中只能有一个线程在运行
好处:解决了线程的安全问题
弊端:消耗了资源,多个线程需要判断锁。

同步方法

public synchronized void show() { }
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其它线程都会发生阻塞,直到当前线程访问完毕后,其它线程才有机会执行方法。

同步函数的锁是 this。函数需要被对象调用,那么函数都有一个所属对象引用。
想让线程停一下  Thread.sleep(10);
如果同步函数被静态修饰后,使用的锁是 classsynchronized (对象名.class)
1.明确哪些代码是多线程运行代码
2.明确共享数据
3.明确多线程运行代码中哪些语句是操作共享数据的
Synchronized不会被继承,如果一个类声明一个方法为synchronized,子类继承该类,这个方法被继承后则不再保持同步,除非子类中的这个方法也使用关键字synchronized修饰。

死锁问题

由于两个线程相互等待对方已被锁定的资源
循环等待条件:第一个线程等待其它线程,后者又在等待第一个线程。
同步中嵌套同步,可能会发生 避免死锁:当几个线程都要访问共享资源A、B、C时,保证使每个线程都按照同样的顺序去访问它们,比如都先访问A,在访问B和C。要避免死锁,应该确保在获取多个锁时,在所有的线程中都以相同的顺序获取锁。

多线程通讯

多个线程在操作同一个资源,但是操作的动作不同。
1.是不是两个或两个以上的线程。解决办法 两个线程都要被同步。
2.是不是同一个锁。解决办法 找同一个对象作为锁。

等待唤醒机制

wait后,线程就会存在线程池中,notify后就会将线程池中的线程唤醒。
notifyAll();唤醒线程池中所有的线程。
实现方法 :
给资源加个标记 flag   
synchronized(r) {
    while(r.flag) //多个生产者和消费者   if(r.flag)
//一个生产者和消费者
    r.wait();
     代码
    r.flag=true;
    r.notify();
    r.notifyAll();
}
上面三种方法都使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁。

为什么这些操作线程的方法要定义在object类中呢?
因为这些方法在操作同步中线程的时候,都必须要表示它们所操作线程只有的锁。只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。

也就是说,等待和唤醒必须是同一个锁,而锁可以是特意对象,可以被任意对象调用的方法定义在Object类中

Lock接口

Lock 替代了synchronized 
Condition 替代了 Object监视器方法

好处

将同步synchronized 替换成了 Lock
将object中的wait notify notifyAll 替换成了 Condition对象
该对象可以Lock锁进行获取。一个锁可以对应多个Condition对象
注意:释放锁的工作一定要执行

private Lock lock=new ReentrantLock();
private Condition condition =lock.newCondition();
public void cet(String name ) throws {
lock.lock();
try {
while(flag)
contidition.await();
this.name=name+"--"+count++;
sop(Thread.currentThread().getName()+"...生产者..."+this.name)
flag=true;
condition.signalAll();
}
finally {
lock.unlock();
}
}

final void wait() throws InterruptedException方法
通知当前的线程进入睡眠直到其他线程进入调用notify()唤醒它。在睡眠之前,线程会释放掉所占有的“锁标志”,则其占用的所有synchronized代码块可被别的线程使用。

final void notify()方法
唤醒在该同步代码块中第一个调用wait()的线程(即第一个进入休眠状态的线程),并且这时会从线程等待池转移到锁标志等待池中,因为该线程没有立刻获取锁标志。

final void notifyAll()方法
唤醒在该同步代码块中所有调用wait()的线程,并且会从线程等待池中所有该对象的所有线程转移至锁标志等待池中,因为这些线程没有获取锁标志。

基于线程调度的通信

Java 5以前

wait() - 线程暂停/等待
notify() / notifyAll() - 唤醒等待的线程
这些方法在根类Object中是用final关键字声明的,因此所有的类都包含这些方法
这3个方法仅在synchronized修饰块中才能被调用。

Java 5+

Condition
await() - 线程等待
signal() / signalAll() - 唤醒等待的线程

线程池

用空间换时间,事先创建多个线程放入池中,节省创建和销毁线程在时间上的开销

创建线程池

Executors工具类
newFixedThreadPool(int)推荐在服务器编程时使用
newCachedThreadPool

ExecutorService

execute(Runnable) - 启动线程分配任务
submit(Callable<T>) - 启动线程分配任务
Future<T>在将来的某个时间获得线程的执行结果
shutdown() - 等待线程执行完毕关闭线程池
shutdownNow() - 立即关闭线程池返回等待执行的线程
isTerminated() - 是否执行完所有线程