day19 多线程

多线程

1. 进程

进行中的应用程序,应用程序的执行实例,有独立的内存空间和系统资源。

2.线程

CPU调度和分派的基本单位,应用程序运算的最小单位

3. 进程和线程的关系

线程是包含在进程之中的,一个进程至少包含一个线程,否则将无法运行

举例:进程和线程的关系,就像汽车的车身和车轮 ,车轮不是越多越好,要根据实际的硬件环境来决定

即:线程不是越多越好,要根据实际的硬件环境来决定

4. 线程的执行

在单核心CPU下,多个线程轮流交替执行,轮流占用CPU资源,而非同时执行

多个线程轮流交替执行,每个任务最多执行20ms,当前任务准备就绪之后,向CPU发出准备就绪信息,CPU是否执行,不确定。

5. 并发和并行

并发:同时发生,轮流交替执行

并行:真正意义上的同时执行

6. 创建线程的方式

6.1 继承Thread类

创建线程方式1:继承Thread类 重写 run方法 调用start方法 开启新线程 并且执行

面试题:调用start方法 和 调用 ru方法的区别?

调用start方法会开启新的线程

调用run方法不会开启新的线程

package com.atguigu.test1;
/**
 * 创建线程方式1:继承Thread类 重写 run方法 调用start方法 开启新线程 并执行
 *
 * 面试题:调用start方法 和 调用run方法的区别?
 *      调用start方法会开启新的线程
 *      调用run方法不会开启新的线程
 * */
public class Test2 extends Thread{
    @Override
    public void run() {
        for (int i = 0;i < 10;i++){
            System.out.println(Thread.currentThread().getName() + "线程执行:i的值为:" + i);
        }
    }

    public static void main(String[] args) {
        Test2 th1 = new Test2();

        // 设置th1的线程名
        th1.setName("A");
        th1.start();

        Test2 th2 = new Test2();

        // 设置th2的线程名
        th2.setName("B");
        th2.start();
    }
}

6.2 实现Runnable接口

创建线程方式2:实现Runnable接口 重写 run方法

package com.atguigu.test1;
/**
 * 创建线程方式2:实现Runnable接口 重写 run方法
 * */
public class Test3 implements Runnable{
    @Override
    public void run() {
        for (int i = 1;i <= 10; i++){
            System.out.println(Thread.currentThread().getName() + "===" + i);
        }
    }

    public static void main(String[] args) {
        Test3 th1 = new Test3();

        Thread t1 = new Thread(th1);

        t1.setName("线程A");
        t1.start();

        Thread t2 = new Thread(th1);
        t2.setName("线程B");
        t2.start();
    }
}
6.3 Callable接口

三种线程创建方式区别?

继承Thread 实现 Runnable接口 实现Callable接口

前两种方式没有返回值 实现Callable方式可以有返回值

前两种方式不能声明异常 因为是重写父类的方法 父类没有声明 子类也不能声明

实现Callable方式可以声明异常

Callable实现类可以作为参数传入到 FutureTask 构造方法中

FutureTask实现了 RunnableFuture接口

RunnableFuture 继承自 Runnable

package com.atguigu.test6;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 三种线程创建方式区别?
 *      继承Thread    实现Runnable接口    实现Callable接口
 *      前两种方式没有返回值 实现Callable方式可以有返回值
 *      前两种方式不能声明异常 因为是重写父类的方法 父类没有声明 子类也不能声明
 *      实现Callable方式可以声明异常
 *
 * Callable实现类可以作为参数传入到 FutureTask 构造方法中
 * FutureTask实现了 runnableFuture接口
 * RunnableFuture 继承自 Runnable
 *
 * */
public class TestCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName());  // 线程A
        return 100;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建Callable实现类对象
        TestCallable testCallable = new TestCallable();

        // 将实现类对象testCallable当作参数传入到FutureTask构造方法中
        FutureTask<Integer> task = new FutureTask<>(testCallable);

        // 将task当作参数传给Thread类
        Thread th1 = new Thread(task, "线程A");

        // 执行线程
        th1.start();

        // 获取call()方法中的返回值
        System.out.println(task.get());  // 100
    }
}

6.4 线程池

线程池:将多个线程统一管理的容器

为什么之前创建线程的方式有哪些不好的地方?

1.之前创建的线程在多任务情况下 需要频繁的创建 回收 浪费系统资源

2.之前创建的线程不能定时执行某些任务

3.之前创建的线程都属于独立的资源 无法进行统一管理

以上问题 线程池都能解决

newCachedThreadPool() 根据任务情况创建多个线程的线程池

newFixedThreadPool(int nums) 创建指定个数线程的线程池

newScheduledThreadPool(int corePoolSize) 创建指定个数 可以定时执行的线程池

newSingleThreadExecutor() 创建单个线程的线程池

int corePoolSize 核心线程数

int maximumPoolSize 最大线程数

long keepAliveTime 空闲时间 表示空闲多久之后回收线程

TimeUnit unit 时间单位 秒 毫秒 纳秒

execute方法和 submit方法的区别?

execute方法只能接收Runnable实现类

submit方法可以接收Runnable实现类 和 Callable实现类

submit底层依然调用execute方法

线程池相关内容都在JUC包

package com.atguigu.test7;

import java.util.concurrent.*;

/**
 * 线程池:将多个线程统一管理的容器
 * 为什么之前创建线程的方式有哪些不好的地方?
 *      1.之前创建的线程在多任务情况下 需要频繁的创建 回收 浪费系统资源
 *      2.之前创建的线程不能定时执行某些任务
 *      3.之前创建的线程都属于独立的资源 无法进行统一管理
 *
 * 以上问题 线程池都能解决
 *
 * newCacheThreadPool() 根据任务情况创建多个线程的线程池
 * newFixedThreadPoo(int nums) 创建指定个数线程的线程池
 * newScheduledThreadPool(int corePoolSize) 创建指定个数 可以定时执行的线程池
 * newSingleThreadExecutor() 创建单个线程的线程池
 *      int corePoolSize 核心线程数
 *      int maximumPoolSize 最大线程数
 *      long keepAliveTime 空闲时间 表示空闲多久之后回收线程
 *      TimeUnit unit 时间单位 秒、毫秒、纳秒等
 *
 * execute方法和submit方法的区别?
 * execute方法只能接收Runnable实现类
 * submit方法可以接收Runnable实现类 和 Callable实现类
 * submit底层依然调用execute方法
 *
 * 线程池相关内容都在JUC包
 *
 * */
public class Test1 {
    public static void main(String[] args) {
        // 会等待60s后才运行结束
        Executor es1 = Executors.newCachedThreadPool();

        es1.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "hello CachedThreadPool");
            }
        });


        ExecutorService es2 = Executors.newCachedThreadPool();
        es2.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "hello CachedThreadPool");
            }
        });

        // 指定线程数量为10的线程池
        ExecutorService es3 = Executors.newFixedThreadPool(10);
        es3.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "hello FixedThreadPool");
            }
        });

        // 创建指定数量,定时执行的线程池
        ScheduledExecutorService es4 = Executors.newScheduledThreadPool(10);
        es4.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 5秒之后执行");
            }
        }, 5, TimeUnit.SECONDS);  // 5秒后执行

        // 创建单个线程的线程池
        ExecutorService es5 = Executors.newSingleThreadExecutor();
        es5.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "hello SingleThreadExecutor");
            }
        });
    }
}

7. 两种方式对比

继承Thread类
​ 编写简单,可直接操作线程
​ 适用于单继承

实现Runnable接口
​ 避免单继承局限性
​ 便于共享资源

8. 线程的状态

线程的状态:创建 就绪 运行 阻塞 死亡

package com.atguigu.test2;
/**
 * 线程的状态:创建 就绪 运行 阻塞 死亡
 * */
public class Test1 extends Thread{

    @Override
    public void run() {  // 运行状态
        System.out.println("run方法开始运行");
        try {
            Thread.sleep(3000);  // 阻塞状态
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName());
    }
    // 死亡

    public static void main(String[] args) {
        Test1 th1 = new Test1();  // 创建状态

        th1.start();  // 就绪状态
    }

}

9. 线程的优先级

线程的优先级:默认为5 可设置 1 ~ 10 优先级高的线程获取CPU资源的概率大 并不能保证一定会优先执行

setPriority(int num) 设置优先级

getPriority() 获取优先级

package com.atguigu.test2;
/**
 * 线程的优先级:默认为5 可设置为 1 ~ 10 优先级高的线程获得CPU资源的概率大,不能保证一定会执行
 *
 * setPriority(int num) 设置优先级
 * getPriority() 获取优先级
 * */
public class Test2 extends Thread{
    @Override
    public void run() {
        for (int i = 0;i < 10;i++){
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }

    public static void main(String[] args) {
        Test2 th1 = new Test2();
        th1.setName("线程A");

        th1.setPriority(MAX_PRIORITY);  // 也可以直接写 1~10之间的数字

        // 获取th1的优先级
        System.out.println(th1.getPriority());  // 10
        th1.start();

        Test2 th2 = new Test2();
        th2.setName("线程B");

        th2.setPriority(Thread.MIN_PRIORITY);
        System.out.println(th2.getPriority());  // 1

        th2.start();

    }
}

10. 线程的插队

线程插队

join() 等待插队线程执行完毕

join(long millis) 等待插队线程指定时间 毫秒

join(long millis,int nanos) 等待插队线程指定时间 毫秒 + 纳秒

package com.atguigu.test2;
/**
 * 线程插队
 * join() 等待插队线程执行完毕
 * join(long millis) 等待插队线程指定时间  单位:毫秒
 * join(long millis, int nanos)  等待插队线程指定时间  单位:毫秒 + 纳秒
 * */
public class Test3 extends Thread{
    @Override
    public void run() {
        for (int i = 0;i < 20;i++){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test3 th1 = new Test3();
        th1.setName("子线程A");
        th1.start();

        for (int i = 0;i < 50;i++){
            if (i == 10){
                th1.join();
            }
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}

11. 线程的礼让

线程的礼让 : 暂停当前线程 礼让其他线程执行 只是提供一种可能 并不能保证一定为礼让

join和yield方法的区别?

join方法表示插队 一定会优先让插队线程执行

yield方法表示礼让 不一定会礼让

yield()

package com.atguigu.test2;
/**
 * 线程的礼让:暂停当前线程 礼让其他线程执行 只是提供一种可能 并不能保证一定会礼让
 *
 * join和yield方法的区别?
 * join方法表示插队 一定会优先让插队线程执行
 * yield方法表示礼让 不一定会礼让
 * */
public class Test4 extends Thread{
    @Override
    public void run() {
        for (int i = 1;i <= 10;i++){
            if (i == 5){
                Thread.yield();  // 让CPU先执行别的线程,CPU可能会忽略
                System.out.println(Thread.currentThread().getName() + "线程礼让");
            }
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }

    public static void main(String[] args) {
        Test4 th1 = new Test4();
        Test4 th2 = new Test4();

        th1.setName("线程A");
        th2.setName("线程B");

        th1.start();
        th2.start();
    }
}

12. 线程练习题

12.1 模拟爬山

使用多线程模拟多个人爬山

分析:在ClimbThread类中添加三个属性,分别为总长度,线程名称,每爬100米耗时,要求调用者在构造方法中完成属性初始化

package com.atguigu.test3;
/**
 * 使用多线程模拟多个人爬山
 * */

public class ClimbThread extends Thread{
    @Override
    public void run() {
        while (length > 0){
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            length -= 100;
            System.out.println(Thread.currentThread().getName() + "爬了100米,还剩" + length + "米!");
        }
        System.out.println(Thread.currentThread().getName() + "到达终点!");
    }

    private int time;  // 爬100米的耗时
    private int length;  // 总长度

    public ClimbThread(int time, int length) {
        this.time = time;
        this.length = length;
    }


    public static void main(String[] args) {
        ClimbThread youngMan = new ClimbThread(500, 1000);   // 创建年轻人对象
        ClimbThread oldMan = new ClimbThread(1000, 1000);  // 创建老年人对象

        youngMan.setName("练习两年半的练习生");  // 年轻人线程设置线程名
        oldMan.setName("去成华大道的老年人");  // 老年人线程设置线程名

        youngMan.start();  // 执行年轻人线程
        oldMan.start();  // 执行老年人线程
    }
}

12.2 模拟看病

某科室一天需看普通号50个,特需号10个

特需号看病时间是普通号的2倍

开始时普通号和特需号并行叫号,叫到特需号的概率比普通号高

当普通号叫完第10号时,要求先看完全部特需号,再看普通号

使用多线程模拟这一过程

使用子线程表示特需号,主线程表示普通号,分别休眠不同的时间代表不同的耗时,优先级设置不同表示叫号概率不同。线程插队来实现‘当普通号叫完第10号时,要求先看完全部特需号’

package com.atguigu.test3;
/**
 *  某科室一天需看普通号50个,特需号10个
 *  特需号看病时间是普通号的2倍
 *  开始时普通号和特需号并行叫号,叫到特需号的概率比普通号高
 *  当普通号叫完第10号时,要求先看完全部特需号,再看普通号
 *  使用多线程模拟这一过程
 *
 * */
public class Special extends Thread{

    @Override
    public void run() {
        for (int i = 0;i < 10;i++){
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + i + "号病人在看病");
        }
        System.out.println(Thread.currentThread().getName() + "看完了");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread main = Thread.currentThread();
        Special th1 = new Special();  // 普通号10 看病时间1000毫秒
        main.setName("普通号");
        th1.setName("特需号");
        th1.setPriority(MAX_PRIORITY);  // 提高特需号的优先级
        th1.start();
        for (int i = 0;i < 50;i++){
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName() + i + "号病人在看病");
            if (i == 10){
                th1.join();
            }
        }
    }
}

13. 同步关键字synchronized

13.1 修饰代码块

使用多线程模拟多人抢票

版本1:

ticketCount定义为实例级别 将会售出30张票 因为每new一个对象 就存在一份

将ticketCount使用static修饰 依然存在问题 因为同一个循环条件下 可能会进入多个线程

package com.atguigu.test4;
/**
 * 使用多线程模拟多人抢票
 *
 * 版本1:
 *      ticketCount定义为实例级别,将会售出30张票 因为每new一个对象 就存在一份
 *      将ticketCount使用static修饰 依然存在问题 因为同一个循环条件下 可能会进入多个线程
 * */
public class Ticket1 extends Thread{
    static int ticketCount = 10;

    @Override
    public void run() {
        while (ticketCount > 0){
            try {
                sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketCount--;
            System.out.println(Thread.currentThread().getName() + "抢到了第" + (10 - ticketCount) + "张票,还剩余" + ticketCount + "张票");
        }
    }

    public static void main(String[] args) {
        Ticket1 zs = new Ticket1();
        zs.setName("赵四");
        zs.start();

        Ticket1 gk = new Ticket1();
        gk.setName("广坤");
        gk.start();

        Ticket1 xb = new Ticket1();
        xb.setName("小宝");
        xb.start();
    }
}

使用多线程模拟多人抢票

版本2:

将当前类实现为 Runnable实现类

将当前类修改为 Runnable实现类 依然无法保证数据安全性

我们可以通过上锁来实现 通过synchronized关键字实现

synchronized关键字可以用于

修饰代码块 表示同一时间只能有一个线程访问此代码块

修饰方法 表示同一时间只能有一个线程访问此方法

同步代码块中 通常都书写this 表示锁定当前对象 也可以不写this 但是需要 互斥效果/上锁效果 必须保证多个线程

访问的 竞争的是同一个资源才可以

package com.atguigu.test4;
/**
 * 使用多线程模拟多人抢票
 *
 * 版本2:
 *      将当前类实现为Runnable实现类
 *
 *      将当前类修改为Runnable实现类 依然无法保证数据安全性
 *      我们可以通过上锁来实现 通过synchronized关键字实现
 *
 *      synchronized关键字可以用于
 *          修饰代码块   表示同一时间只能有一个线程访问此代码中的内容
 *          修饰方法     表示同一时间只能有一个线程访问此方法
 *
 *  同步代码块种 通常都书写this 表示锁定当前对象 也可以不写this 但是需要互斥效果/上锁效果 必须保证多个线程访问的时同一个资源才可以
 * */
public class Ticket2 implements Runnable{
    int ticketCount = 10;
    @Override
    public void run() {
        // 使用Runnable实现类仍然出现数据不安全问题
//        while (ticketCount > 0){
//            try {
//                Thread.sleep(500);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            ticketCount--;
//            System.out.println(Thread.currentThread().getName() + "抢到了第" + (10 - ticketCount) + "张票");
//        }

        while (true){
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 使用synchronized关键字,保证数据的安全性
            synchronized (this){
                if (ticketCount == 0){
                    break;
                }
                ticketCount--;
                System.out.println(Thread.currentThread().getName() + "抢到了第" + (10 - ticketCount) + "张票,还剩余" + ticketCount + "张票");
            System.out.println();
            }
        }

    }

    public static void main(String[] args) {
        Ticket2 ticket2 = new Ticket2();
        Thread zs = new Thread(ticket2, "赵四");
        zs.start();

        Thread gk = new Thread(ticket2, "广坤");
        gk.start();

        Thread xb = new Thread(ticket2, "小宝");
        xb.start();
    }
}

13.2 修饰方法

使用多线程模拟多人抢票

使用同步方法实现多人抢票操作

我们所学过的线程安全的类 都是使用同步方法实现的

Vector StringBuffer Hashtable

package com.atguigu.test5;
/**
 * 使用多线程模拟多人抢票
 * 使用同步方法实现多人拉票操作
 *
 * 我们所学过的线程安全的类 都是使用同步方法实现的
 * Vector   StringBuffer    Hashtable
 * */
public class Ticket3 implements Runnable{
    int ticketCount = 10;

    // 使用synchronized修饰方法,保证方法同时只能让一个线程访问
    @Override
    public synchronized void run() {
        while (ticketCount > 0){
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketCount--;
            System.out.println(Thread.currentThread().getName() +
                    "抢到了第" + (10 - ticketCount) + "张票,还剩余" + ticketCount);

            if (ticketCount == 0){
                System.out.println("票售空了");
            }
        }
    }

    public static void main(String[] args) {
        Ticket3 ticket3 = new Ticket3();
        Thread th1 = new Thread(ticket3, "赵四");
        Thread th2 = new Thread(ticket3, "大拿");
        Thread th3 = new Thread(ticket3, "小宝");

        th1.start();
        th2.start();
        th3.start();
    }
}

14. 同步关键字补充

多个并发线程访问同一资源的同步代码块时

同一时刻只能有一个线程进入synchronized(this)同步代码块

当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定

当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非synchronized(this)同步代码

15. 懒汉单例模式完善

之前书写的懒汉单例模式如果在多线程情况下,很有可能会创建多个对象,可以使用同步关键字修饰方法解决

package com.atguigu.test6;
/**
 * 使用synchronized关键字 完善懒汉模式的线程不安全的问题
 * */
public class LazySingleton {
    private static LazySingleton instance = null;

    private LazySingleton(){}

    public static synchronized LazySingleton getInstance(){
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }

}

16. Lock接口和可重入锁

我们可以使用Lock接口实现类来实现上锁的效果

lock() 获得锁

unlock() 释放锁

package com.atguigu.test6;

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

/**
 * 使用多线程模拟多人抢票
 * */
public class Ticket2 implements Runnable{
    int ticketCount = 10;
    Lock lock = new ReentrantLock();


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

            try{
                lock.lock();  // 获得锁
                if (ticketCount == 0){
                    break;
                }
                ticketCount--;
                System.out.println(Thread.currentThread().getName() + "抢到了第"
                        + (10 - ticketCount) + "张票,还剩余" + ticketCount + "张票");

                if (ticketCount == 0){
                    System.out.println("售票结束");
                }

            }catch (Exception e){
                e.printStackTrace();
            }finally {  // 不管是否成功执行还是出现异常,都需要释放锁。不然可能会出现死锁
                lock.unlock();  // 释放锁
            }

        }
    }

    public static void main(String[] args) {
        Ticket2 ticket2 = new Ticket2();
        Thread th1 = new Thread(ticket2, "赵四");
        Thread th2 = new Thread(ticket2, "广坤");
        Thread th3 = new Thread(ticket2, "小宝");

        th1.start();
        th2.start();
        th3.start();
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值