多线程 - 一文读懂多线程【附源码】

(所有源码均在:https://github.com/zongzhec/JavaPractise

目录

程序,进程,和线程

Java要如何开启main以外的线程

继承java.lang.Thread类

步骤

源码

实现java.lang.Runnable接口

步骤

源码

继承Thread类和实现Runnable接口的区别

线程的生命周期

Thread方法

源码:多线程实现龟兔赛跑

线程的安全问题和同步代码块

概述

如何判断?

解决:加锁(同步)

两种形式

线程通信

什么情况需要线程通信?

注意点

源码(一):以“厨师”和“服务员”来举例生产者-消费者模式

源码(二):利用多线程通信实现奇偶数的交替打印

源码(三):利用多线程及其之间的通信实现存钱、取钱的分开操作


多线程

程序,进程,和线程

程序(Program):为了完成某个任务/功能而选择某个编程语言编写的一组指令的集合。
    这组集合是以静态的方式存在于电脑中。
进程(Process):程序的一次运行。进程是操作系统分配资源的最小单位。
    同一个进程是共享同一份内存等资源。不同的进程之间不共享内存资源。
    如果两个进程之间要进行数据交换则比较复杂。可以通过文件,网络通信的方式,成本较高。
线程(Thread):当某个进程㤇同时完成多个功能时,采用的一种方法。
    线程是进程的其中一条执行路径,一个进程至少有一个线程。
    线程是CPU调用资源的最小单位。多个线程之间是由共享内存的。


JVM的运行时内存:方法区、堆、栈(虚拟机栈,本地方法栈),程序计数器。其中堆内存和方法区的内存是共享的,栈和程序计数器是线程间独立的。
    堆:对象
    方法区:类的信息,常量,静态等
    栈:局部变量
JavaApplication中至少有一个main线程,但是后台JVM中海油一些其他的线程:GC,机场的监视和处理,类加载等。


Java要如何开启main以外的线程

1. 继承Thread类
2. 实现Runnable接口


继承java.lang.Thread类

步骤

1. 编写线程类,继承Thread类
2. 重写run方法:这个run()的方法体,就是线程体,就是当前线程要完成的任务代码。
    注意:不能手动调用run(),它不是程序员调用的,线程调度器会自动调用。
3. 创建线程对象
4. 启动线程:调用线程的start()方法

源码

package zongzhe.java_basic.multithread;

public class ThreadDemo {

    public static void main(String args[]) {
        MyThread thread1 = new MyThread("thread 1");
//       thread1.run(); // 不能手动调用run(),它不是程序员调用的,线程调度器会自动调用。
        thread1.start();

        MyThread thread2 = new MyThread("thread 2");
        thread2.start();

        for (int i = 1; i <= 100; i += 2) {
            if (i == 5) {
                try {
                    thread1.join(); // main 线程被thread1 线程加塞,只能等到thread1结束
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("main - 奇数:" + i);
        }


    }

}

class MyThread extends Thread {
    private String name;

    public MyThread(String name) {
        this.name = name;
    }

    // 重写run方法
    @Override
    public void run() {
        for (int i = 2; i <= 100; i += 2) {
            System.out.println(name + " - 偶数:" + i);
        }

    }
}


实现java.lang.Runnable接口

步骤

1. 声明线程类,实现Runnable接口
2. 重写run方法
3. 创建线程对象
4. 启动线程:因为只有Thread类中才有start方法,所以必须通过Thread类的对象才能启动线程

源码

package zongzhe.java_basic.multithread;

public class RunnableDemo {

    public static void main(String args[]) {
        MyRunnable runnable = new MyRunnable("runnable 1");
//        runnable.run(); 错误,这样调用不是多线程
//        runnable.start(); 错误,Runnable中没有start方法

        Thread thread1 = new Thread(runnable); // 因为只有Thread类中才有start方法,所以必须通过Thread类的对象才能启动线程
        thread1.start();
        Thread thread2 = new Thread(runnable);
        thread2.start();
    }

}

class MyRunnable implements Runnable {

    private String name;

    public MyRunnable(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 2; i < 10; i += 2) {
            System.out.println(name + " - 偶数: " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


继承Thread类和实现Runnable接口的区别

1. 共享数据
    Thread 需要使用静态
    Runnable 不需要静态,只要用同一个MyRunnable的对象就可以
2. 选择锁对象时,Runnable可以直接用this对象,比较方便。
3. 继承有单继承限制,而接口没有。

 

线程的生命周期

1. 新建:new出来的,此时和普通的Java对象没有区别。
2. 就绪:当线程启动(start)之后,就会从新建状态到就绪状态,就绪状态即为可以被CPU调度的状态。每一个线程对象只能start一次。
3. 运行:正在被调度的状态。此状态时间非常短暂,调度时间结束就会回到就绪状态,等待下次调度。
    运行状态中的线程有三个去向:
    3.1. 就绪:调度时间结束,回到就绪状态;或者yield()暂停当前线程,让出本次CPU资源,重新加入下次调度队列
    3.2. 死亡:任务结束,或者发生异常
    3.3. 阻塞:(1)遇到了耗时操作,例如键盘输入,网络连接等;(2)遇到sleep或者wait;(3)遇到join加塞;(4)遇到等待锁
4. 阻塞:阻塞状态只能回到就绪状态。
5. 死亡:一旦死亡,便彻底结束


Thread方法

1. 构造器
    Thread()
    Thread(Runnable target)
    Thread(Runnable target, String name)
2. 方法
    (1)getName()/setName(): 默认线程名称:Thread-0,Thread-1...
    (2)Thread.currentThread(): 获取当前线程对象
    (3)setPriority(int newPriority)/getPriority(): 设置或获取线程的优先级。Java线程优先级一共有1-10十个等级。
    (4)sleep(毫秒)
    (5)join:加塞
    (6)yield:暂停当前线程,让出CPU资源
3. 注意
    (1)当main频繁的获取线程的值但是值没有变化的时候,主线程就会停止去主存访问,而是转到缓存访问。解决:加volatile关键字

源码:多线程实现龟兔赛跑

package zongzhe.java_basic.multithread;

/**
 * 多线程的案例:龟兔赛跑
 * 赛跑长度为30米
 * 兔子速度每秒10米,每跑完10米休眠10秒
 * 乌龟速度美妙1米,每跑完10米休眠1秒
 * 要求等兔子和乌龟的线程都结束,主线程(裁判)才能公布最后结果
 */
public class TortoiseHareRacePrac {

    static Racer hare;
    static Racer tortoise;

    public static void main(String[] args) {
        prepare();
        race();

    }

    public static void prepare() {
        hare = new Racer("兔子", 100, 10000);
        tortoise = new Racer("乌龟", 1000, 1000);
    }

    public static void race() {
        hare.start();
        tortoise.start();

        try {
            hare.join();
            tortoise.join(); // 只是阻塞了main
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if (hare.getRunTime() < tortoise.getRunTime()) {
            System.out.println(hare.getName() + "赢了");
        } else if (hare.getRunTime() > tortoise.getRunTime()) {
            System.out.println(tortoise.getName() + "赢了");
        } else {
            System.out.println("平手!");
        }
    }
}

class Racer extends Thread {
    private long timePerMeter; // 跑一米的时间
    private long restTime; // 跑10米的休息时间
    private long runTime = -1; // 跑完全程的时间

    public Racer(String name, long timePerMeter, long restTime) {
        super(name);
        this.timePerMeter = timePerMeter;
        this.restTime = restTime;
    }

    public long getRunTime() {
        return runTime;
    }

    @Override
    public void run() {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i <= 30; i++) {
            try {
                System.out.println(getName() + "已经跑完 " + i + "米");
                Thread.sleep(timePerMeter);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                // 每跑完10米休眠时间
                if (i == 10 || i == 20) {
                    System.out.println(getName() + "大休中");
                    Thread.sleep(restTime);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        long endTime = System.currentTimeMillis();
        runTime = endTime - startTime;
        System.out.println(getName() + "到达终点,用时: " + runTime);
    }

}

class Hare extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 30; i += 10) {
            try {
                // 兔子跑一米需要0.1秒
                System.out.println("兔子已经跑完 " + i + "米");
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                // 兔子每跑完10米休眠10秒
                if (i == 10 || i == 20) {
                    System.out.println("兔子大休中");
                    Thread.sleep(10000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("兔子到达终点");
    }
}

class Tortoise extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 30; i += 1) {
            try {
                // 乌龟跑一米需要1秒
                System.out.println("乌龟已经跑完 " + i + "米");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                // 乌龟每跑完10米休眠1秒
                if (i == 10 || i == 20) {
                    System.out.println("乌龟大休中");
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("乌龟到达终点");
    }
}

 

线程的安全问题和同步代码块

概述

多个线程使用共享的数据,就有线程的安全问题。

如何判断?

1.是否有多个线程使用同一份数据;
2. 有多条语句来操作和访问此数据。

解决:加锁(同步)

1. 同步的锁对象可以使任意类型的对象
2. 使用共享数据的这些线程,使用(承认)同一个锁对象

两种形式

1. 同步代码块:synchronize(同步的对象)
    synchronized(同步的锁对象){
        需要锁起来的代码:一个线程在运行这段代码期间,不想别的线程半路插进来
}
和共享数据相关的语句都要锁起来
2. 同步方法:如果一次任务是在一个方法中完成的,那么就可以直接锁一个方法
    同步方法的锁对象:
        非静态方法:this。需要考量this是否可以做锁对象。
        静态方法:当前类的Class对象。每一个类型被加载到内存后都会生成一个Class对象来表示这个类型。只要是同一个类型,Class的对象就是同一个。

 

线程通信

什么情况需要线程通信?

——当遇到了“生产者-消费者”问题时,需要用到线程通信。
    当缓冲区满了的时候,生产者就需要停下来。等消费者取走数据,就可以重新唤醒/通知生产者继续生产。反之亦然。
    线程通信就是用wait和notify/notifyAll方法。
    wait和notify/notifyAll操作对象必须是“同步锁”对象。

注意点

    1. 如果有多个生产者和消费者,记得使用notifyAll(),因为notify()并不能保证唤醒的是另一方。
    2. 被唤醒的线程需要重新判断,因为如果是己方唤醒的,很有可能已经没有资源或者资源溢出了。

源码(一):以“厨师”和“服务员”来举例生产者-消费者模式

package zongzhe.java_basic.multithread;

/**
 * 本演示使用“厨师”-“服务员”来扮演生产者和消费者。
 * 出菜台表示数据的缓冲区域
 */
public class CommunicateDemo {
    public static void main(String[] args) {
        WorkBench wb = new WorkBench();
        Cook c1 = new Cook(wb);
        Cook c2 = new Cook(wb);
        Waiter w1 = new Waiter(wb);
        Waiter w2 = new Waiter(wb);
        Waiter w3 = new Waiter(wb);

        c1.start();
        c2.start();
        w1.start();
        w2.start();
        w3.start();
    }
}

class Cook extends Thread {
    private WorkBench wb;

    public Cook(WorkBench wb) { // wb显然是不能new一个的,否则厨师和服务员将在不同的平台工作。
        super();
        this.wb = wb;
    }

    @Override
    public void run() {
        while (true) {
            wb.put();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

class Waiter extends Thread {
    private WorkBench wb;

    public Waiter(WorkBench wb) { // wb显然是不能new一个的,否则厨师和服务员将在不同的平台工作。
        super();
        this.wb = wb;
    }

    @Override
    public void run() {
        while (true) {
            wb.take();
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

// 工作台:出菜的窗口平台
class WorkBench {
    private static final int MAX_VALUE = 10;
    private int num; // 工作台上菜的数量

    public synchronized void take() {
        while (num <= 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num--;
        System.out.println("服务员取菜,剩余:" + num);
        this.notifyAll(); // 通知在wait的线程,从阻塞状态回到就绪状态,但是注意不一定会唤醒“厨师”线程
    }

    public synchronized void put() {
        while (num >= MAX_VALUE) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num++;
        System.out.println("厨师做好了菜,剩余:" + num);
        this.notifyAll();
    }
}

源码(二):利用多线程通信实现奇偶数的交替打印

package zongzhe.java_basic.multithread;

/**
 * 要求:创建两个线程,一个打印奇数,一个打印偶数,保证交替打印。
 */
public class OddEvenPrac {

    public static void main(String[] args) {
        NumPrinter np = new NumPrinter();
        OddPriter oddPrinter = new OddPriter(np);
        EvenPrinter evenPrinter = new EvenPrinter(np);

        oddPrinter.start();
        evenPrinter.start();
    }

}

class NumPrinter {
    private static int i = 0;
    private static Object lock = new Object();

    public NumPrinter() {
    }

    public static void count(String name) {
        synchronized (lock) {
            if ((name.equals("odd") && i % 2 == 0) || (name.equals("even") && i % 2 != 0)) {
                try {
                    lock.notify();
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                System.out.println("counting " + name + ": " + i);
                i++;
            }
        }
    }
}

class OddPriter extends Thread {
    private NumPrinter np;

    public OddPriter(NumPrinter np) {
        super();
        this.np = np;
    }

    @Override
    public void run() {
        System.out.println("odd starting");
        while (true) {
            NumPrinter.count("odd");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class EvenPrinter extends Thread {

    private NumPrinter np;

    public EvenPrinter(NumPrinter np) {
        super();
        this.np = np;
    }

    @Override
    public void run() {
        System.out.println("even starting");
        while (true) {
            NumPrinter.count("even");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

源码(三):利用多线程及其之间的通信实现存钱、取钱的分开操作

package zongzhe.java_basic.multithread;

/**
 * 练习:实现一个银行账户,由丈夫和妻子管理。
 * 丈夫负责存钱,妻子负责取钱。
 */
public class BankAccountPrac {

    public static void main(String[] args) {
        Husband husband = new Husband();
        Wife wife = new Wife();

        husband.start();
        wife.start();
    }
}

class BankAccount {
    private static int balance = 0;
    private static Object accounLock = new Object();

    public static void saveMoney(int amount) {
        synchronized (accounLock) {
            balance += amount;
            System.out.println("存入 " + amount + ", 余额: " + balance);
            accounLock.notify();
        }
    }

    public static void withdrawMoney(int amount) {
        synchronized (accounLock) {
            if (amount > balance) {
                System.out.println("准备取" + amount + ",但是余额只有" + balance + ",叫你丈夫打钱。");
                try {
                    accounLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                balance -= amount;
                System.out.println("取出 " + amount + ", 余额: " + balance);
            }

        }
    }
}

class Husband extends Thread {

    @Override
    public void run() {
        while (true) {
            int saveAmount = (int) (Math.random() * 50);
            BankAccount.saveMoney(saveAmount);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Wife extends Thread {

    @Override
    public void run() {
        while (true) {
            int withdrawAmount = (int) (Math.random() * 100);
            BankAccount.withdrawMoney(withdrawAmount);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值