多线程

来源:互联网 发布:linux mkdir创建目录 编辑:程序博客网 时间:2024/05/22 04:52



  • 多线程概述

    • 进程:正在进行中的程序。
    • 线程:进程中的独立控制单元。线程控制着进程的执行。
    • 一个进程中至少有一个线程。
    • jvm启动时有一个java.exe进程,该进程至少有一个主线程负责程序的执行,这个线程运行的代码存在于main函数中,该线程称之为主线程。
    • jvm启动时,除了主线程,还有负责垃圾回收的线程。
    • 多线程程序:有不止一个线程(执行路径)的程序就是多线程程序,如迅雷。
    • 多线程存在的意义:
      • 使程序中不同部分的代码产生“同时运行”的效果。

  • 线程的创建方式

    • 继承Thread类

      • 创建线程的步骤:
        1. 定义类继承Thread。
        2. 覆写Thread类中的run方法。
          • 目的:将自定义代码存储在run()方法中,让线程运行。
        3. 调用线程的start方法
          • 该方法有两个作用:启动线程,调用run方法。
          • 如果直接调用run方法,相当于仅仅是对象调用普通方法。线程虽然创建了,但是并没有运行。
      • Thread类
        • Thread类用于描述线程。
        • Thread类中的run()方法用于存储线程要运行的代码。
        • start()方法用于开启线程,并执行该线程的run()方法。
      • 练习
        • 需求:创建两个线程,和主线程交替运行。
        • 代码:
          package cn.itcast.heima;public class Test {public static void main(String[] args) {SubThread st1 = new SubThread();SubThread st2 = new SubThread();st1.start();st2.start();for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "run..." + i);}}}class SubThread extends Thread{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "run..." + i);}}}
          打印结果:

    • 实现Runnable接口

      • 创建线程的步骤:
        1. 定义类实现Runnable接口。
        2. 覆盖Runnable接口中的run()方法。
        3. 创建类Thread的线程对象。
        4. 将Runnable子类对象作为实际参数传递给Thread类的构造函数。
        5. 通过Thread对象的start()方法开启线程并调用Runnable接口子类的run()方法。
      • 优势:
        • 避免了单继承的局限性
        • 可以创建多个线程共享Runnable子类对象的数据,避免了共享数据定义为静态。
      • 例子:
        • 需求:简单的卖票程序。实现多个窗口同时卖票。
        • 如果按照继承Thread类的方式实现多线程,代码如下:
          package cn.itcast.heima;public class Test {public static void main(String[] args) {//开放四个窗口售票Ticket t1 = new Ticket();Ticket t2 = new Ticket();Ticket t3 = new Ticket();Ticket t4 = new Ticket();t1.start();t2.start();t3.start();t4.start();}}class Ticket extends Thread{private static int ticket = 20;//总共20张票@Overridepublic void run() {while (ticket > 0) {System.out.println(Thread.currentThread().getName() + "...sale:" + ticket--);}}}

          打印结果:


          这种方式需要将票数定义为静态成员。
          缺陷:ticket成员变量生命周期过长,且Ticket类不能继承其它类。
        • 可以将其改为实现Runnable接口的方式实现多线程,代码如下:
          package cn.itcast.heima;public class Test {public static void main(String[] args) {Ticket t = new Ticket();//开放四个窗口售票Thread t1 = new Thread(t);Thread t2 = new Thread(t);Thread t3 = new Thread(t);Thread t4 = new Thread(t);t1.start();t2.start();t3.start();t4.start();}}class Ticket implements Runnable{private int ticket = 20;//总共20张票@Overridepublic void run() {while (ticket > 0) {System.out.println(Thread.currentThread().getName() + "...sale:" + ticket--);}}}
          打印结果:


          这种方式避免了将ticket成员变量定义为static,并且避免了单继承的局限性。
        • 实际开发中,建议使用实现Runnable接口的方式实现多线程。

    • 两种方式区别:

      • 继承Thread类:线程代码存放在Thread子类的run()方法中。
      • 实现Runnable接口:线程代码存放在Runnable接口子类的run()方法中。

  • 多线程的特性:随机性

    • 对于多线程程序,每一次的运行结果都不相同。
    • 因为多个线程都需要获取CPU的执行权,CPU执行到哪个线程,哪个线程就运行。
    • 在某一时刻,只能有一个线程在运行,CPU在做着快速的切换,以达到看上去是同时运行的效果。

  • 线程的五种状态

    • 线程的五种状态:被创建,运行,冻结,临时(阻塞),消亡。关系如下图:

  • 获取线程对象以及名称

    • 线程有默认的名称:Thread-编号(从0开始)。
    • currentThread():获取当前线程对象(静态)。
    • getName():获取线程名称。
    • 设置线程名称:setName()或者构造函数。

  • 多线程的安全问题:同步

    • 问题原因:
      • 多个线程在操作同一个共享数据时,一个线程对多条语句只执行了一部分,另一个线程就参与进来,导致共享数据出现问题。
    • 解决办法:
      • 一个线程在执行过程中,其它线程不可以参与执行。
    • Java对于多线程的安全问题提供了专业的解决方案:同步代码块。
    • 同步代码块格式:

    • 判断需要被同步的代码:
      • 操作共享数据的代码就是需要被同步的代码。
    • 同步代码块的原理:
      • 对象如同锁。持有锁的线程才可以进入同步代码块执行语句。不持有锁的线程即使获得了CPU的执行权,也不能进入同步代码块执行语句。
      • 火车上的卫生间的例子。
    • 同步的前提:
      • 同步需要两个或者两个以上的线程。
      • 多个线程使用的是同一个锁。
    • 同步的好处:
      • 解决了多线程的安全问题。
    • 同步的弊端:
      • 当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形 中会降低程序的运行效率。
    • 同步函数
      • 格式:
        在函数上加上synchronized修饰符即可。
      • 同步函数使用的锁是this。要保证同步代码块和同步函数用的是同一个锁,同步代码块的参数必须是this。
    • 静态同步函数
      • 静态同步函数的锁是该方法所在类的字节码文件对象:类名.class。
    • 死锁
      • 死锁的出现:同步中嵌套同步。
      • 实际开发中,要避免出现死锁问题。

  • 线程间通信

    • 概念

      • 线程间通信就是多个线程在操作同一个资源,但是操作的动作不同。

    • 等待唤醒机制

      • 多线程操作共享数据时要考虑使用等待唤醒机制。
      • 等待线程存放在线程池中,notify()通常唤醒线程池中的最先等待的线程,notifyAll()唤醒线程池中的所有线程。
      • wait(),notify(),notifyAll()都要用在同步中,因为要对持有监视器(锁)的线程操作。
      • wait(),notify(),notifyAll()都是Object类中的方法,因为这些方法在操作线程时,都必须要标识他们所操作线程持有的锁。
        一个被等待的线程,只能被持有同一个锁的线程唤醒,也就是说,等待和唤醒必须是同一个锁。
        为锁可以是任意对象,所以可以被任意对象调用的方法定义在Object类中。
      • wait和sleep比较:
        • wait():释放cpu执行权,释放锁。
        • sleep():释放cpu执行权,不释放锁。到达休眠时间后线程将继续执行,直到完成。若在休眠期另一线程中断该线程,则该线程退出。

    • 生产者消费者例子

      • 需求:生产者和消费者分别创建两个线程,保证生产者进程和消费者进程能够交替执行。
      • 代码:
        package cn.itcast.heima;public class Test {public static void main(String[] args) {Resource r = new Resource();//商品Producer pro = new Producer(r);//生产商品Consumer con = new Consumer(r);//消费商品Thread t1 = new Thread(pro);//生产线程Thread t2 = new Thread(pro);//生产线程Thread t3 = new Thread(con);//消费线程Thread t4 = new Thread(con);//消费线程t1.start();t2.start();t3.start();t4.start();}}//商品类class Resource{private String name;//商品名称private int count = 1;//商品编号private boolean flag = false;//判断商品是否还在//生产商品public synchronized void set(String name){//如果商品还在,生产线程等待while (flag) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}this.name = name + "---" + count++;System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);flag = true;//商品生产出来了notifyAll();//唤醒所有等待线程}//消费商品public synchronized void out(){//如果商品没了,消费线程等待while (!flag) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "...消费者......" + this.name);flag = false;//商品被拿走了notifyAll();//唤醒所有等待线程}}//生产者类class Producer implements Runnable{private Resource res;public Producer(Resource res) {this.res = res;}@Overridepublic void run() {while (true) {res.set("商品");}}}//消费者类class Consumer implements Runnable{private Resource res;public Consumer(Resource res) {this.res = res;}@Overridepublic void run() {while (true) {res.out();}}}
        打印结果:
      • 需要定义while判断标记flag:
        • 原因:让被唤醒的线程再一次判断标记,否则会导致所有线程都在等待。
      • 并且为了避免线程全部冻结,需要在解锁之前唤醒所有线程:
        • 原因:需要唤醒对方线程。因为只用notify()容易出现只唤醒本方线程的情况,导致程序中所有线程都挂起。

    • JDK5.0升级版

      • 定义了Lock接口,代替了同步函数和同步代码块。
      • 定义了Condition接口,代替了Object中的wait(),notify(),notifyAll()方法。Condition对象可以通过Lock接口子类的newCondition()方法获取。
      • 一个Lock可以对应多个Condition对象,该方式可以对指定的某个Condition对象对应的线程进行等待和唤醒处理,而不需要唤醒所有线程。
      • 生产者消费者例子的JDK5.0升级版代码:
        package cn.itcast.heima;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Test {public static void main(String[] args) {Resource r = new Resource();//商品Producer pro = new Producer(r);//生产商品Consumer con = new Consumer(r);//消费商品Thread t1 = new Thread(pro);//生产线程Thread t2 = new Thread(pro);//生产线程Thread t3 = new Thread(con);//消费线程Thread t4 = new Thread(con);//消费线程t1.start();t2.start();t3.start();t4.start();}}//商品类class Resource{private String name;//商品名称private int count = 1;//商品编号private boolean flag = false;//判断商品是否还在private Lock lock = new ReentrantLock();//定义锁对象private Condition condition_pro = lock.newCondition();//定义生产线程的Condition对象private Condition condition_con = lock.newCondition();//定义消费线程的Condition对象//生产商品public void set(String name){try{lock.lock();//上锁//如果商品还在,生产线程等待while (flag) {try {condition_pro.await();//生产线程等待} catch (InterruptedException e) {e.printStackTrace();}}this.name = name + "---" + count++;System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);flag = true;//商品生产出来了condition_con.signal();//唤醒消费线程}finally{lock.unlock();//解锁}}//消费商品public void out(){try {lock.lock();//上锁//如果商品没了,消费线程等待while (!flag) {try {condition_con.await();//消费线程等待} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName()+ "...消费者......" + this.name);flag = false;//商品被拿走了condition_pro.signal();//唤醒生产线程} finally {lock.unlock();//解锁}}}//生产者类class Producer implements Runnable{private Resource res;public Producer(Resource res) {this.res = res;}@Overridepublic void run() {while (true) {res.set("商品");}}}//消费者类class Consumer implements Runnable{private Resource res;public Consumer(Resource res) {this.res = res;}@Overridepublic void run() {while (true) {res.out();}}}
        打印结果:

  • 线程生命控制

    • 停止线程

      • stop()方法已经过时。
      • 如何停止线程:
        • 只有一种方法,run()方法结束。
        • 开启多线程运行,运行代码通常都是循环结构。只要控制住循环,就可以结束run()方法,从而结束线程。
      • Thread类提供的interrupt()方法,可以中断线程的冻结状态。
        当没有指定的方式让冻结的线程恢复到运行状态时,就需要对冻结状态进行清除。强制线程恢复到运行状态,并在异常处理中通过改变标记让线程结束。

    • 守护线程

      • 守护线程相当于后台线程,当前台线程全部结束后,后台线程自动结束,JVM退出。
      • 如果一个线程的运行依赖于另一个线程的运行,当第一个线程结束时,希望第二个线程也随之结束,则第二个线程可以标记为守护线程。
      • 设置守护线程的方法:
        • 在线程开启前调用setDaemon(true)方法。

    • 联合线程

      • 当A线程执行到了B线程的join()方法时,A就会等待,直到B线程都执行完,A线程才会继续执行。
      • join()方法可以用来临时加入线程执行。

    • 优先级和yield方法

      • 优先级
        • 线程被哪个线程开启,就属于哪个线程组。
        • 线程默认优先级为5。优先级取值范围1-10。
        • Thread类的toString()方法返回线程名称、优先级和线程组。
        • 优先级代表抢占cpu的频率。
        • setPriority方法:改变线程优先级。
      • yield方法
        • yield方法:暂停当前正在执行的线程对象,并执行其他线程。
        • 可以实现当前线程执行完后让出CPU执行权,达到不同线程交替执行的效果。