由浅入深学习java并发(一文解决多线程、线程池)

一、进程和线程

1.进程

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

2.线程

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

二、多线程的实现方式

1.继承Thread类(可以不重写run方法)

注意:如果不重写run方法的话子类.start()就会调用Thread类中默认的run()方法,默认run方法是不执行任何操作并返回。

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方 法就是通过 Thread 类的 start()方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法

public class MyThread extends Thread { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
} 
MyThread myThread1 = new MyThread(); 
myThread1.start(); 

2.实现Runnable接口(必须重写run方法)

Runnable是函数式接口(只有一个方法的接口),如果一个类实现一个接口必须实现接口中的全部方法。

public class MyThread extends OtherClass implements Runnable { 
 public void run() { 
 System.out.println("MyThread.run()"); 
 } 
} 
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
	MyThread myThread = new MyThread(); 
	Thread thread = new Thread(myThread); 
	thread.start(); 
//事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用target.run()
	public void run() { 
	 if (target != null) { 
        target.run(); 
     } 
	} 
实现Runnable接口无返回值,如果需要返回值必须使用Callable接口,并且Callable可以抛出异常!

3.实现Callable接口(函数式接口)

@FunctionalInterface
public interface Callable<V> {
 V call() throws Exception;
}

执行 Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务 返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现有返回结果的多线程了。

// ⾃定义Callable
class Task implements Callable<Integer>{
 	@Override
 	public Integer call() throws Exception {
 // 模拟计算需要⼀秒
 	Thread.sleep(1000);
	 return 2;
 }
/创建一个线程池
	ExecutorService pool = Executors.newFixedThreadPool();
// 创建多个有返回值的任务
	Task task = new Task(); 
// 执行任务并获取 Future 对象
	Future<Integer> f = pool.submit(task); 
// 关闭线程池
	pool.shutdown(); 
// 从 Future 对象上获取任务的返回值,并输出到控制台
	System.out.println("res:" + f.get().toString()); 
}

(1)Future接口

Future是我们在使用java实现异步时最常用到的一个接口,我们可以向线程池提交一个Callable,并通过future对象获取执行结果。

public interface Future<V> {
 	public abstract boolean cancel(boolean paramBoolean);
 	public abstract boolean isCancelled();
 	public abstract boolean isDone();
 	public abstract V get() throws InterruptedException, ExecutionException;
 	public abstract V get(long paramLong, TimeUnit paramTimeUnit)
 	throws InterruptedException, ExecutionException, TimeoutException;
}

(2)FutureTask类

由于了Future 只是⼀个接⼝,而它⾥⾯的 cancel , get , isDone 等⽅法要⾃⼰实现 起来都是⾮常复杂,所以JDK提供了⼀个 FutureTask 类来供我们使⽤。

FutureTask 是 实现的 RunnableFuture 接⼝的,⽽ RunnableFuture 接⼝同时继承了 Runnable 接⼝ 和 Future 接⼝:

public interface RunnableFuture<V> extends Runnable, Future<V> {
 /**
 * Sets this Future to the result of its computation
 * unless it has been cancelled.
 */
 void run();
}

class Task implements Callable<Integer>{
 	@Override
 	public Integer call() throws Exception {
 	// 模拟计算需要⼀秒
 	Thread.sleep(1000);
 	return 2;
 }
 
 public static void main(String args[]){
 	// 使⽤
 	ExecutorService pool = Executors.newCachedThreadPool();
 	//上面的demo是
 	//Task task=new Task()
 	//Future f=pool.submit(task)
 	FutureTask<Integer> futureTask = new FutureTask<>(new Task());
 	pool.submit(futureTask);
     //上文通过Future f取值,这里直接通过futureTask取值
 	System.out.println(futureTask.get());
 }
}

对比可以发现,submit中的参数如果是FutureTask类型则是没有返回值的,这里实际调用的是submit(Runnable task)方法,而上文调用的是submit(Callable task)方法。

在很多高并发的环境下,有可能Callable和FutureTask会创建多次,FutureTask能够确保任务只执行⼀次。(详情移至FutureTask源码)

4.匿名内部类

5.线程池

6.spring异步

三、线程中的常用方法

1.wait()等待

​ 调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回。**调用 wait()方法后,会释放对象的锁。**因此,wait 方法一般用在同步方法或同步代码块中。

2.sleep()睡眠

​ sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致 线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态

3.yield()让步

​ yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下, 优先级高的线程有更大的可能性成功竞争得到 CPU 时间片。

4.join()阻塞等待

​ join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞 状态,等到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。

5.notify()唤醒

​ Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象 上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调 用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继 续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞 争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

6.interrupt()中断

​ 中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这 个线程本身并不会因此而改变状态(如阻塞,终止等)。

四、线程池详解

1.线程池的体系结构

image-20220409111612434

说说几个比较重要的部分:

(1)Executor

线程池的顶级接口,只定义了一个执行无返回值任务的方法。

public interface Executor {
    void execute(Runnable command);
}

(2)ExecutorService

线程池次级接口,对Executor做了一些扩展,主要增加了关闭线程池执行有返回值任务批量执行任务等方法。一般都通过ExecutorService创建线程池。

(3)Executors

线程池工具类,定义了一系列快速实现线程池的静态方法,可以直接通过Executors.new~()创建,不过阿里手册是不建议使用这个类来新建线程池的,会发生OOM。

image-20220409113718289

newFixedThreadPool为例

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

image-20220409114021601

FixedThreadPool 被称为可重用固定线程数的线程池。

SingleThreadExecutor 是只有一个线程的线程池。

CachedThreadPool 是一个会根据需要创建新线程的线程池。

ScheduledThreadPoolExecutor 主要用来在给定的延迟后运行任务,或者定期执行任务。

(4)ThreadPoolExecutor

普通线程池类,这也是我们通常所说的线程池,创建线程池时一般用的就是ThreadPoolExecutorExecutorService,它包含最基本的一些线程池操作相关的方法实现。

线程池的主要实现逻辑都在这里面,比如线程的创建、任务的处理、拒绝策略等.

2.线程池的创建

(1)7大参数

image-20220409141337639

  • corePoolSize : 核心线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  • unit:keepAliveTime 参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :饱和/拒绝策略。

(2)拒绝策略

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy 抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy 调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。
  • ThreadPoolExecutor.DiscardPolicy 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy 此策略将丢弃最早的未处理的任务请求。

(3)阻塞队列

  • **ArrayBlockingQueue:**基于数组结构的有界阻塞队列,按先进先出对元素进行排序。
  • **LinkedBlockingQueue:**基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于 ArrayBlockingQueue。Executors.newFixedThreadPool 使用了该队列。
  • **PriorityBlockingQueue:**是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。
  • DelayQueue(缓存失效、定时任务 ):是一个支持延时获取元素的无界阻塞队列。
  • **SynchronousQueue:**不存储数据、可用于传递数据。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。

(4)大小如何设置

​ 对于计算密集型,设置 线程数 = CPU数 + 1,通常能实现最优的利用率。

​ 对于I/O密集型,网上常见的说法是设置 线程数 = CPU数 * 2

(5)一个Demo

public class Demo01 {
    public static void main(String[] args) {
        // 自定义线程池!工作 ThreadPoolExecutor
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,// int corePoolSize, 核心线程池大小(候客区窗口2个)
                5,// int maximumPoolSize, 最大核心线程池大小(总共5个窗口) 
                3,// long keepAliveTime, 超时3秒没有人调用就会释,放关闭窗口 
                TimeUnit.SECONDS,// TimeUnit unit, 超时单位 秒 
                new LinkedBlockingDeque<>(3),// 阻塞队列(候客区最多3人)
                Executors.defaultThreadFactory(),// 默认线程工厂
            	// 4种拒绝策略之一:
            	// 队列满了,尝试去和最早的竞争,也不会抛出异常!
                new ThreadPoolExecutor.DiscardOldestPolicy());  
        
        //队列满了,尝试去和最早的竞争,也不会抛出异常!
        try {
            // 最大承载:Deque + max
            // 超过 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(
                        Thread.currentThread().getName()+" ok");
                });
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

(6)执行流程

线程池执行流程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值