关于线程、多线程和线程池你必须知道的

线程

什么是线程?

线程是进程中负责程序执行的执行单元。一个进程中至少有一个线程

进程是操作系统中进行保护和资源分配的基本单位,操作系统分配资源以进程为基本单位.而线程是进程的组成部分,它代表了一条顺序的执行流.

创建线程的两种方式:

一、继承 Thread 类,扩展线程。

图片

继承Thread类,覆盖run()方法。在run()方法体中编写要完成的任务

创建线程对象并用start()方法启动线程。

二、实现 Runnable 接口

图片

应当熟知的问题:

两种创建线程的方式,用哪个好?

Java不支持类的多重继承,但允许调用多个接口。所以如果要继承其他类,当然是调用Runnable接口更好了。

run()和start()方法区别

run()方法只是线程的主体方法,和普通方法一样,不会创建新的线程。

调用start()方法,会启动一个新的线程,新线程会调用run()方法,线程才会开始执行。

多线程

什么是多线程呢?

即就是一个程序中有多个线程在同时执行。

通过下图来区别单线程程序与多线程程序的不同

单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如,去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。

多线程程序:即,若有多个任务可以同时执行。如,去网吧上网,网吧能够让多个人同时上网。

图片

使用多线程的优缺点:

优点:

1)适当的提高程序的执行效率(多个线程同时执行)。

2)适当的提高了资源利用率(CPU、内存等)。

缺点:

1)占用一定的内存空间。

2)线程越多CPU的调度开销越大。

3)程序的复杂度会上升。

应当熟知的问题:

在Java中wait和seelp方法的不同

wait()方法属于Object类,调用该方法时,线程会放弃对象锁,只有该对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

sleep()方法属于Thread类,sleep()导致程序暂停执行指定的时间,让出CPU,但它的监控状态依然保存着,当指定时间到了又会回到运行状态,sleep()方法中线程不会释放对象锁。

谈谈wait/notify/notifyAll()关键字的理解

这三个方法是 java.lang.Object 的 final native 方法,任何继承 java.lang.Object 的类都有这三个方法。它们是Java语言提供的实现线程间阻塞和控制进程内调度的底层机制.

wait():

导致线程进入等待状态,直到它被其他线程通过notify()或者notifyAll唤醒,该方法只能在同步方法中调用notify():

notify():

随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态,该方法只能在同步方法或同步块内部调用。

notifyAll():

解除所有那些在该对象上调用wait方法的线程的阻塞状态,同样该方法只能在同步方法或同步块内部调用。

调用这三个方法中任意一个,当前线程必须是锁的持有者,如果不是会抛出一个 IllegalMonitorStateException 异常。

什么导致线程阻塞?

一般线程阻塞

1)线程执行了Thread.sleep(int millsecond)方法,放弃CPU,睡眠一段时间,一段时间过后恢复执行;

2)线程执行一段同步代码,但无法获得相关的同步锁,只能进入阻塞状态,等到获取到同步锁,才能恢复执行;

3)线程执行了一个对象的wait()方法,直接进入阻塞态,等待其他线程执行notify()/notifyAll()操作;

4)线程执行某些IO操作,因为等待相关资源而进入了阻塞态,如System.in,但没有收到键盘的输入,则进入阻塞态。

5)线程礼让,Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或更高优先级的线程,但并不会使线程进入阻塞态,线程仍处于可执行态,随时可能再次分得CPU时间。

线程自闭,join()方法,在当前线程调用另一个线程的join()方法,则当前线程进入阻塞态,直到另一个线程运行结束,当前线程再由阻塞转为就绪态。

6)线程执行suspend()使线程进入阻塞态,必须resume()方法被调用,才能使线程重新进入可执行状态

synchronized原理

synchronized通过对象的对象头(markword)来实现锁机制,java每个对象都有对象头,都可以为synchronized实现提供基础,都可以作为锁对象,在字节码层面synchronized块是通过插入monitorenter monitorexit完成同步的。持有monitor对象,通过进入、退出这个Monitor对象来实现锁机制

volatile的原理

有volatile变量修饰的共享变量进行写操作的时候会多一条汇编代码,lock addl $0x0,lock前缀的指令在多核处理器下会将当前处理器缓存行的数据会写回到系统内存,这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。同时lock前缀也相当于一个内存屏障,对内存操作顺序进行了限制。

如何保证线程安全?

线程安全性体现在三方法:

1)原子性:提供互斥访问,同一时刻只能有一个线程数据进行操作。

JDK中提供了很多atomic类,如AtomicInteger\AtomicBoolean\AtomicLong,它们是通过CAS完成原子性。JDK提供锁分为两种:synchronized依赖JVM实现锁,该关键字作用对象的作用范围内同一时刻只能有一个线程进行操作。另一种是LOCK,是JDK提供的

代码层面的锁,依赖CPU指令,代表性是ReentrantLock。

2)可见性:一个线程对主内存的修改及时被其他线程看到。

JVM提供了synchronized和volatile,volatile的可见性是通过内存屏障和禁止重排序实现的,volatile会在写操作时,在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到主内存;会在读操作时,在读操作前加一条load指令,从内存中读取共享变量。

3)有序性:指令没有被编译器重排序。

可通过volatile、synchronized、Lock保证有序性。

线程如何关闭?

1 ) 使用标志位

2)使用stop()方法,但该方法就像关掉电脑电源一样,可能会发生预料不到的问题

3)使用中断interrupt()

public class Thread { 
    // 中断当前线程 
    public void interrupt(); 
    // 判断当前线程是否被中断 
    public boolen isInterrupt(); 
    // 清除当前线程的中断状态,并返回之前的值 
    public static boolen interrupted();   
} 

但调用interrupt()方法只是传递中断请求消息,并不代表要立马停止目标线程。

线程池

什么是线程池

线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

图片

为什么要使用线程池

在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

应当熟知的问题

Runnable和Callable的区别

在提交任务到线程池的时候,通常实现Runnable接口的使用execute()方法提交,实现Callable接口的使用submit()方法提交。最后,使用线程池后别忘了shutdown()

(1)Callable规定的方法是call(),Runnable规定的方法是run()

(2)Callable的任务执行后可返回值,而Runnable的不能返回值

(3)call方法可以抛出异常,run方法不可以

(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果,它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果

什么是 Executor 框架?

Executor框架在Java 5中被引入,Executor 框架是一个根据一组执行策略调用、调度、执行和控制的异步任务的框架。

无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架可以非常方便的创建一个线程池。

Executors为Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池。

使用Executor框架创建线程池

Executor框架最核心的类是ThreadPoolExecutor,线程池都是通过对ThreadPoolExecutor进行不同配置来实现的

ThreadPoolExecutor有四个重载的构造方法,参数最多的那一个重载的构造方法,有7个参数

1.corePoolSize 线程池中核心线程的数量

2.maximumPoolSize 线程池中最大线程数量

3.keepAliveTime 非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长

4.unit 第三个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等

5.workQueue 线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的

6.threadFactory 为线程池提供创建新线程的功能

7.handler 拒绝策略

Executor可以创建三个比较常见的线程池(线程池的种类)

FixedThreadPool,创建固定长度的线程池,每次提交任务创建一个线程,直到达到线程池的最大数量,线程池的大小不再变化,固定数量一次支付之后使用不再开销

SingleThreadExecutor,线程池中只有一个线程,所有任务按照添加顺序执行,任务之间不会相互干扰,结果更准确

CachedThreadPooll是一个”无限“容量的线程池,和缓存有关的线程池,如果池中没有空闲的线程,线程池就会为这个任务创建一个线程,如果有空闲的线程,就会使用已有的空闲线程执行任务,CachedThreadPool是一个比较通用的线程池,它在多数情况下都能表现出优良的性能。遇事不决,用缓存线程池

线程池执行策略

1.execute(执行)一个线程之后,如果线程池中的线程数未达到核心线程数,则会立马启用一个核心线程去执行

2.execute一个线程之后,如果线程池中的线程数已经达到核心线程数,且workQueue未满,则将新线程放入workQueue中等待执行

3.execute一个线程之后,如果线程池中的线程数已经达到核心线程数但未超过非核心线程数,且workQueue已满,则开启一个非核心线程来执行任务

4.execute一个线程之后,如果线程池中的线程数已经超过非核心线程数,则拒绝执行该任务

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值