【日积月累】并发编程(二)

文章详细介绍了Java中创建线程的多种方式,包括继承Thread、实现Runnable接口、使用Callable和Future以及线程池。此外,还讨论了ThreadLocal的作用、使用方法和原理,以及线程池的优势和创建策略。文章提到了线程池的核心参数如corePoolSize和maximumPoolSize,并解释了线程池的工作流程。最后,提到了Future类在异步计算中的角色和Callable与Future的关系。
摘要由CSDN通过智能技术生成

创建线程的几种方式?

继承Thread类,创建线程类,重写run()方法

  1. 定义一个Thread的子类,并重写run()方法,run()方的方法提代表了线程要完成的任务,因此把run()方法成为执行体。
  2. 创建Thread子类实例,即创建了线程对象
  3. 调用线程对象的start()方法启动该线程
//创建10个线程,并显示出每个线程线程名
public class ThreadTestDemo extends Thread{
    
    @Override
    public void run() {
        
        System.out.println("线程名: " + Thread.currentThread().getName());
        
    }

    public static void main(String[] args) throws InterruptedException {
        
        for(int i=0;i<10;i++){
            ThreadTestDemo threadTestDemo = new ThreadTestDemo();
            threadTestDemo.setName(String.valueOf(i));
            sleep(1000);
            threadTestDemo.start();
        }
        
    }
}

在这里插入图片描述

实现Runable,接口创建线程类,重写run()方法

  1. 定义Runable接口的实现类,实现Runable接口
  2. 创建Runable接口的实现类的实例,并且以该实例作为target创建Thread对象,该Thread对象才是真正的线程对象
  3. 调用线程对象的start()方法启动该线程
public class ThreadRunableDemo implements Runnable{
    @Override
    public void run() {
        System.out.println("目前阶段的线程名称是:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadRunableDemo threadDemo2 = new ThreadRunableDemo();
        for(int i=0;i<10;i++){
            Thread thread = new Thread(threadDemo2, String.valueOf(i));
            //System.out.println("thread: " + thread);
            Thread.sleep(1000);
            thread.start();
        }
    }
}

在这里插入图片描述

使用Callable和Future创建线程

  1. 定义Callable接口实现类,并实现call()方法,该方法将作为线程的执行体,并且有返回值。
  2. 创建Callable实例,使用FutureTask类来包装Callable对象(FutureTask实现的定制化),该FutureTask对象封装了该Callable对象的call()方法的返回值。FutureTask是一个包装器,通过接受Callable对象来创建。同时实现了Future和Runable接口
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程
  4. 调用FutureTask对象的get()方法来获得子线程的返回值
public class CallableTestDemo implements Callable {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableTestDemo callableTestDemo = new CallableTestDemo();
        FutureTask futureTask = new FutureTask(callableTestDemo);
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" +i);
            if(i == 5){
                new Thread(futureTask,"有返回值的线程").start();
                
            }
        }

        try {
            System.out.println("子线程的返回值:" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
    
    @Override
    public Object call() throws Exception {
        int i = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " --------" + i);
        }
        return i;
    }
}

使用线程池创建线程

使用Executors框架创建

在这里插入图片描述

class ExecutorsTest {
    //创建一个实现Runnable接口的类型
    static class MyRunnable implements Runnable{
    //实现Runnable的抽象run()方法
        @Override
        public void run() {
            for (int i=0;i<100;i++) {
                System.out.println(i);
            }
        }
    }
    public static void main(String[] args) {
        //创建ExecutorServiceduix(创建线程池)
        ExecutorService excutorService = Executors.newFixedThreadPool(10);
        //创建了实现Runnable类的对象
        MyRunnable myRunnable = new MyRunnable();
        
        //System.out.println(myRunnable);
        
        //把实现Runnable类的对象最为参数放入ExecutorService类的对象execute() 方法
        excutorService.execute(myRunnable);//Runnable用execute()方法,Callable用submit()方法
        
        excutorService.shutdown();
        
    }
}

在这里插入图片描述

使用ThreadPoolExecutor创建
  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数(包含核心线程数)
  • keepAliveTime:空闲线程的存活时间
  • TimeUnit:时间单位
  • BlockingQueue:任务队列,用于存储线程池的待执行任务的
  • ThreadFactory:线程工厂,用于生成线程
  • handler:拒绝策略
public class ThreadPoolDemo10 {
    public static void main(String[] args) {
        ThreadFactory factory=new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread=new Thread(r);
                return thread;
            }
        };
        ThreadPoolExecutor executor=new ThreadPoolExecutor(2,5,10,
                TimeUnit.SECONDS,new LinkedBlockingDeque<>(2),factory,new ThreadPoolExecutor.DiscardPolicy());
        for (int i = 1; i < 6; i++) {
            int finalI=i;
            executor.submit(()->{
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任务:"+finalI);
            });
        }
    }
}
线程池策略
  1. 当线程池中有任务时,判断线程池是否还在运行状态,否->任务拒绝(执行拒绝策略),是->进行下一步;
  2. 判断线程池中线程数是否小于核心线程数(corePoolSize),是->创建工作线程运行此任务,且任务结束后将该线程保留在线程池中,不做销毁处理,否->进行下一步;
  3. 判断阻塞队列/工作队列(workQueue)是否已满,否->将任务添加到工作队列中,是->进行下一步;
  4. 判断线程池中的线程数是否小于最大线程数(maxmumPoolSize),是->采用饱和策略来处理,否->新建一个工作线程来执行这个任务。【注意】当线程池中的线程数大于maxmumPoolSize时,当线程的空闲时间超过空闲线程的存活时间(keepAliveTime)时,这个线程就会被终止,知道线程中的数量不大于corePoolSize。

总结:corePoolSize(线程池中线程数是否大于,否:创建新线程执行,是:进行下一步)–>workQueue(工作队列是否已满,否:将任务添加到工作队列,是:进行下一步)–>maxmumPoolSize(线程池中线程数是否大于,否:创建新工作线程来执行任务,是:采用饱和策略(拒绝策略)才处理)。
在这里插入图片描述

饱和策略(拒绝策略 handler)

当工作队列满且线程个数达到maximunPoolSize后所采取的策略 java4个 自定义一个
AbortPolicy:默认策略;新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。
CallerRunsPolicy:既不抛弃任务也不抛出异常,使用调用者所在线程运行新的任务。
DiscardPolicy:丢弃新的任务,且不抛出异常。
DiscardOldestPolicy:调用poll方法丢弃工作队列队头的任务,然后尝试提交新任务
自定义策略:根据用户需要定制。

线程的创建看起来有四种方式: 继承 Thread 类、实现 Runnable 接口、实现 Callable 接口、使用线程池。
***但是本质上,线程的创建方式其实只有一种:

new 一个 Thread 对象,并给它一个 Runnable 类型的对象

Thread 对象负责启动线程,Runnable 对象负责执行线程***

ThreadLocal

ThreadLocal 有什么用?

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?JDK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
总结:类似每个线程中各自拥有的程序计数器,虚拟机栈,本地方法栈,都是这个线程私有的。

如何使用 ThreadLocal?

代码描述
创建0-9共十个线程,为每个线程变量赋值后初始化simpleDateFormat的值,对比变量formatter前后的差异。

public class ThreadLocalDemo implements Runnable {
    //每一个线程专属的变量,
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
    //上面这段创建专属变量的代码等价于下面,重写ThreadLocal的initialValue()方法,返回一个指定的日期格式,但是在Java8中提供了withInitial方法将 Supplier接口作为参数,使用了lambda表达式简化了代码
    private static final ThreadLocal<SimpleDateFormat> formatter2 = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue(){
            return new SimpleDateFormat("yyyyMMdd HHmm");
        }
    };
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
    
    public static void main(String[] args) throws InterruptedException{
        ThreadLocalDemo obj = new ThreadLocalDemo();
        for(int i=0;i<10; i++){
            Thread thread = new Thread( obj,"" +i);
            //随机0-1000毫秒(ms),如果采取这种方式休眠,则苏醒时间不固定,查看结果比较混乱,不利于理解,可以用下面1s
            //Thread.sleep(new Random().nextInt(1000));
            //间隔1s执行
            Thread.sleep(1000);
            thread.start();
        }
    }

    @Override
    public void run() {
        //赋值给了第i个线程的专属变量
        System.out.println("线程: " + Thread.currentThread().getName() + "的变量被修改为:" + formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //返回描述此日期格式的模式字符串
        System.out.println("初始化日期格式后变量:"+ Thread.currentThread().getName()+ " 的值为 " +simpleDateFormat.toPattern());
        formatter.set(new SimpleDateFormat());

        System.out.println("---------线程" + Thread.currentThread().getName() + " 被初始化后的变量值为:" +formatter.get().toPattern());
    }
}

在这里插入图片描述

ThreadLocal 原理了解吗?

public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

Thread中有两个变量,threadLocals 和 inheritableThreadLocals 他们都是ThreadLocalMap类型(理解为ThreadLocal类实现的定制化HashMap),默认情况下都是null。只有当前线程调用ThreadLocal类的set()或者get()方法时才创建他们,调用这两个方法时,我们实际上调用的是ThreadLocalMap类对应的get(),set()方法。

public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

实际上,我们创建的变量最终是存放再练当前线程的ThreadLocalMap中,并不是存放在ThreadLocal上,ThreadLocal可以理解为知识ThreadLocalMap的封装,传递了变量值,ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。
每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
> 总结:当我们为当前线程创建一个变量时,他是存储在ThreadLocalMap里的,一个key-value的Hash Map 他是以ThreadLocal 为key Object为对象的键值对.

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue){
}

ThreadLocal 数据结构如下图所示:
在这里插入图片描述

总结:如果在一个线程创建两个变量(声明两个ThreadLocal对象),那么他们都是存放在ThreadLocalMap里,它的key是ThreadLocal对象,value是ThreadLocal对象调用set()方法设置的值。

ThreadLocal 内存泄露问题是怎么导致的?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
> 总结:ThreadLocalMap是一个结构为<ThreadLocal,Object>的HashMap,它的key是若引用,value是强引用。在垃圾回收时,key会被清理调,而value不会被清理。这就产生了一个key为null而value不为null的一个Enrty(键值对),如果不做任何措施,这个value永远不会被GC(垃圾回收器)回收,这时就有可能产生内存泄漏(占用了内存,无法回收)。但是ThreadLocalMap已经考虑了这种情况,et()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录,但是我们使用完 ThreadLocal方法后 最好手动调用remove()方法。

线程池

线程池是管理一系列线程的资源池。当有任务需要处理,直接从线程池获取线程来处理,处理完成之后线程并不会立即被销毁,而是会等待下一个任务。(池化技术,类似数据库连接池,Http连接池,都是对这个思想的应用,其目的是为了减少每次获取资源的消耗,提高对资源的利用率)

优势

降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗(当任务完成后线程并不会立即被销毁,而是会等待下一个任务)。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行(提前创建了线程)。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的创建方式

1.通过ThreadPoolExecutor构造函数创建(阿里巴巴开发手册强制用这个)【强制使用】
2.通过 Executor 框架的工具类 Executors 来创建(内置线程池,在《阿里巴巴 Java 开发手册》规范中不被允许在应用中自行显式创建线程)。

ThreadPoolExecutor的三个重要参数

corePoolSize(线程池的核心线程数量) : 任务队列未达到队列容量时,最大可以同时运行的线程数量。
maximumPoolSize(线程池的最大线程数) :任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
workQueue:新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

在这里插入图片描述

多线程的内存泄漏

在应用程序中,未引用的对象会被垃圾回收器(GC)回收,而引用的对象不会。但是可能出现,对象不再被使用而GC无法回收它,这就是内存泄漏。无用的对象并不都是未引用的对象,有些无用对象也可能是引用对象,这正是引起内存泄漏的来源。(曾经有用到过的对象,但是目前没有使用到且仍然占用着内存,没有及时释放,会导致内存占用率越来越高,影响系统性能,导致进程甚至系统崩溃)

Future类

Future类的作用

Future类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直在原地等待耗时任务执行完成,执行效率太低。为了避免这种情况,我们可以将这个耗时任务交给一个子线程去异步执行,同时我们可以去干其他的事情,不用再傻傻等待耗时任务执行完成,等我们其他事情干完后,再通过Future类获取到耗时任务的执行结果,这样一来我们程序的执行效率就明显提高了。
> 总结:在java中,Future类就是为了去获取异步调用后的结果,那么这其实是多线程中的Future模式,可以看做是一种设计模式,其核心思想是异步调用,主要用在多线程领域,并非java语言得有。

Future类源码

// V 代表了Future执行的任务返回值的类型
public interface Future<V> {
    // 取消任务执行
    // 成功取消返回 true,否则返回 false
    boolean cancel(boolean mayInterruptIfRunning);
    // 判断任务是否被取消
    boolean isCancelled();
    // 判断任务是否已经执行完成
    boolean isDone();
    // 获取任务执行结果
    V get() throws InterruptedException, ExecutionException;
    // 指定时间内没有返回计算结果就抛出 TimeOutException 异常
    V get(long timeout, TimeUnit unit)

        throws InterruptedException, ExecutionException, TimeoutExceptio

}
 

简单理解就是:我有一个任务,提交给了 Future 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 Future 那里直接取出任务执行结果。

Callable和Future的关系

FutureTask提供了Future接口的基本实现,常用来封装Callable和Runable,具有取消任务,查看任务是否执行完成以及获取任务执行结果的方法。ExecutorService.submit() 方法返回的其实就是 Future 的实现类 FutureTask 。

CompletableFuture类有什么用

参考

JavaGuide

lambda表达式

创建线程的几种方式

线程的创建(线程池)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

顶子哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值