黑马程序员-多线程

本文详细介绍了Java中多线程的基础概念、创建方法、线程安全及死锁问题的解决方案,同时还探讨了线程间的通信机制及JDK1.5的新特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

——- android培训java培训、期待与您交流! ———-

1.1 进程和线程

进程:正在进行中的程序(直译)。
线程:进程中一个负责程序执行的控制单元(执行路径)。

  • 一个进程中可以有多个执行路径,称之为多线程。
  • 一个进程中至少要有一个线程。
  • 开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容

    P.S.一个进程中有多个线程,称为多线程。
    

    思考:jvm启动是单线程还是多线程?
    java的虚拟机jvm启动的是多线程,jvm启动至少有两个线程,一个执行java程序,一个执行垃圾回收。所以是多线程。如果只有一个线程,则很可能发生内存泄露。

1.2 多线程的优缺点

  • 好处 : 解决了多部分同时运行的问题,提高效率
  • 弊端 : 线程太多,会导致效率的降低 ,这是因为多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低。

    P.S. System类的gc方法告诉垃圾回收器调用finalize方法,但不一定立即执行
    

1.3 创建线程的两种方式

1.3.1 继承Thread类

 package com.test.blog9;

/*
 *多线程实现方式一:继承Thread类
 1.定义一个类继承Thread类。
 2.覆盖Thread类中的run方法。
 3.直接创建Thread的子类对象创建线程。
 4.调用start方法开启线程并调用线程的任务run方法执行。
 * */
public class ThreadDemo extends Thread {
    public static void main(String[] args) {
        // 定义两个线程类
        ThreadDemo td1 = new ThreadDemo();
        ThreadDemo td2 = new ThreadDemo();

        // 启动线程,观察执行顺序
        td1.start();
        td2.start();
    }

    // 复写run方法
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 输出线程名和打印的数字
            System.out.println("线程名:" + Thread.currentThread().getName()
                    + ">>>" + i);
        }
    }

}

运行结果:
这里写图片描述
可以看到两个线程是交互执行的顺序。

1.3.2 实现Runnable接口

package com.test.blog9;

/*
 *多线程实现方式二:实现Runnable接口
 1. 定义类实现Runnable接口。
 2. 覆盖接口中的run方法,将线程的任务代码封装到run方法中。
 3. 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传
 递。为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明
 确要运行的任务。
 4. 调用线程对象的start方法开启线程。
 * */
public class ThreadDemo2 {

    public static void main(String[] args) {
        // 定义两个线程,将实现runnable接口的类传入构造函数中
        RunnableDemo rd = new RunnableDemo();
        Thread t1 = new Thread(rd);
        Thread t2 = new Thread(rd);
        // 启动线程
        t1.start();
        t2.start();
    }
}

class RunnableDemo implements Runnable {
    // 实现run方法
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 输出线程名和打印的数字
            System.out.println("线程名:" + Thread.currentThread().getName()
                    + ">>>" + i);
        }

    }
}

运行结果:

这里写图片描述

1.4 线程安全问题

模拟买票程序,使用4个线程同时卖票:

package com.test.blog9;

public class TicketDemo implements Runnable {
    private int num = 100;// 票数100

    @Override
    public void run() {
        while (true) {
            if (num > 0) {
                try {
                    // 让当前线程睡眠一段时间
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ">卖了第..."
                        + num-- + " 张票");
            }else{
                break;
            }
        }
    }

    public static void main(String[] args) {
        TicketDemo td = new TicketDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        Thread t3 = new Thread(td);
        Thread t4 = new Thread(td);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

运行结果:
这里写图片描述

从上图看到 ,显示有负数的票张。正常到0后应该跳出循环不在卖票的,这是因为多个线程同时执行,很有可能会出现安全问题,出现安全问题的原因就是:
1.多个线程在操作共享的数据。
2.操作共享数据的线程代码有多条。

那么该如何解决呢?

在Java中,需要使用同步代码块加锁,将操作共享数据的部分锁起来,只有等一个线程执行完毕,另一个线程才能参与这部分代码的执行

加入同步锁代码块优化后:

package com.test.blog9;

public class TicketDemo implements Runnable {
    private int num = 100;// 票数100
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            // 加入同步锁
            synchronized (obj) {
                if (num > 0) {
                    try {
                        // 让当前线程睡眠一段时间
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + ">卖了第..." + num-- + " 张票");
                } else {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) {
        TicketDemo td = new TicketDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        Thread t3 = new Thread(td);
        Thread t4 = new Thread(td);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

运行结果:
这里写图片描述

线程安全需要注意地方:

  • 锁的对象可以是任意,但必须是同一个,否则无效
  • 必须对共享操作的数据加锁

    P.S. 静态的同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class
    表示。

同步synchronized加到函数方法前也可以实现同步,称为同步函数,那么同步函数和同步代码块的区别:
1.同步函数的锁是固定的this。
2.同步代码块的锁是任意的对象。
建议使用同步代码块。

1.5 死锁问题

例子:

package com.test.blog9;

public class DeadLockThreadDemo {
    public static void main(String[] args) {
        DeadLock d1 = new DeadLock(true);
        DeadLock d2 = new DeadLock(false);
        Thread t1 = new Thread(d1);
        Thread t2 = new Thread(d2);
        t1.start();
        t2.start();
    }

}

class DeadLock implements Runnable {
    private boolean flag = false;
    static A a = new A();
    static B b = new B();

    public DeadLock(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            while (true) {
                synchronized (a) {
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "需要A锁才能执行");
                    synchronized (b) {
                        System.out
                                .println("线程"
                                        + Thread.currentThread().getName()
                                        + "需要B锁才能执行");
                    }
                }
            }
        } else {
            while (true) {
                synchronized (b) {
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "需要B锁才能执行");
                    synchronized (a) {
                        System.out
                                .println("线程"
                                        + Thread.currentThread().getName()
                                        + "需要A锁才能执行");
                    }
                }
            }
        }
    }
}

class A {
    // 定义死锁对象参数
}

class B {
    // 定义死锁对象参数
}

运行结果:

这里写图片描述

可以看到程序仿佛”死到”这里不在进行,这是因为两个线程都需要对方手里的资源对象才能进行。线程0有B锁对象,需要A锁对象;线程1有A锁对象,需要B锁对象,都在等待对方释放锁对象,都不在执行,假死现象发生。

P.S.实际开发中,要避免死锁的发生

1.6 线程的通信

等待/唤醒机制涉及的方法:
1. wait():让线程处于冻结状态,被wait的线程会被存储到线程池中。
2. notify():唤醒线程池中的一个线程(任何一个都有可能)。
3. notifyAll():唤醒线程池中的所有线程。

注意:这些方法都是改变线程状态的方法,所以都必须定义在同步中,要明确操作的是哪个锁上的线程

P.S. 在同步代码块中, 使用锁对象的wait()方法可以让当前线程等待, 直到有其他线程唤醒为止.使用锁对象的notify()方法可以唤醒一个等待的线程,或者notifyAll唤醒所有等待的线程.

生产者消费者举例:

package com.test.blog9;

public class ProducerConsumerDemo {
    public static void main(String[] args) {
        Resource r = new Resource();
        Producer p = new Producer(r);
        Consumer c = new Consumer(r);
        // 定义两个生产者线程
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(p);
        // 定义两个消费者线程
        Thread t3 = new Thread(c);
        Thread t4 = new Thread(c);
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

}

/*
 * 操作的资源类 包含生产和消费两个方法
 */
class Resource {

    private String name;// 资源名称
    private int count = 1;// 资源的计数器
    private boolean flag = false;// 循环的标志.true代表已经生产完毕,需要消费;false代表消费完毕,需要生产

    // 生产方法
    public synchronized void set(String name) {
        while (flag) {// 注意此处必须使用while循环,使用if只能判断一次,数据会发生错乱
            try {
                // 已经生产了,线程等待
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name = name + count;
        count++;
        System.out.println("生产者生产 >>" + this.name);
        flag = true;// 完成生产,将标志改为true;
        notifyAll();// 唤醒线程,使消费者可以消费(不能使用notify().因为可能唤醒的还是生产线程,那就全部进入等待状态了)
    }

    // 消费方法
    public synchronized void pull() {
        while (!flag) {// 注意此处必须使用while循环,使用if只能判断一次,数据会发生错乱
            try {
                this.wait();// 消费完毕,线程等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        flag = false;// 更改标志
        notifyAll();// 唤醒线程,使生产者可以生产(不能使用notify,因为唤醒的可能还是消费者线程,那就会全部进入等待装改)
        System.out.println("消费者消费>>" + this.name);
    }
}

// 有多个生产者,实现多线程
class Producer implements Runnable {
    private Resource r;// 指名生产者生产的对象

    public Producer(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.set("馒头");// 生产者一直生产
        }

    }

}

// 有多个消费者,实现多线程
class Consumer implements Runnable {
    private Resource r;// 指定消费的对象

    public Consumer(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        // 消费者一直消费
        while (true) {
            r.pull();
        }
    }
}

运行结果:
这里写图片描述

可以看到生产者和消费者是分别执行的,没有发生数据错乱的现象

1.7 JDK1.5新特性(多线程新的实现方式)

这里写图片描述

这里写图片描述

Lock接口更能替换synchronized 功能

使用Lock实现生产者消费者问题:

package com.test.blog9;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerConsumerDemoByLock {
    public static void main(String[] args) {
        Resource r = new Resource();
        Producer p = new Producer(r);
        Consumer c = new Consumer(r);
        // 定义两个生产者线程
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(p);
        // 定义两个消费者线程
        Thread t3 = new Thread(c);
        Thread t4 = new Thread(c);
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

}

/*
 * 操作的资源类 包含生产和消费两个方法
 */
class Resource {

    private String name;// 资源名称
    private int count = 1;// 资源的计数器
    private boolean flag = false;// 循环的标志.true代表已经生产完毕,需要消费;false代表消费完毕,需要生产
    private Lock lock = new ReentrantLock();
    private Condition condition_con = lock.newCondition();// 消费者的condition
    private Condition condition_pro = lock.newCondition();// 生产者的condition

    // 生产方法
    public void set(String name) {
        lock.lock();// 加锁
        try {
            while (flag) {// 注意此处必须使用while循环,使用if只能判断一次,数据会发生错乱
                try {
                    // 已经生产了,线程等待
                    condition_pro.await();// 生产者等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            this.name = name + count;
            count++;
            System.out.println("生产者生产 >>" + this.name);
            flag = true;// 完成生产,将标志改为true;
            condition_con.signal();// 唤醒消费者
        } finally {
            lock.unlock();// 释放
        }

    }

    // 消费方法
    public void pull() {
        lock.lock();// 加锁
        try {
            while (!flag) {// 注意此处必须使用while循环,使用if只能判断一次,数据会发生错乱
                try {
                    condition_con.await();// 消费完毕,消费线程等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            flag = false;// 更改标志
            System.out.println("消费者消费>>" + this.name);
            condition_pro.signal();// 唤醒生产者线程
        } finally {
            lock.unlock();// 释放锁
        }
    }
}

// 有多个生产者,实现多线程
class Producer implements Runnable {
    private Resource r;// 指名生产者生产的对象

    public Producer(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.set("馒头");// 生产者一直生产
        }

    }

}

// 有多个消费者,实现多线程
class Consumer implements Runnable {
    private Resource r;// 指定消费的对象

    public Consumer(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        // 消费者一直消费
        while (true) {
            r.pull();
        }
    }
}

运行结果:
这里写图片描述

注意,原来的wait,notify,notifyAll方法都换成新的
void await() 造成当前线程在接到信号或被中断之前一直处于等待状态。
void signal() 唤醒一个等待线程。
void signalAll() 唤醒所有等待线程。
因为上面三个方法是object里的,condition对象也会有这几个方法,但是在多线程调用的时候如果不使用新的方法会报异常

1.8 线程的其他方法

1.8.1 停止线程

P.S.原来使用stop方法,显示JDK显示该方法已经过时,不在使用

停止线程的方法只有一种,就是run方法结束。如何让run方法结束呢?
开启多 线程运行,运行代码通常是循环体,只要控制住循环,就可以让run方法结束,也就是结束线程。(设置标识符,通过控制标识符控制循环是否结束即可)

    特殊情况:当线程属于冻结状态,就不会读取循环控制标记,则线程就不会结束。为解决该特殊情况,可引入Thread类中的Interrupt方法结束线程的冻结状态;当没有指定的方式让冻结线程恢复到运行状态时,需要对冻结进行清除,强制让线程恢复到运行状态

1.8.2 后台线程

setDaemon(boolean on):将该线程标记为守护线程或者用户线程。

当主线程结束,守护线程自动结束.
当正在运行的线程都是守护线程时,java虚拟机jvm退出;所以该方法必须在启动线程前调用;

守护线程的特点:

  1. 当所有前台线程都结束后,守护线程会自动结束。

1.8.3 join方法

  P.S.用来临时加入线程执行

  当A线程执行到B线程的join方法时,A就会等待B线程都执行完,A才会执行

1.8.3 线程优先级

yield():暂停当前正在执行的线程对象,并执行其他线程
setPriority(int newPriority):更改线程优先级
int getPriority() 返回线程的优先级。
String toString() 返回该线程的字符串表示形式,包括线程名称、优先级和线程组

(1)MAX_PRIORITY:最高优先级(10级)
(1)Min_PRIORITY:最低优先级(1级)
(1)Morm_PRIORITY:默认优先级(5级)

1.9 注意

  • 实现多线程首选实现runnable接口方式
  • run方法是执行任务内容方法,启动线程使用start方法
  • sleep()来自Thread类,wait()来自Object类
  • wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
  • 停止线程使用控制循环中断方法,stop方法已过时
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值