轻轻松松学会多线程

本文详细介绍了Java中多线程的相关概念,包括线程与进程的区别、线程的实现方式(如继承Thread、实现Runnable和Callable)、线程生命周期、线程同步方法(如synchronized、Lock)以及线程安全问题的解决。此外,还讨论了线程池的工作原理和优化配置,以及生产者消费者模式的应用。
摘要由CSDN通过智能技术生成

学习目标

  1. 线程的概念
    1.1. 线程和进程的区别
    1.2. 串行、并行和并发的概念

  2. 线程的实现

  3. 线程的生命周期

  4. 线程的常用方法

  5. 线程的上下文切换

  6. 线程的安全(同步)问题

  7. 线程安全问题的解决方法

  8. ThreadLocal的介绍

  9. 线程的等待和通知

  10. 生产者消费者模式

  11. 线程池

线程的概念

程序和进程的区别

  • 程序是一种静态概念,是保存在磁盘上的一系列文件
  • 进程是一种动态概念,是运行中的程序,一个程序包括一个或多个进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xw1v1AxA-1654834700739)(线程.assets/image-20211207104914272.png)]

进程和线程的区别

  • 进程是程序执行相关资源(CPU、内存、磁盘等)分配的最小单元

    进程之间是相互独立的,有自己的内存空间

  • 线程是CPU资源分配的最小单元

    进程包含一个或多个线程

    线程需要的资源更少,可以看做是一种轻量级的进程

    线程会共享进程中的内存,线程也有独立的空间(栈、程序计数器)

    线程相互通信更加方便

串行、并行和并发

  • 串行

    多个指令依次执行

  • 并发

    每个线程单独执行一段指令,一个cpu在线程间切换(并不是同时执行)

  • 并行

    多个CPU内核同时执行多个线程,线程是同时执行的

线程的实现

Java几种实现线程的方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 使用线程池

继承Thread类

  1. 继承Thread类
  2. 重写run方法
  3. 调用start启动线程
/**
 * 自定义线程类
 */
class MyThread extends Thread{

    /**
     * 执行指令
     */
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() +"执行了" + i);
        }
    }
}

public class ThreadDemo {

    public static void main(String[] args) {
        //创建线程对象
        MyThread myThread = new MyThread();
        //启动线程
        myThread.start();
        //主线程执行
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() +"执行了" + i);
        }
    }
}

线程的run和start的区别

  • run没有启动新线程,在主线程中执行
  • start才能启动新线程

实现Runnable接口

  1. 实现Runnable接口
  2. 实现run方法
  3. 创建实现Runnable接口的对象,传入Thread对象中
  4. 启动线程
/**
 * 实现Runnable接口的类
 */
class MyRunnable implements Runnable{

    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "--" + i);
        }
    }
}
public class RunnableDemo {
    public static void main(String[] args) {
        //创建Runnable对象,传入Thread对象
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        //启动线程
        thread.start();

        //匿名内部类
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + "--" + i);
                }
            }
        });
        thread2.start();

        //Lambda表达式
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "--" + i);
            }
        }).start();
    }
}

继承Thread和实现Runnable的区别

  1. 继承Thread类,不能继承其它的类,语法有限制
  2. 实现Runnable接口,可以继承其它类,语法没有限制
  3. Runnalbe接口强制要求实现run方法,不容易出现错误

推荐实现Runnable方式

实现Callable接口

实现Callable接口可以返回值,继承Thread类和Runnable不行

  1. 实现Callable接口,实现call方法

  2. 创建Callable对象,传入FutureTask对象

  3. 创建FutureTask对象,传入Thread对象

  4. 启动线程

  5. 调用get方法得到返回结果

/**
 * 实现Callable接口
 */
class MyCallable implements Callable<Long> {

    @Override
    public Long call() throws Exception {
        //模拟复杂运算
        long sum = 0;
        for(long i = 0;i < 1000000000L;i++){
            sum += i;
        }
        return sum;
    }
}
public class CallableDemo {

    public static void main(String[] args) {
        //创建Callable对象,传入FutureTask对象
        FutureTask<Long> task = new FutureTask<>(new MyCallable());
        //创建FutureTask对象到Thread对象中
        Thread thread = new Thread(task);
        //启动线程
        thread.start();
        System.out.println("-----等待结果-----");
        //获得返回值
        try {
            System.out.println(task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

线程的生命周期

线程几种状态:

  • 新建 NEW
  • 准备/就绪 START
  • 运行 RUNNING
  • 阻塞 BLOCKING
  • 死亡 DEAD

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HfTEBCEv-1654834700743)(线程.assets/image-20211207161252588.png)]

线程的常用方法

常用方法

方法介绍
start()启动
stop()停止(禁用,可能导致线程死锁等问题),停止线程可以让run执行结束
String getName()获得线程的名字
setName(String)设置线程名字
sleep(long)进入睡眠,毫秒
setPriority(int)设置线程的优先级(1~10从低到高)越高抢CPU几率更高
setDaemon(boolean)设置为后台线程 true ,后台线程是为其它线程服务的,如果没有其它线程存在,就自动死亡;使用案例:GC就是一种后台线程
join()线程的加入(合并)让其它线程先执行完,再执行自己的指令

线程的上下文切换

前提:一个CPU的内核一个时间只能运行一个线程中的一个指令

线程并发:CPU内核会在多个线程间来回切换运行,切换速度非常快,达到同时运行的效果

问题1:

线程切换回来后,如何从上次执行的指令后执行?

程序计数器(每个线程都有,用于记录上次执行的行数)

问题2:

线程执行会随时切换,如何保证重要的指令能完全完成?

线程安全问题

问题3:

CPU进行上下文切换的过程中,性能会降低。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BudUWYjI-1654900294607)(线程.assets/image-20211208091225116.png)]

线程的安全(同步)问题

CPU在多个线程间切换,可能导致某些重要的指令不能完整执行,出现数据的问题。

出现线程安全问题的三个条件:

  • 多个线程
  • 同一个时间
  • 执行同一段指令或修改同一个变量
/**
 * 银行转账的案例
 */
public class BankDemo {

    //模拟100个银行账户
    private int[] accounts = new int[100];

    {
        //初始化账户
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = 10000;
        }
    }

    /**
     * 模拟转账
     */
    public void transfer(int from,int to,int money){
        if(accounts[from] < money){
            throw new RuntimeException("余额不足");
        }
        accounts[from] -= money;
        System.out.printf("从%d转出%d%n",from,money);
        accounts[to] += money;
        System.out.printf("向%d转入%d%n",to,money);
        System.out.println("银行总账是:" + getTotal());
    }

    /**
     * 计算总余额
     * @return
     */
    public int getTotal(){
        int sum = 0;
        for (int i = 0; i < accounts.length; i++) {
            sum += accounts[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        BankDemo bank = new BankDemo();
        Random random = new Random();
        //模拟多次转账过程
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                int from = random.nextInt(100);
                int to = random.nextInt(100);
                int money = random.nextInt(2000);
                bank.transfer(from,to,money);
            }).start();
        }
    }
}

线程安全问题的解决方法

解决方法:给程序上锁,让当前线程完整执行一段指令,执行完释放锁,其它线程再执行

几种不同上锁方法:

  • 同步方法
  • 同步代码块
  • 同步锁

同步方法

给方法添加synchronized关键字

作用是给整个方法上锁

过程:

当前线程调用方法后,方法上锁,其它线程无法执行,调用结束后,释放锁。

    /**
     * 模拟转账
     */
    public synchronized void transfer(int from,int to,int money){
        if(accounts[from] < money){
            throw new RuntimeException("余额不足");
        }
        accounts[from] -= money;
        System.out.printf("从%d转出%d%n",from,money);
        accounts[to] += money;
        System.out.printf("向%d转入%d%n",to,money);
        System.out.println("银行总账是:" + getTotal());
    }

锁对象:

  • 非静态方法 --> this
  • 静态方法 —> 当前类.class

同步代码块

粒度比同步方法小,粒度越小越灵活,性能更高

给一段代码上锁

synchronized(锁对象){
	代码
}

锁对象,可以对当前线程进行控制,如:wait等待、notify通知;

任何对象都可以作为锁,对象不能是局部变量

        //同步代码块
        synchronized (lock) {
            accounts[from] -= money;
            System.out.printf("从%d转出%d%n", from, money);
            accounts[to] += money;
            System.out.printf("向%d转入%d%n",to,money);
            System.out.println("银行总账是:" + getTotal());
        }

synchronized的基本的原理:

一旦代码被synchronized包含,JVM会启动监视器(monitor)对这段指令进行监控

线程执行该段代码时,monitor会判断锁对象是否有其它线程持有,如果其它线程持有,当前线程就无法执行,等待锁释放

如果锁没有其它线程持有,当前线程就持有锁,执行代码

底层汇编实现:

monitorenter
....
monitorexit

同步锁

在java.concurrent并发包中的

Lock接口

基本方法:

  • lock() 上锁
  • unlock() 释放锁

常见实现类

  • ReentrantLock 重入锁
  • WriteLock 写锁
  • ReadLock 读锁
  • ReadWriteLock 读写锁

使用方法:

  1. 定义同步锁对象(成员变量)
  2. 上锁
  3. 释放锁
//成员变量
Lock lock = new ReentrantLock();

//方法内部上锁
lock.lock();
try{
	代码...
}finally{
	//释放锁
	lock.unlock();
}

三种锁对比:

  • 粒度

    同步代码块/同步锁 < 同步方法

  • 编程简便

    同步方法 > 同步代码块 > 同步锁

  • 性能

    同步锁 > 同步代码块 > 同步方法

  • 功能性/灵活性

    同步锁(有更多方法,可以加条件) > 同步代码块 (可以加条件) > 同步方法

悲观锁和乐观锁

悲观锁

认为线程的安全问题非常容易出现,会对代码上锁

前面所讲的锁机制都属于悲观锁

悲观锁的锁定和释放需要消耗比较多的资源,降低程序的性能

乐观锁

认为线程的安全问题不是非常常见的,不会对代码上锁

有两种实现方式:

  • 版本号机制

    利用版本号记录数据更新的次数,一旦更新版本号加1,线程修改数据后会判断版本号是否是自己更新的次数,如果不是就不更新数据。

  • CAS (Compare And Swap)比较和交换算法

    • 通过内存的偏移量获得数据的值
    • 计算出一个预计的值
    • 将提交的实际值和预计值进行比较,如果相同执行修改,如果不同就不修改

悲观锁和乐观锁对比

  • 悲观锁更加重量级,占用资源更多,应用线程竞争比较频繁的情况,多写少读的场景
  • 乐观锁更加轻量级,性能更高,应用于线程竞争比较少的情况,多读少写的场景
public class AtomicDemo {

    static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            new Thread(() ->{
                count++;
            }).start();
        }
        System.out.println(count);
    }
}

问题:多线程同时执行++操作,最后结果少了

分析:

count++ 分解为三个指令:

  1. 从内存中读取count的值
  2. 计算count+1的值
  3. 将计算结果赋值给count

这三个指令不是原子性的,A线程读取count值10,加1后得到11,准备赋值给count;B线程进入读取count也是10,加1得到11,赋值给count为11;切换会A线程,赋值count为11。

解决方案:

  1. 悲观锁,使用同步方法、同步块、同步锁

  2. 乐观锁

    使用原子整数

原子类

AtomicInteger类

原子整数,底层使用了CAS算法实现整数递增和递减操作

常用方法:

  • incrementAndGet 原子递增
  • decrementAndGet 原子递减
public class AtomicDemo {

    static int count = 0;

    static AtomicInteger integer = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            new Thread(() ->{
                count++;
                //递增
                integer.incrementAndGet();
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count:"+count);
        System.out.println("atomic:"+integer.get());
    }
}

CAS算法存在的问题

  1. ABA问题 ????
  2. 如果预期值和实际值不一致处于循环等待状态,对CPU的消耗比较大

ThreadLocal

线程局部变量,会为每个线程单独变量副本,线程中的变量不会相互影响

以空间换时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oi5MZ82A-1654900294609)(线程.assets/image-20211208164323114.png)]

public class ThreadLocalDemo {

    static int count = 0;

    //线程局部变量
    static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        //设置初始值
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                count++;
                local.set(local.get() + 1);
                System.out.println(Thread.currentThread().getName() + "--->" + local.get());
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
        System.out.println(local.get());
    }
}

ThreadLocal底层的实现

数据结构:Map键值对的结构

通过当前线程,得到当前线程中的ThreadLocalMap集合

将数据绑定到该Map中

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

线程的等待和通知

Object类中的方法

  • wait() 让当前线程进入等待状态,直到被通知为止
  • wait(long) 让当前线程进入等待状态,同时设置时间;直到被通知为止或时间结束
  • notify() 随机通知一个等待线程
  • notifyAll() 通知所有的等待线程

注意:等待和通知方法必须是锁对象,否则会抛出IllegalMonitorStateException

/**
 * 通过锁对象将线程等待,经过5秒通知该线程来执行
 */
public class WaitDemo {

    public synchronized void print() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" +i);
            if(i == 50){
                //让当前线程等待
                this.wait();
            }
        }
    }

    public synchronized void notifyTest(){
        //让等待的线程执行
        this.notifyAll();
    }

    public static void main(String[] args) {
        WaitDemo waitDemo = new WaitDemo();
        new Thread(()->{
            try {
                waitDemo.print();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        waitDemo.notifyTest();
    }
}

wait()和sleep()的区别

  1. 调用对象不同

    wait() 由锁对象调用

    sleep() 由线程调用

  2. 锁使用不同

    执行wait后,自动释放锁

    执行sleep后,不会释放锁

  3. 唤醒机制不同

    执行wait后,可以被通知唤醒

    执行sleep后,只能等待时间结束后,自动唤醒

生产者消费者模式

一种设计模式,不属于GOF23

生产者和消费者

  • 生产者

    某些程序/进程/线程负责生产数据就属于生产者

  • 消费者

    某些程序/进程/线程负责使用数据就属于消费者

生产者和消费者之间的问题

  • 耦合性高,生产者和消费者联系紧密,不利于系统的扩展和维护
  • 并发性能低,同时能处理请求量少
  • 忙闲不均,生产者和消费者的速度不一致,带来系统资源的浪费

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ijew5hEf-1654900530500)(线程.assets/image-20211209102355124.png)]

实现过程:

  1. 通过添加缓冲区,设置上限

  2. 生产者生产数据,向缓冲区存放,如果满了,生产者进入等待,直到缓冲区有空的位置通知生产者生产;

  3. 消费者从缓冲区取数据进行消费,如果空了,消费者进入等待,直到缓冲区有数据再通知消费者消费。

解决问题:

  • 解耦
  • 提高并发性能
  • 解决忙闲不均
package com.hopu.thread;

import java.util.ArrayList;
import java.util.List;

/**
 * 包子铺
 */
public class BaoziShop {
    /**
     * 包子
     */
    class Baozi{
        private int id;
        public Baozi(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "包子--" + id;
        }
    }
    //上限
    public static final int MAX_COUNT = 100;
    //缓冲区 存放数据
    private List<Baozi> baozis = new ArrayList<>();

    /**
     * 做包子
     */
    public synchronized void makeBaozi() throws InterruptedException {
        //判断缓冲区是否满了
        if(baozis.size() == MAX_COUNT){
            System.out.printf("缓冲区满了,%s等待%n",Thread.currentThread().getName());
            //让生产者线程等待
            this.wait();
        }else{
            //通知生产者线程生产
            this.notifyAll();
        }
        //创建包子
        Baozi baozi = new Baozi(baozis.size() + 1);
        System.out.println(Thread.currentThread().getName()+"做了"+baozi);
        //保存到缓冲区
        baozis.add(baozi);
    }

    /**
     * 拿包子
     */
    public synchronized void takeBaozi() throws InterruptedException {
        //判断缓冲区是否空了
        if(baozis.size() == 0){
            System.out.printf("缓冲区空了,%s等待%n", Thread.currentThread().getName());
            //让消费者等待
            this.wait();
        }else{
            //通知消费者消费
            this.notifyAll();
        }
        //获得第一个包子,并删除
        if(baozis.size() > 0){
            Baozi baozi = baozis.remove(0);
            System.out.println(Thread.currentThread().getName()+"吃了"+baozi);
        }
    }

    public static void main(String[] args) {
        BaoziShop baoziShop = new BaoziShop();
        //一个生产者
        Thread productor = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    baoziShop.makeBaozi();
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        productor.start();
        //10个消费者吃10个包子
        for (int i = 0; i < 10; i++) {
            Thread consumer = new Thread(() ->{
                try {
                    for (int j = 0; j < 10; j++) {
                        baoziShop.takeBaozi();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            consumer.start();
        }
    }
}

应用场景:

  • 12306售票

  • 消息队列

  • 线程池

  • 等等

阻塞队列

应用了生产者消费者模式的集合,能够根据数据满或空的情况,自动对线程执行等待和通知

BlockingQueue 接口

  • put 添加数据,达到上限会自动让线程等待
  • take 取并删除数据,数据空了会自动让线程等待

实现类

ArrayBlockingQueue 类 数据结构为数组

LinkedBlockingQueue类 链表结构

/**
 * 包子铺
 */
public class BaoziShop2 {
    /**
     * 包子
     */
    static class Baozi{
        private int id;
        public Baozi(int id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "包子--" + id;
        }
    }


    public static void main(String[] args) {
        //阻塞队列
        BlockingQueue<Baozi> baozis = new ArrayBlockingQueue<>(100);
        //生产者线程
        new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                //创建包子,添加到阻塞队列,满了就自动阻塞线程
                Baozi baozi = new Baozi(baozis.size() + 1);
                try {
                    baozis.put(baozi);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"生产了"+baozi);
            }
        }).start();
        //消费者线程
        for(int i = 0;i < 5;i++){
            new Thread(() -> {
                //取包子,空了会自动阻塞
                for (int j = 0; j < 40; j++) {
                    try {
                        Baozi baozi = baozis.take();
                        System.out.println(Thread.currentThread().getName()+"消费了"+baozi);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

线程池

作用:回收利用线程资源

线程是一种宝贵的系统资源,执行完任务后会死亡,如果有大量任务需要处理,需要频繁的创建和销毁线程,造成系统性能降低。

线程池会保存一定量的线程,线程执行完任务后,会回到线程池中,等待下一个任务,节省系统资源,提升性能。

线程池的使用

顶层接口:Executor

  • execute(Runnable) 启动线程执行一个任务

ExecutorService

继承 Executor

添加线程池管理方法,如:shutdown()、shutdownNow()

Executors

用于创建线程池的工具类

主要的方法

方法名说明
newCachedThreadPool()创建长度不限的线程池
newFixedThreadPool(int )创建固定长度的线程池
newSingleThreadExecutor()创建单一个数的线程池
newScheduledThreadPool(int)创建可以调度的线程池
package com.hopu.thread;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo {

    public static void useCachedThreadPool(){
        //创建长度不限的线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            int n = i;
            //使用线程池启动线程
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName()+"执行了任务" + n);
            });
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }

    public static void useFixedThreadPool(){
        //创建长度固定的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 100; i++) {
            int n = i;
            //使用线程池启动线程
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName()+"执行了任务"+n);
            });
        }
        executorService.shutdown();
    }

    public static void useSingleThreadPool(){
        //创建单一长度的线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 100; i++) {
            int n = i;
            //使用线程池启动线程
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName()+"执行了任务"+n);
            });
        }
        executorService.shutdown();
    }

    public static void useScheduledThreadPool(){
        //获得可调度的线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        //执行可调度任务
        System.out.println("------------");
        scheduledExecutorService.scheduleAtFixedRate(()->{
            System.out.println(Thread.currentThread().getName()+"---->"+ LocalDateTime.now());
        },1,3, TimeUnit.SECONDS);
    }

    public static void main(String[] args) {
//        useCachedThreadPool();
//        useFixedThreadPool();
//        useSingleThreadPool();
        useScheduledThreadPool();
    }
}

线程池的优化配置

线程池的实现类

ThreadPoolExecutor

线程池的构造方法参数:

  • corePoolSize 核心线程数,创建线程池后自带线程,不会进行销毁
  • maximumPoolSize 最大线程数
  • keepAliveTime 存活时间,非核心线程能够闲置的时间,超过后被销毁
  • timeUnit 时间单位
  • blockingQueue 阻塞队列 存放任务(Runnable)的集合

优化配置

  1. 核心线程数 应该和CPU内核数量相关 CPU内核数 * N (N和任务执行需要时间和并发量相关)

    Runtime.getRuntime().availableProcessors()
    
  2. 最大线程数可以和核心线程数一样,避免频繁创建和销毁线程

  3. 如果存在非核心线程,设置大一点,避免频繁创建和销毁线程

  4. 阻塞队列使用LinkedBlockingQueue,插入和删除任务效率更高

线程池的实现原理

问题:线程池是如何对线程进行回收利用的?

所有线程保存在HashSet中

/**
 * Set containing all worker threads in pool. Accessed only when
 * holding mainLock.
 */
private final HashSet<Worker> workers = new HashSet<Worker>();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hAAYf6M9-1654900530501)(线程.assets/image-20211209165058288.png)]

总结

  1. 线程的等待和通知(是干什么,谁来调用)
  2. wait和sleep的区别
  3. 生产者和消费者模式(是什么,解决什么问题,如何实现的,项目中有哪些应用)
  4. 阻塞队列(了解)
  5. 线程池(作用,有哪几种线程池,线程池的创建有哪些参数,如何优化,实现原理(看源码、画图、归纳))

自学volatile关键字

作业

  1. 了解什么是ABA问题

  2. 编写懒汉式的单例模式,创建100个线程,每个线程获得一个单例对象,看是否存在问题(打印对象的hashCode,看是否相同)

    分析问题原因,解决问题

作业

  1. 设计两个线程,一个线程负责打印1100以内所有的偶数;然后,另外一个线程负责打印1100以内所有的奇数。

测试时,分别设置线程的优先级,观察执行的顺序。

  1. 实现一个线程,用于扫描某个目录下的所有文本文件(包括:java、txt、html),并将文字内容打印出来。

  2. 某人正在看电视连续剧,从第1~88集,看到第10集时,来了一个送快递的,收完快递后后,继续看电视。

  3. 多线程模拟龟兔赛跑:

​ 乌龟和兔子进行1000米赛跑,兔子前进5米,乌龟只能前进1米。

​ 但兔子每20米要休息500毫秒,而乌龟是每100米休息500毫秒。

​ 谁先到终点就结束程序,并显示获胜方

每天一篇博客,介绍今天学习的内容,编写的案例,完成作业(代码+截图)

预习内容:

什么是线程同步问题?

如何解决线程同步?

线程的通知和等待

html),并将文字内容打印出来。

  1. 某人正在看电视连续剧,从第1~88集,看到第10集时,来了一个送快递的,收完快递后后,继续看电视。

  2. 多线程模拟龟兔赛跑:

​ 乌龟和兔子进行1000米赛跑,兔子前进5米,乌龟只能前进1米。

​ 但兔子每20米要休息500毫秒,而乌龟是每100米休息500毫秒。

​ 谁先到终点就结束程序,并显示获胜方

每天一篇博客,介绍今天学习的内容,编写的案例,完成作业(代码+截图)

预习内容:

什么是线程同步问题?

如何解决线程同步?

线程的通知和等待

生产者消费者模式是什么?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值