Java笔记 -多线程

概述

依靠 CPU,计算机同时执行多个程序

并行/并发

  • 在同一时间有多个指令(多件事)在 “多个 CPU(多个核心)” 同时 执行叫做并行
  • 在同一时刻,有多个指令在 “单个 CPU 核心” 交替 执行

进程

正在运行的软件

特性

  • 独立性: 进程是一个独立运行的基本单位,同时也是系统分配和调度资源的独立单位
  • 动态性: 进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
  • 并发性: 任何进程都可以同其他进程一起并发执行

线程

是进程中的单个顺序控制流,是一条执行路径

特性

  • 单线程: 一个进程如果只有一条执行路径,则称为单线程程序
  • 多线程: 一个进程如果有多条执行路径,则称为多线程程序

线程状态

  • 新建状态: 创建线程对象
  • 就绪状态: start() 方法
  • 阻塞状态: 无法获得锁对象
  • 等待状态: wait() 方法
  • 计时等待: sleep() 方法
  • 结束状态: 全部代码运行完毕,线程死亡

在这里插入图片描述

多线程实现

注意事项

  • 一个运行的软件,最少要有一个线程,我们以前写的代码都是单线程的程序,这个线程我们叫做 main 线程, 也叫主线程
  • 哪个线程抢到 CPU,哪个线程就干活,但是哪一个线程能抢到 CPU 我们是无法控制的,所以每一次运行代码,看到的打印效果都是不一样的

方法1(继承Thread类)

步骤
  1. 自定义一个类并继承 Thread
  2. 重写 run() 方法
  3. 在测试类中创建自定义对象
  4. 根据自定义的方法使用 start() 方法启动线程

例:

public class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程: " + super.getId() + "..." + i);
        }
    }
}

public class MyThreadTest {
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        myThread1.start();
        System.out.println("线程1: " + myThread1.getName());
        MyThread myThread2 = new MyThread();
        myThread2.start();
        System.out.println("线程2: " + myThread2.getName());
    }
}

方法2(实现Runnable接口)

步骤
  1. 自定义一个类实现 Runnable 接口
  2. 重写 Run() 方法
  3. 在测试类中自定义类的对象
  4. 创建 Thread 类的对象,并且将自定义类的对象作为参数传递给 Thread 类的对象
  5. 使用 Thread 类的对昂调用 start() 方法启动线程

例:

public class MyThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程: " + i);
        }
    }
}

public class MyThreadTest2 {
    public static void main(String[] args) {
        MyThread2 myThread1 = new MyThread2();
        MyThread2 myThread2 = new MyThread2();
        Thread t1 = new Thread(myThread1);
        t1.start();

        Thread t2 = new Thread(myThread2);
        t2.start();
    }
}

方法3(Callable和Future)

步骤
  1. 自定义一个类实现 Callable 接口并指定泛型
  2. 重写 call() 方法
  3. 在测试类中创建一个自定义类对象
  4. 创建一个自定义类的对象,并且将自定义对象传递给 FutureTask 类的对象
  5. 创建 Thread 类的对象,并且将 Futuretask 类的对象传递给 Thread 类的对象
  6. 使用 Thread 类的对象调用 start() 方法启动线程

例:

public class MyThread3 implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程 " + i);
        }
        // 返回值就表示线程运行完毕之后的结果
        return "结束";
    }
}

public class MyThreadTest3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread3 myThread = new MyThread3();

        FutureTask futureTask = new FutureTask(myThread);

        Thread thread = new Thread(futureTask);

        thread.start();

        System.out.println(futureTask.get());
    }
}

其他

new Thread() {
	@Override
	public void run() {
		System.out.println("Test");
	}
}.start();

new Thread(new Runnable() {
	@Override
	public void run() {
		System.out.println("Test");
	}
}).start();
注意事项
  • get() 方法如果没有得到线程运行 call() 方法结束后的结果,就会死等结果,所以应放在 start() 之后

三种实现方式的对比

在这里插入图片描述

Thread类中常见的方法

  • getName(): 获取线程名称
  • setName(): 设置线程的名称(也可以使用带参构造设置线程名称)

run方法和start方法的区别

  • start() 方法: 作用是启动线程,只能启动一次
  • run() 方法: 和之前创建对象,调用方法相同,并没有开启线程,主要作用是在内部定义我们要进行的操作,可以调用多次

获取当前运行线程的线程对象

  • Thread.currentThread(): CPU 当前被哪个线程占用,就获取哪个线程

线程休眠

  • Thread.sleep()

线程调度

  • 分时调度模型: 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  • 抢占式调度模型: 优先让优先级高的使用 CPU

线程的优先级

  • getPriority()
  • setPriority(): 设置失败的概率较高,集体和操作系统/CPU 厂商有关

守护线程

当普通线程运行完毕之后,守护线程也就没有必要继续运行下去了,不会立即结束,但是也不会运行完毕

  • setDaemon()

线程安全

出现线程安全的前提条件

当多条线程操作共享数据时

为什么会出现线程安全问题

当线程再次抢到 CPU 时,可能此时数据已经发生改变

线程安全问题解决思路

多线程开发中,能不共享数据,就不共享数据

方法1(同步代码块)

  • 格式:
synchronized (任意对象) { // 多个线程必须使用同一个锁
}
  • 例1:
public class MyThread implements Runnable {
    private int ticket = 100;
    Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket <= 0) {
                    break;
                } else {

                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "在买票, 还剩下: " +
                            ticket + " 张票");
                }
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadLockTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        
        Thread thread1 = new Thread(myThread);
        thread1.setName("售票口1");
        Thread thread2 = new Thread(myThread);
        thread2.setName("售票口2");
        
        thread1.start();
        thread2.start();
    }
}
  • 例2:
public class MyThread1 extends Thread {
    private static int ticket = 100;
    private static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket <= 0) {
                    break;
                } else {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "在买票, 还剩下: " +
                            ticket + " 张票");
                }
            }

        }
    }
}

public class ThreadLockTest1 {
    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();
        MyThread1 myThread2 = new MyThread1();

        myThread1.setName("窗口1");
        myThread2.setName("窗口2");

        myThread1.start();
        myThread2.start();
    }
}

方法2(同步方法)

同步方法时的锁对象是this

在同一个tomcat中时,一个线程结束,另一个线程才可以调用该方法,但是多个tomcat之间起不到同步作用

  • 例:
/**
 * 同步方法
 */
public class MyThread4 implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            if ("窗口1".equals(Thread.currentThread().getName())) {
                boolean res = synchronizedMethod();
                if (res) {
                    break;
                }
            }
            if ("窗口2".equals(Thread.currentThread().getName())) {
                synchronized (this) {
                    if (ticket == 0) {
                        break;
                    } else {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        ticket--;
                        System.out.println(Thread.currentThread().getName() +
                                "在卖票, 还剩: " + ticket);
                    }
                }
            }
        }
    }

    private synchronized boolean synchronizedMethod() {
        if (ticket == 0) {
            return true;
        } else {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket--;
            System.out.println(Thread.currentThread().getName() +
                    "在卖票, 还剩: " + ticket);
            return false;
        }
    }
}

public class ThreadLockTest4 {
    public static void main(String[] args) {
        MyThread4 myThread4 = new MyThread4();

        Thread t1 = new Thread(myThread4);
        Thread t2 = new Thread(myThread4);

        t1.setName("窗口1");
        t2.setName("窗口2");

        t1.start();
        t2.start();
    }
}
和同步代码块的区别
  • 同步代码块可以锁定代码,同步方法是锁住方法中的所有代码
  • 同步代码块可以指定锁对象,同步方法不能指定锁对象
注意
  • 普通同步方法的锁对象: 只能是 this
  • 静态同步方法的锁对象,只能是 类名.class

方法3(lock)

步骤
  1. 创建 lock 对象
  2. 加锁
  3. 释放锁
  • 例:
public class MyThread2 implements Runnable {
    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (ticket <= 0) {
                    break;
                } else {
                    Thread.sleep(100);
                    ticket--;
                    System.out.println(Thread.currentThread().getName() +
                    "在买票, 还剩: " + ticket + "张");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

public class ThreadLockTest2 {
    public static void main(String[] args) {
        MyThread2 myThread2 = new MyThread2();

        Thread t1 = new Thread(myThread2);
        Thread t2 = new Thread(myThread2);

        t1.setName("窗口1");
        t2.setName("窗口2");

        t1.start();
        t2.start();
    }
}
锁的作用
  • 可以解决线程安全问题
  • 可以进行线程之间的通信

同步的好处和弊端

  • 好处: 解决了多线程的数据安全问题
  • 弊端: 当线程很多时,因为每个线程都会去判断同步上的锁,这很消耗资源,会降低程序的运行效率

生产者和消费者

等待和唤醒的方法(Object)

  • wait()
  • notify(): 唤醒正在等待对象监视器的单个线程
  • notifyAll(): 唤醒正在等待对象监视器的所有线程

/**
 * 桌子
 */
public class Desk {
    /**
     * 表示桌子上是否有汉堡
     * true 表示有汉堡
     * false 表示没有汉堡
     */
    public static boolean flag = false;

    /**
     * 表示现在桌子上可以放多少个汉堡
     */
    public static int num = 10;

    /**
     * 用于线程唯一对象,表示只有一个桌子
     */
    public static final Object desk = new Object();
}

/**
 * 生产者
 */
public class Cooker extends Thread {
    @Override
    public void run() {
        while (true) {
            // 同步控制
            synchronized (Desk.desk) {
                // 如果今天的总量已经用完,结束掉该线程
                if (Desk.num == 0) {
                    System.out.println("今天的10个汉堡做完了, 下班了");
                    break;
                } else {
                    // 如果桌子上没有汉堡
                    if (!Desk.flag) {
                        System.out.println("生产者生产汉堡, 今日汉堡还剩: " + Desk.num);
                        Desk.flag = true;
                        Desk.desk.notifyAll();
                    } else {
                        try {
                            System.out.println("桌子上还有1个汉堡, 等待消费者吃掉它");
                            // 等待消费者消耗汉堡
                            Desk.desk.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 消费者 
 */
public class Foodie extends Thread {
    @Override
    public void run() {
        while (true) {
            // 同步控制
            synchronized (Desk.desk) {
                // 如果总量已经用完,结束该线程
                if (Desk.num == 0) {
                    System.out.println("今天的10个汉堡吃完了, 明天再来吧");
                    break;
                } else {
                    // 如果桌子上有汉堡
                    if (Desk.flag) {
                        System.out.println("消费者吃汉堡, 今日汉堡还剩: " + Desk.num);
                        // 桌子上已经没有汉堡
                        Desk.flag = false;
                        // 总数减1
                        Desk.num--;
                        // 唤醒所有线程
                        Desk.desk.notify();
                    } else {
                        try {
                            System.out.println("桌子上没有汉堡, 等待生产者做汉堡");
                            Desk.desk.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 测试
 */
public class Demo {
    public static void main(String[] args) {
        Cooker cooker = new Cooker();
        Foodie foodie = new Foodie();

        cooker.start();
        foodie.start();
    }
}

线程池

步骤

  1. 创建线程池 ExecutorService es = Executors.newCachedThreadPool();
  2. 有任务执行时,先看线程池中有没有空闲线程,没有的话创建,使用完后 submit 进线程池,有的话复用旧线程
  3. 没有任务需要执行时,关闭线程池(shutdown)

submit注意事项

  • 每次调用 submit 方法,就会从线程池中征用一个线程
    • 对象如果没有空闲的线程对象,拿不到,创建一个新的线程对象,存储线程池中
    • 如果有空闲的线程,可以拿到,直接使用,就不会创建新的线程对象
  • 线程对象使用完之后,不会销毁

方式1(基本不用): 最大线程数为int的最大数

Executors.newCachedThreadPool()

方式2(优选): 可指定最多线程数量的线程池

ExecutorService es = Executors.newFixedThreadPool(long);

例:

ExecutorService executorService = Executors.newFixedThreadPool(10);
ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
System.out.println(pool.getPoolSize());

for (int i = 0; i < 20; i++) {
    executorService.submit(() -> {
        System.out.println(Thread.currentThread().getName() + "执行");
    });


}

System.out.println(pool.getPoolSize());

executorService.shutdown();

方式3: ThreadPoolExecutor

构造方法: ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

参数

  • 核心线程数: 不会因为超时被清理
  • 最大线程数: 核心线程+临时线程
  • 空闲线程最大存活时间: 超时将临时线程清理
  • 时间单位: TimeUnit
  • 任务队列(阻塞队列): 线程用完之后将任务放进队列
  • 创建线程工厂: 底层帮我们新建线程 可省略,使用默认
  • 任务的拒绝策略: 任务数超出最大线程数和队列数之后进行操作 可省略,使用默认
    1. ThreadPoolExecutor.AbortPolicy(最推荐): 对其任务并抛出 RejectedExecutionException 异常
    2. ThreadPoolExecutor.DiscardPolicy: 丢弃任务,不抛出异常
    3. ThreadPoolExecutor.DiscardOldestPolity: 抛弃队列中等待时间最久的任务,然后把当前任务加入队列中
    4. ThreadPoolExecutor.CallerRunsPolicy: 调用任务的 run() 方法绕过线程池直接执行

工作流程

1、如果正在运行的线程数量小于 `corePoolSize`,那么马上创建线程运行这个任务
2、如果正在运行的线程数量大于或等于 `corePoolSize`,那么将这个任务放入队列
3、如果这时候队列满了,而且正在运行的线程数量小于  `maximumPoolSize`,那么还是要创建非核心线程立刻运行这个任务
4、如果队列满了,而且正在运行的线程数量大于或等于  `maximumPoolSize`,那么线程池会抛出异常 `RejectExecutionException`

例:

ThreadPoolExecutor pool = new ThreadPoolExecutor(3,
        5, 2, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(10));

for (int i = 0; i < 100; i++) {
    pool.submit(() -> {
        System.out.println(Thread.currentThread().getName() +
                "在执行");
    });
//            try {
//                Thread.sleep(100);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
}

pool.shutdown();
这时候队列满了,而且正在运行的线程数量小于  `maximumPoolSize`,那么还是要创建非核心线程立刻运行这个任务
4、如果队列满了,而且正在运行的线程数量大于或等于  `maximumPoolSize`,那么线程池会抛出异常 `RejectExecutionException`

例:

ThreadPoolExecutor pool = new ThreadPoolExecutor(3,
        5, 2, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(10));

for (int i = 0; i < 100; i++) {
    pool.submit(() -> {
        System.out.println(Thread.currentThread().getName() +
                "在执行");
    });
//            try {
//                Thread.sleep(100);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
}

pool.shutdown();

如何定义线程池

  • io密集型: 推荐线程数是cpu核数的两倍
  • cpu密集型: 推荐线程数是cpu核数
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值