多线程

一:基本概念:程序,进程,线程

程序(program):程序是为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

进程(process):进程是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程:有它自身的产生,存在,消亡的过程。——生命周期。进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

线程(thread):进程可进一步细分为多个线程,是一个程序内部执行的一条路径。

  • 若一个程序同一时间并行执行多个线程,则称该程序是支持多线程的
  • 线程作为调度和执行的,每个线程拥有独立的运行栈和程序计数器(PC)
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间——堆和方法区是一个进程所拥有的,线程可以访问其中的对象和变量。这使得线程通信更加简便,有效,但同时多个线程共享的系统资源可能会带来安全隐患

多线程程序的优点:

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  2. 提高计算机系统CPU的利用率。
  3. 改善程序结构,将既长又复杂的进程分为多个线程,独立运行,有利于理解和修改。

何时需要多线程:

  1. 程序需要同时执行两个或多个任务
  2. 程序需要实现一些等待的任务时,如用户输入,文件读写操作,网络操作等
  3. 需要一些后台运行的程序

多线程的创建

1. 继承自Thread类

  1. 创建一个类继承自Thread类
  2. 重写Thread类的run()方法,run()方法中包含该线程需要执行的逻辑
  3. new一个Thread类的子类的对象
  4. 通过调用该对象的start()方法启动线程
class ChildThread extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {if(i % 2 == 0){System.out.println("白");}}}
}
public class MyThread {public static void main(String[] args) {ChildThread t1 = new ChildThread();// start()方法有两个作用,启动线程,调用该线程的run方法// 若调用t1.run(),那么没有启动子线程,只是调用了对象的run()方法。// 一个线程对象只能start()一次t1.start();for (int i = 0; i < 100; i++) {if(i % 2 != 0) {System.out.println("梦" + "Main");}}}
}

Thread类中的常用方法:

  • start():启动当前线程,并调用线程的run()方法
  • run():通常需要重写Thread类中的run()方法,将创建的线程的执行逻辑写在此方法中
  • currentThread:静态方法,获得当前正在执行的线程
  • getName:返回当前正在执行线程的名字
  • setName:设置当前线程的名字
  • yield():释放当前CPU的执行权
  • join():在线程a中执行线程b的join()方法,此时线程a进入阻塞状态,直到线程b执行完之后,线程a才结束阻塞状态
  • sleep(Long millitime):让当前线程睡眠(阻塞)millitime毫秒
  • isAlive:判断当前线程是否存活

线程的优先级:

  1. 优先级

    • MIN_PRIORITY = 1
      
    • NORM_PRIORITY = 5
      
    • MAX_PRIORITY = 10
      
  2. 获得和设置优先级

    public final int getPriority() {return priority;}
    
    public final void setPriority(int newPriority)
    

高优先级的线程可以抢占低优先级线程的资源,但并不绝对,只是说它拥有这个能力。

卖票实例(具有隐患):

// 多个线程共享资源带来了一定的安全隐患,后续解决class WindowDemo extends Thread{// ticket变量需声明为static,需要多个窗口(对象)共用一个变量private static int ticket = 100;@Overridepublic void run() {while (true){if(ticket > 0){System.out.println(getName() + ":" + "卖出票" + ticket);ticket--;}else{break;}}}
}
public class SellTicketDemo {public static void main(String[] args) {WindowDemo w1 = new WindowDemo();WindowDemo w2 = new WindowDemo();WindowDemo w3 = new WindowDemo();w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");w1.start();w2.start();w3.start();}
}

2.实现Runnable接口的类

  1. 创建一个实现Runnable接口的类
  2. 实现Runnable接口中的方法(run()方法)
  3. 创建一个该类的对象
  4. 将此对象作为参数传入到Thread类的构造器中,创建一个Thread对象
  5. 通过调用Thread对象的start方法,开启线程
// 同理,该类存在线程安全问题
class WindowDemo2 implements Runnable{private int ticket = 100;@Overridepublic void run() {while (true){if(ticket > 0){// 该类非Thread类的子类,所以需要调用完整的getName()方法System.out.println(Thread.currentThread().getName() + ":" + "卖出票" + ticket);ticket--;}else{break;}}}
}
public class SellTicketDemo2 {public static void main(String[] args) {WindowDemo2 windowDemo2 = new WindowDemo2();// 这里不需要将ticket声明为static,因为是使用同一个对象去创建的线程,三个线程访问同一个ticketThread t1 = new Thread(windowDemo2);t1.setName("窗口1");Thread t2 = new Thread(windowDemo2);t2.setName("窗口2");Thread t3 = new Thread(windowDemo2);t3.setName("窗口3");/*调用Thread类的start方法,而start方法又会调用Thread类的run(),为什么这里他去调用了实现*Runnable接口类的run()方法呢。因为Thread类的start()方法会先判断是否传入实现Runnable接口*的类,若是的话,则调用实现Runnable接口类的run()方法 */t1.start();t2.start();t3.start();}
}

比较创建多线程的两种方式:

  1. 开发中,优先选择使用第二种方式,实现Runnable接口的方式

    1. 实现接口的方式可以摆脱Java中类的继承的局限性
    2. 在需要开启多个线程,且这些线程拥有共享数据的时候,实现接口的方式更适合
  2. 实际上,Thread类就是实现Runnable接口的类,我们创建一个类去继承Thread类,并覆盖它的run()方法,归根结底还是实现Runnable接口中的run()方法

3. JDK5.0新增的两种方式

3.1 实现Callable接口的类

与Runnable相比,Callable的功能更加强大

  • call方法可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

实现步骤:

  1. 创建一个实现Callable接口的实现类
  2. 实现类重写call方法,将此线程需要执行的逻辑写在call()方法中
  3. 创建实现类的一个对象
  4. 将实现类的对象作为参数传入到FutureTask的构造器中,创建一个FutureTask对象
  5. 将FutureTask对象作为参数传入到Thread类的构造器中,创建一个Thread对象,并调用start()方法
class ThreadTest implements Callable<Integer>{@Overridepublic Integer call() throws Exception {Integer sum = 0;for (int i = 1; i <= 100; i++) {if(i % 2 == 0){System.out.println("遍历到了" + i);sum += i;}}System.out.println("和为:" + sum);return sum;}
}
public class CallableDemo {public static void main(String[] args) {ThreadTest threadTest = new ThreadTest();// Future接口的唯一实现类,实现了Runnable接口和Callable接口FutureTask<Integer> integerFutureTask = new FutureTask<>(threadTest);new Thread(integerFutureTask).start();try {Integer ans = integerFutureTask.get();System.out.println(ans);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}

3.2 使用线程池

思路:提前创建好多个线程放入线程池,使用时直接获取,使用完又放回到池中。这样可以避免线程多次创建销毁,进而实现重复利用。

好处:

  • 提高响应速度(减少创建新线程的时间)

  • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)

  • 便于线程管理

    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • KeepAliveTime:线程没有任务时最多保持多久会终止

线程的生命周期

Thread.State类中定义了线程生命周期中的5个状态:

  • 当一个Thread类或它的子类对象被生命并创建时,新生的线程对象处于新建状态(新建)
  • 处于新建状态的线程调用start方法后,将进入线程队列,等待CPU时间片,此时它已经具备了运行的条件,只是还没被CPU分配内存资源(就绪)
  • 当就绪的线程被被调度并且获得CPU资源时,便进入了运行状态,run()方法中定义了线程的操作和功能(运行)
  • 在某种特殊情况下,被人为挂起或执行输入输出时,让出CPU并临时终止自己的执行,进入阻塞状态(阻塞)
  • 线程完成了它的全部功能或线程被强制地终止或出现异常导致线程结束(死亡)

多线程的安全问题:

  1. 多个线程执行的不确定性引起执行结果的不稳定
  2. 多个线程对数据的共享,可能会对数据造成破坏

解决方法

解决:当一个线程在操作共享数据时,其它的线程不能够参与进来,直到该线程结束后,才允许其它线程对共享数据的操作。

在Java中,我们通过同步机制来解决线程安全的问题。

**方法一:**同步代码块

synchronized(同步监视器){

// 需要被同步的代码,(操作共享数据的代码),共享数据为多个线程共同操作的变量。

}

同步监视器俗称“锁”,任意一个对象即可。但是所有的存在安全隐患线程需要共同使用一个对象,即它们共同拥有一把锁

**方法二:**同步方法

如果同步代码块被完全声明在一个方法中,我们可以将这个方法声明为同步的。

声明同步方法的方式是在方法的返回值前面加上synchronized关键字。

同步方法仍然会用到同步监视器,只不过不需要被主动的声明。

非静态的同步方法,同步监视器为this。该情况对应的是以Runnable接口的形式创建线程

静态的同步方法,同步监视器为当前类本身。该情况对应的是以Thread类子类的形式创建线程

同步的方式解决了线程安全的问题,但同时每次只能够有一个线程执行同步代码块,相当于又回到了单线程的过程(局限性)。

单例模式之懒汉式的线程安全

1. 什么是单例模式

单例模式是保证整个应用程序周期内,在任何时刻,被指定的类只能够有一个实例。

实现单例模式的方式:

  1. 构造方法声明为私有的,外界不能够调用构造方法构造对象
  2. 类本身需要构造一个对象——调用构造方法即可
  3. 通过公共的方法对外提供这个唯一的实例对象

2. 实例

// 外界只能通过getInstance()方法来获得Bank类的唯一对象
class Bank{private Bank(){};private static Bank instance = null;public static Bank getInstance(){// 该层if语句可以提高一定的效率, 因为同步操作实际上只需要进入的第一个// 线程操作。if(instance == null){// 同步代码块的方式synchronized(Bank.class){if(instance == null){instance = new Bank();}}}return instance;}
}

线程的死锁问题

  • 死锁的理解

    • 不同的线程占据着对方所需要的同步监听器不放弃,都在等待着对方所占据的自己所需要的同步监听器,进而形成了线程的死锁。
    • 死锁出现后,程序并不会抛出异常,也不会出现提示,只是线程此时处于阻塞状态,无法继续。这不符合我们对于线程的定义,所有的线程最后都应该消亡。
  • 解决办法
    • 专门的算法,原则
    • 尽量减少同步资源的定义
    • 尽量避免嵌套同步

死锁示例:

public class DeadLock {public static void main(String[] args) {StringBuilder s1 = new StringBuilder();StringBuilder s2 = new StringBuilder();new Thread(){@Overridepublic void run() {synchronized(s1){s1.append('a');s2.append(1);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s2){s1.append('b');s2.append(2);}}System.out.println(s1);System.out.println(s2);}}.start();new Thread(() -> {synchronized (s2){s1.append('c');s2.append(3);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s1){s1.append('d');s2.append(4);}}System.out.println(s1);System.out.println(s2);}).start();}
}

Lock锁

Lock锁是JDK5.0新增的解决线程安全的方法。同时Lock是一个接口,我们一般使用它的实现类ReentrantLock类。 // 它自己就是一把抽象的锁。

class WindowDemo extends Thread{// ticket变量需声明为static,需要多个窗口(对象)共用一个变量// Lock锁同样要声明为静态的private static ReentrantLock lock = new ReentrantLock(true);private static int ticket = 100;@Overridepublic void run() {while (true) {lock.lock();try {if (ticket > 0) {System.out.println(getName() + ":" + "卖出票" + ticket);ticket--;} else {break;}}finally {lock.unlock();}}}

synchronized与Lock的区别:

  • synchronized的机制是在执行完同步代码块或方法后,自动释放同步监视器。

  • Lock需要手动的开启同步(Lock.lock()),同时在同步结束之后需要手动的结束结束同步(Lock.unlock())。

线程的通信

线程通信涉及到的三个方法:

  • wait():执行此方法,当前线程将进入到阻塞状态,并且释放同步监视器。
  • notify():执行此方法,将唤醒被wait()的一个线程,如果有多个线程被wait()则唤醒优先级高的那个线程。
  • notifyAll():执行此方法,将唤醒所以被wait()的线程。

注意:

  • 以上三个方法必须使用在同步代码块或同步方法中
  • 以上三个方法的调用者必须是同步代码块或者同步方法的同步监听者
  • 以上三个方法定在在java.lang.Object类中

sleep()方法和wait()方法的异同:

相同点:一旦执行上述方法,当前正在进行的线程将进入阻塞状态。

不同点:1)sleep()方法声明在Thread类中,为静态方法。wait()方法声明在Object()类中。

​ 2)调用的要求不同,只要我们想要,随时可以通过Thread.sleep()调用sleep()方法。而wait()方法必须在 同步方法或同步代码块中调用。

​ 3)调用wait()方法会释放同步监视器,而调用sleep()方法不会释放同步监视器。

生产者与消费者问题

有一个生产者可以一直生产产品,柜台工作人员可以从生产者这里得到产品,而消费者可以从柜台工作人员这里买到产品。要求唱片最多为66个,一旦有了66个产品,柜台工作人员就会通知生产者不要生产了,同时当没有产品得时候,柜台工作人员会告述消费者不要来买东西了,没有了。

分析:

  • 多线程问题,生产者是一类线程,消费者是一类线程。
  • 存在线程安全问题,它们之间有共享数据,柜台工作人员或者说是柜台工作人员手中的产品
/** Clerk类表示柜台工作人员,成员变量productNumber表示当前产品的个数。* 生产者可以到调用produce()方法表示生产一个产品* 消费者可以调用consume()方法表示买走一个产品* 生产者和消费者需要调用getProductNumber()方法才能得得当前的产品个数*/
class Clerk{private int productNumber = 0;public int getProductNumber() {return productNumber;}public void produce() {productNumber++;}public void consume() {productNumber--;}
}
class Producer implements Runnable{// 声明成员变量clerk,它是多线程的共同变量,可充当同步锁使用private Clerk clerk;// 声明构造方法从外部传入公共的clerkpublic Producer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {while (true) {synchronized (clerk) {if (clerk.getProductNumber() < 66) {// notify方法与wait方法的调用对象应该和同步锁一样,若不声明由谁调用方法,将会默认为this.方法clerk.notify();clerk.produce();System.out.println(Thread.currentThread().getName() + "生产产品:" + clerk.getProductNumber());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}} else {try {clerk.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}}
}
class Customer implements Runnable{private Clerk clerk;public Customer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {while (true) {synchronized (clerk) {if (clerk.getProductNumber() > 0) {// notify方法与wait方法的调用对象应该和同步锁一样,若不声明由谁调用方法,将会默认为this.方法clerk.notify();System.out.println(Thread.currentThread().getName() + "消费产品:" + clerk.getProductNumber());clerk.consume();try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}} else {try {clerk.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}}
}
public class ThreadDemo {public static void main(String[] args) {Clerk clerk = new Clerk();Producer producer = new Producer(clerk);Customer customer = new Customer(clerk);Customer customer1 = new Customer(clerk);Thread t1 = new Thread(producer);Thread t2 = new Thread(customer);Thread t3 = new Thread(customer1);t1.setName("生产者1");t2.setName("消费者1");t3.setName("消费者2");t1.start();t2.start();t3.start();}
}

尚硅谷视频总结——Java多线程相关推荐

  1. 尚硅谷《全套Java、Android、HTML5前端视频》

    尚硅谷<全套Java.Android.HTML5前端视频> (百万谷粉推荐:史上最牛.最适合自学的全套视频.资料及源码) [尚硅谷官网资料导航] 谷粒学院在线学习:http://www.g ...

  2. 尚硅谷MySQL高级JAVA版

    尚硅谷MySQL高级JAVA版 1.MySQL环境 1.1.环境安装 1.2.安装位置 1.3.修改字符集 1.4.配置文件 2.MySQL逻辑架构 3.存储引擎 4.SQL性能下降的原因 5.SQL ...

  3. Spring框架(基于尚硅谷视频)

    该笔记源于尚硅谷视频对Spring的讲解(适合新手,大佬划走),视频连接如下: https://www.bilibili.com/video/BV1Vf4y127N5?p=27&spm_id_ ...

  4. 史上最牛、最适合自学的尚硅谷《全套Java视频教程》

    涵盖:视频教程.在线课堂.最新教程SpringMVC   视频教程 Java基础阶段: 一.20天横扫Java基础(课堂实录) http://pan.baidu.com/s/1kTgIYDT 二.尚硅 ...

  5. 尚硅谷2022版Java课程体系,霸气来袭

    摘要:技术为王,课比天大. 尚硅谷当家学科硬核升级, 全新Java课程体系重磅来袭! 见证好课,一睹为快: 01 额外加赠架构师课 加量不加价,扶上马再送一程. 为进一步增强学员市场竞争力, 面授结束 ...

  6. Docker_尚硅谷视频学习笔记

    文章目录 1 Docker 简介 前提知识+课程定位 Docker 是什么? 问题:为什么会有docker出现 docker理念 总结 能干嘛 之前的虚拟机技术 容器虚拟化技术 开发/运维(DevOp ...

  7. 4.Java学习笔记第四节——程序流程控制(尚硅谷视频整理)

    文章目录 一.分支语句 1.       if-else 结构 1)如何从键盘获取不同类型的变量 2.      switch-case 结构 二.循环结构 1.      for 循环 一.分支语句 ...

  8. 尚硅谷和黑马java,全网首发!

    网易严选java一面 基本只问了Java相关的内容 近期做的项目有遇到什么困难吗,怎么解决的: HashMap详细介绍一下,怎么计算下标值的,时间复杂度是多少,最坏的时间复杂度是多少,在扩容的时候时间 ...

  9. JavaSE(尚硅谷视频学习笔记)

    文章目录 Java基础编程 Java语言概述 Java语言简述 1.基础图解 2.常识 3.计算机语言的发展迭代 4.Java语言版本迭代概述 5. Java语言应用的领域 6.Java语言的特点 开 ...

最新文章

  1. 鸿蒙生死印里的声音是谁,逆天邪神:喊逆玄的确实是黎娑,鸿蒙生死印应该也是活物...
  2. 自己动手,打造轻量级VSCode/C#环境代替LinqPad
  3. 栖息在生态办公室,裸心社与USGBC达成战略合作
  4. 4.5.2 OSPF协议与链路状态算法
  5. php如何和c进行数据交换,PHP与 后台c交换数据 | 学步园
  6. 浅入浅出理解傅里叶变换
  7. 如何计算给定一个unigram语言模型_CS224n笔记[5]:语言模型(LM)和循环神经网络(RNNs)...
  8. ArcGIS API for JS4.7加载FeatureLayer,点击弹出信息并高亮显示
  9. 持久层和数据访问层_什么是持久层? JDBC 演变的 Mybatis 架构分析
  10. C++算法学习(力扣:1003. 检查替换后的词是否有效)
  11. bat之ping操作
  12. python3吧_基于python3 抓取贴吧图片与评论 图片下载保存
  13. AOP技术介绍--(AOP技术基础)
  14. canal mq数据同步
  15. IDEA配置连接数据库时报错Server returns invalid timezone. Go to ‘Advanced‘ tab and set ‘serverTimezone‘ propert
  16. Shell文件查找之find命令(1)
  17. python开发bi报表_bi报表
  18. 计算机的普及与运用的英语作文,求篇英语作文.120字左右随着电脑和手机的普及 有些人认为可以方便交流和省钱,但写信的人会越来越少,信会完全消失,有些人...
  19. Linux 3.进程间通信(IPC)(pipe 无名管道、mkfifo 有名管道、ftok、msgget、msgrcv、msgsnd、msgctl 消息队列)
  20. 随机信号分析第2版[赵淑清郑薇编著](课后作业答案自己写的,第一二三四章全部,第五章2345题)

热门文章

  1. C语言知识点总结 -思维导图
  2. C++ 文件读写实战——2进制文件查看器(16进制显示)
  3. 移动端APP测试用例
  4. linux 修改tomcat编码,修改Tomcat运行时jvm编码问题
  5. 华为ENSP安装介绍(高效解决#####,40问题)
  6. 关于绿盾解密功能java代码。
  7. linux字符设备和块设备的区别 以及网络设备
  8. mocha的使用总结
  9. 高通QFIL版本烧录过程
  10. node mocha_如何使用Mocha和Assert测试Node.js模块