当前位置:   article > 正文

JAVA基础 多线程技术(V1.0)_java 多线程学习笔记

java 多线程学习笔记

目录

一、多线程介绍

1.1 多线程中的基本概念

1.1.1多线程与进程

1.1.2 进程、线程的区别和联系

1.1.3 并发和并行的区别

1.1.4   线程的执行特点

1.1.5 主线程与子线程

二、线程的创建及生命周期

2.1  通过继承Thread类实现多线程

2.1.1 继承Thread类实现多线程的步骤:

 2.2 通过Runnable接口实现多线程

2.3 线程的执行流程

2.4 线程状态和生命周期

三、线程的使用

3.1 终止线程的典型方式

3.2 线程休眠

3.3 线程让步

3.4 线程联合

 3.4.1 线程联合案例

3.5  Thread类中的其他常用方法

3.5.1 获取当前线程名称

3.5.2 修改线程名称

3.5.3判断线程是否存活

 四、线程的优先级

4.1 线程优先级的使用

五、守护线程

5.1 守护线程的使用

六、线程同步

6.1 什么是线程冲突?

6.2 同步问题的提出

6.3 实现线程同步

6.4 线程冲突案例演示

6.4.1 没有实现线程冲突

 6.4.2 实现线程同步

 6.5 使用this作为线程对象锁

6.6 使用字符串作为线程对象锁

 6.7 使用Class作为线程对象锁

6.8 使用自定义对象作为线程对象锁

6.9 线程死锁

6.9.1 死锁的概念

 6.9.2 解决线程死锁

 七、线程并发协作

7.1 生产者消费者模式介绍

7.2 实现消费者与生产者模式


一、多线程介绍

1.1 多线程中的基本概念

1.1.1多线程与进程

什么是程序?

程序(Program)是一个静态的概念,一般对应于操作系统中的一个可执行文件。

什么是进程?

执行中的程序叫做进程(Process),是一个动态的概念。其实进程就是一个在内存中独立运行的程序空间 。进程之间相互独立数据不共享,都有自己的CPU时间。缺点是CPU负担较重而且浪费资源。

现代操作系统比如Mac OS X,Linux,Windows等,都是支持“多任务”的操作系统,啥叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用逛淘宝,一边在听音乐,一边在用微信聊天,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。如下,任务管理器中每个应用实际上就是个进程,还有很多的进程在后台运行。

什么是线程?

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

有些进程还不止同时干一件事,。我们可以将微信看作是一个进程,它可以同时进行打字聊天,视频聊天,朋友圈等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。也就是说线程是在应用软件中相互独立,可以同时运行的功能。

为什么要有多线程?

多线程可以减少cpu在程序执行中的等待时间,提高CPU的执行效率。

多线程的应用场景

软件中的耗时操作、拷贝迁移大文件、加载大量的资源文件、聊天软件、后台服务器等都需要多线程技术,因为如果使用单线程的话加载这些资源会很耗时间,浪费了cpu。只要是想让多个事情同时运行就需要多线程。

1.1.2 进程、线程的区别和联系

小案例:

乔布斯想开工厂生产手机,费劲力气,制作一条生产线,这个生产线上有很多的器件以及材料。一条生产线就是一个进程。

只有生产线是不够的,所以找五个工人来进行生产,这个工人能够利用这些材料最终一步步的将手机做出来,这五个工人就是五个线程,为了提高生产率,有两种办法:

  1. 一条生产线上多招些工人,一起来做手机,这样效率是成倍増长,即单进程多线程方式。
  2. 多条生产线,每个生产线上多个工人,即多进程多线程

结论: 

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
  4. 调度和切换:线程上下文切换比进程上下文切换要快得多。

1.1.3 并发和并行的区别

并发是指在一段时间内同时做多个事情。当有多个线程在运行时,如果只有一个CPU,这种情况下计算机操作系统会采用并发技术实现并发运行,具体做法是采用“ 时间片轮询算法”,在一个时间段的线程代码运行时,其它线程处于就绪状。这种方式我们称之为并发。(Concurrent)。

并行指的是在同一时刻,有多个指令在多个CPU上同时执行。

结论:

  1. 串行(serial):一个CPU上,按顺序完成多个任务。
  2. 并行(parallelism):指的是任务数小于等于cpu核数,即任务真的是一起执行的。处理这些任务的CPU不止有一个,而是多个CPU处理不同的任务。
  3. 并发(concurrency):一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多于cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

1.1.4   线程的执行特点

方法的执行特点

Java程序中方法是串行执行的。

线程的执行特点

1.1.5 主线程与子线程

主线程

虚拟机程序实际上就是一个进程,当Java程序启动时,一个线程会立刻运行,该线程通常叫做程序的主线程(main thread),即main方法对应的线程,它是程序开始时就执行的。

Java应用程序会有一个main方法,是作为某个类的方法出现的。当程序启动时,该方法就会第一个自动的得到执行,并成为程序的主线程。也就是说,main方法是一个应用的入口,也代表了这个应用的主线程。JVM在执行main方法时,main方法会进入到栈内存,JVM会通过操作系统开辟一条main方法通向cpu的执行路径,cpu就可以通过这个路径来执行main方法,而这个路径有一个名字,叫main(主)线程。

主线程的特点

它是产生其他子线程的线程。

它不一定是最后完成执行的线程,子线程可能在它结束之后还在运行。

主线程只有一个,除了主线程其他都是子线程。

子线程

在主线程中创建并启动的线程,一般称之为子线程。

二、线程的创建及生命周期

2.1  通过继承Thread类实现多线程

2.1.1 继承Thread类实现多线程的步骤:

1、在Java中负责实现线程功能的类是java.lang.Thread 类。

此种方式的缺点:如果我们的类已经继承了一个类(如小程序必须继承自 Applet 类),则无法再继承 Thread 类。因为Java不允许多继承。

2、可以通过创建 Thread的实例来创建新的线程。

3、每个线程都是通过某个特定的Thread对象所对应的方法run( )来完成其操作的,方法run( )称为线程体。

4、通过调用Thread类的start()方法来启动一个线程。

线程的执行需要在实现Thread类中的run();方法,该方法实际上就是线程体,此外在启动线程时,不是调用run方法,而是调用Thread类中的start()方法类启动线程。

  1. package cn.it.bz.Thread;
  2. public class TestThread extends Thread {
  3. //线程方法(线程体),当线程启动后该方法会立即执行。该方法不能直接调用,而是通过
  4. // Thread类中的start();方法执行。
  5. @Override
  6. public void run() {
  7. for (int i = 0; i < 10; i++) {
  8. System.out.println(this.getName() + ":" + i);//getName()方法是返回线程名称
  9. }
  10. }
  11. //main方法就是线程中的主线程
  12. public static void main(String[] args) {
  13. TestThread testThread1 = new TestThread(); //创建子线程对象1
  14. testThread1.start(); //启动线程,此时主线程和子线程1都在执行
  15. TestThread testThread2 = new TestThread(); //创建子线程对象2
  16. testThread2.start(); //启动线程,此时主线程和子线程1、2都在执行
  17. }
  18. }

 2.2 通过Runnable接口实现多线程

在开发中,我们应用更多的是通过Runnable接口实现多线程。这种方式克服了继承Thread类的缺点,即在实现Runnable接口的同时还可以继承某个类。两种方式比较看,实现Runnable接口的方式要通用一些。

从源码角度看,Thread类也是实现了Runnable接口。Runnable接口的源码如下:

public class Thread implements Runnable{……}
  1. package cn.it.bz.Thread;
  2. public class TestThread2 implements Runnable {
  3. //线程方法
  4. @Override
  5. public void run() {
  6. for (int i = 0; i < 10; i++) {
  7. //Thread类是java.lang包下的类不需要导包
  8. System.out.println(Thread.currentThread().getName() + ":" + i);
  9. }
  10. }
  11. //主线程
  12. public static void main(String[] args) {
  13. //表示将任务交给线程处理。new TestThread2对象可以看作是线程要执行的任务。
  14. Thread thread1 = new Thread(new TestThread2());
  15. thread1.start(); //启动线程
  16. Thread thread2 = new Thread(new TestThread2());
  17. thread2.start();
  18. }
  19. }

或者是使用Lambda表达式创建线程:

  1. package cn.it.bz.Lambda;
  2. //Runnable接口中只有一个抽象方法run,也就是说Runnable是个函数接口。
  3. public class Test3 {
  4. public static void main(String[] args) {
  5. System.out.println("主线程"+ Thread.currentThread().getName()+"启动!");
  6. //Lambda表达式实现run 方法。
  7. Runnable runnable = () -> {
  8. for (int i = 0; i < 10; i++ ) {
  9. System.out.println(Thread.currentThread().getName() + ", "+i);
  10. try {
  11. Thread.sleep(1000);
  12. } catch (InterruptedException e) {
  13. }
  14. }
  15. };
  16. //线程包装
  17. Thread thread = new Thread(runnable, "Lambda线程");
  18. //线程启动
  19. thread.start();
  20. System.out.println("主线程"+ Thread.currentThread().getName()+"结束!");
  21. }
  22. }

一个线程不能被启动两次。 

2.3 线程的执行流程

线程被执行后先进入就绪态,等待被CPU执行。CPU通过时间片轮询的方式执行线程,当时间片用完后该线程又变为就绪态在就绪队列中等待CPU执行,而此时的CPU就处理其他线程。当线程出现故障时,无论该线程的时间片是否结束,cpu都会将该线程变为阻塞态放在阻塞队列中,阻塞结束的时候变为就绪态回到就绪队列中。当线程执行完毕之后线程进入死亡状态。

需要注意的是:就绪态不能直接变为阻塞态,阻塞态不能直接变为运行态。

2.4 线程状态和生命周期

一个线程对象在它的生命周期内,需要经历5个状态。 

  1. 新生状态(New)

    用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

  2. 就绪状态(Runnable)

    处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4种原因会导致线程进入就绪状态:                          

  • 新建线程:调用start()方法,进入就绪状态;
  • 阻塞线程:阻塞解除,进入就绪状态;
  • 运行线程:调用yield()方法,直接进入就绪状态;
  • 运行线程:JVM将CPU资源从本线程切换到其他线程。

3、运行状态(Running)

在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

4、阻塞状态(Blocked)

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。

有4种原因会导致阻塞:

  1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
  2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
  3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
  4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

5、死亡状态(Terminated)

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。当一个线程进入死亡状态以后,就不能再回到其它状态了。

三、线程的使用

3.1 终止线程的典型方式

终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。因为线程可能还有后续工作,不能直接将他们嘎了。控制子线程生死的是主线程。

  1. package cn.it.bz.Thread;
  2. import java.io.IOException;
  3. public class KillThread implements Runnable {
  4. //生死牌,true为生,false为死
  5. private boolean flag = true;
  6. //控制生死牌的方法
  7. public void killThread(){
  8. this.flag = false;
  9. }
  10. //子线程
  11. @Override
  12. public void run() {
  13. System.out.println("子线程开始:"+Thread.currentThread().getName());
  14. int i = 0;
  15. while (flag){
  16. System.out.println(Thread.currentThread().getName()+"-"+i);
  17. i++;
  18. try {
  19. Thread.sleep(1000); //休眠,线程由运行状态变为阻塞状态
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. System.out.println("子线程结束");
  25. }
  26. //主线程
  27. public static void main(String[] args) throws IOException {
  28. System.out.println("主线程开始");
  29. KillThread kill = new KillThread();
  30. Thread thread = new Thread(kill);
  31. //启动子线程
  32. thread.start();
  33. //使主线程阻塞
  34. System.in.read();
  35. //主线程结束,结束子线程
  36. kill.killThread();
  37. System.out.println("主线程结束");
  38. }
  39. }

主线程启动后,将创建的线程对象包装为Thread 对象,调用start();方法启动子线程。此时子线程执行while循环,主线程阻塞,但是子线程一直在执行,当从键盘输入数据时,主线程不再阻塞并开始向下执行,子线程杀死程序和打印输出语句,主线程是不会等待子线程死亡的。子线程被杀死时不是立即结束工作,而是先执行完线程(也就是run方法)后死亡。

3.2 线程休眠

sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。sleep方法为Thread类的静态方法,参数为休眠的毫秒数(1秒 = 1000毫秒)。

  1. package cn.it.bz.Thread;
  2. public class SleepThread implements Runnable {
  3. @Override
  4. public void run() {
  5. System.out.println("子线程开始:"+Thread.currentThread().getName());
  6. for (int i = 0; i < 10; i++) {
  7. System.out.println(Thread.currentThread().getName()+"---"+i);
  8. //子线程休眠
  9. try {
  10. Thread.sleep(1000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }
  16. //主线程,主线程在哪个类是没有区别
  17. public static void main(String[] args) {
  18. System.out.println("主线程开始");
  19. SleepThread sleepThread = new SleepThread();
  20. Thread thread = new Thread(sleepThread);
  21. //启动子线程
  22. thread.start();
  23. System.out.println("主线程结束");
  24. }
  25. }

 主线程是不会等待子线程的,两个线程分别执行各自的。

3.3 线程让步

yield()让当前正在运行的线程回到就绪状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

使用yield方法时要注意的几点:

  • yield是一个静态的方法。
  • 调用yield后,yield告诉当前线程把运行机会交给具有相同优先级的线程。
  • yield不能保证,当前线程迅速从运行状态切换到就绪状态。
  • yield只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。当让步线程遇到堵塞时先变为阻塞态,阻塞结束了再变为就绪态。
  1. package cn.it.bz.Thread;
  2. public class TestyieldThread implements Runnable{
  3. @Override
  4. public void run() {
  5. for (int i = 0; i < 15; i++) {
  6. //如果当前线程名字是Thread-1,就让步,而且只让步第一次
  7. if ("Thread-1".equals(Thread.currentThread().getName())){
  8. if (i == 0){
  9. System.out.println("我™直接让步~");
  10. Thread.yield();
  11. }
  12. }
  13. System.out.println(Thread.currentThread().getName()+"---"+i);
  14. }
  15. }
  16. public static void main(String[] args) {
  17. Thread thread1 = new Thread(new TestyieldThread());//子线程1
  18. Thread thread2 = new Thread(new TestyieldThread());//子线程2
  19. //启动线程,线程的运行顺序取决于CPU的线程调度
  20. thread1.start();
  21. thread2.start();
  22. }
  23. }

3.4 线程联合

当前线程邀请调用方法的线程优先执行,在调用方法的线程执行结束之前,当前线程不能再次执行。线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。和Java中方法的执行顺序差不多。

join方法的使用

join()方法就是指调用该方法的线程在执行完run()方法后,再执行join方法后面的代码,即将两个线程合并,用于实现同步控制。

  1. package cn.it.bz.Thread;
  2. import java.util.stream.Stream;
  3. //子线程A
  4. class A implements Runnable{
  5. @Override
  6. public void run() {
  7. for (int i = 0; i < 10; i++) {
  8. System.out.println("当前A线程:"+Thread.currentThread().getName()+"--"+i);
  9. try {
  10. Thread.sleep(1000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }
  16. }
  17. //主线程
  18. public class TestJoinThread {
  19. public static void main(String[] args) throws InterruptedException {
  20. Thread threadA = new Thread(new A());
  21. threadA.start();
  22. for (int i = 0; i < 10; i++) {
  23. if (i == 2) {
  24. //主线程联合A线程,直接在主线程调用join方法
  25. threadA.join();
  26. }
  27. System.out.println("主线程:"+Thread.currentThread().getName()+"--"+i);
  28. Thread.sleep(1000);
  29. }
  30. }
  31. }

主线程和A线程在没有联合之前是同步执行的,但是执行到threadA.join();时,主线程会等待A线程执行完毕之后再执行。

 3.4.1 线程联合案例

  1. package cn.it.bz.Thread;
  2. //儿子买烟线程
  3. class SonThread implements Runnable{
  4. @Override
  5. public void run() {
  6. System.out.println("儿子得知要去买烟,买烟需要十分钟");
  7. for (int i = 0; i < 10; i++) {
  8. System.out.println("第"+i+"分钟");
  9. try {
  10. Thread.sleep(1000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. System.out.println("儿子买烟回来了");
  16. }
  17. }
  18. //爸爸抽烟线程
  19. class FatherThread implements Runnable{
  20. @Override
  21. public void run() {
  22. System.out.println("爸爸想抽烟发现烟抽完了,让儿子去买包华子");
  23. //启动儿子买烟线程
  24. Thread thread = new Thread(new SonThread());
  25. thread.start();
  26. //爸爸需要等着儿子买烟回来
  27. try {
  28. thread.join();
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. System.out.println("儿子买烟出异常了,爸爸出门找儿子");
  32. System.exit(1); //结束运行在虚拟机的进程,找儿子去吧
  33. }
  34. System.out.println("爸爸开心地接过烟,猛吸了一口,说真好!b( ̄▽ ̄)d ");
  35. }
  36. }
  37. public class TestJoinDemo {
  38. public static void main(String[] args) {
  39. System.out.println("这是个爸爸和儿子的故事~");
  40. Thread thread = new Thread(new FatherThread());
  41. thread.start();
  42. }
  43. }

3.5  Thread类中的其他常用方法

3.5.1 获取当前线程名称

方式一

this.getName()获取线程名称,该方法适用于继承Thread实现多线程方式。

class GetName1 extends Thread{
  @Override
  public void run() {
    System.out.println(this.getName());
   }
}

方式二

Thread.currentThread().getName()获取线程名称,该方法适用于实现Runnable接口实现多线程方式。Thread.currentThread()获取当前线程对象

class GetName2 implements Runnable{
  @Override
  public void run() {
    System.out.println(Thread.currentThread().getName());
   }
}

3.5.2 修改线程名称

方式一

当线程继承Thread类时通过构造方法设置线程名称。

  1. package cn.it.bz.Thread;
  2. class SetName1 extends Thread{
  3. //接受自己定义的线程名称
  4. public SetName1(String name){
  5. super(name); //调用父类的构造方法
  6. }
  7. @Override
  8. public void run() {
  9. System.out.println("SetName1线程名称:"+this.getName());
  10. }
  11. }
  12. //主线程
  13. public class TestSetNameThread {
  14. public static void main(String[] args) {
  15. SetName1 setName1 = new SetName1("setName1");
  16. setName1.start();
  17. }
  18. }

方式二

当线程实现Runable接口时通过setName()方法设置线程名称。

  1. package cn.it.bz.Thread;
  2. class SetName implements Runnable{
  3. @Override
  4. public void run() {
  5. System.out.println("当前线程名字:"+Thread.currentThread().getName());
  6. }
  7. }
  8. public class TestSetNameThread2 {
  9. public static void main(String[] args) {
  10. //创建Thread对象
  11. Thread thread = new Thread(new SetName());
  12. thread.setName("
    声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/446714?site
    推荐阅读
    相关标签