Java-Java多线程

修订记录 版本 是否发布
2020-10-28 v1.0

一、 sleep() ⽅法和 wait() ⽅法区别和共同点.

  • 两者最主要的区别在于:sleep ⽅法没有释放锁,⽽ wait ⽅法释放了锁
  • 两者都可以暂停线程的执⾏。
  • Wait 通常被⽤于线程间交互/通信,sleep 通常被⽤于暂停执⾏。
  • wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或者
    notifyAll() ⽅法。sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(long
    timeout)超时后线程会⾃动苏醒。

二、为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能直接调⽤run() ⽅法?

new ⼀个 Thread,线程进⼊了新建状态;调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状
态,当分配到时间⽚后就可以开始运⾏了。 start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏
run() ⽅法的内容,这是真正的多线程⼯作。 ⽽直接执⾏ run() ⽅法,会把 run ⽅法当成⼀个 main
线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。
总结: 调⽤ start ⽅法⽅可启动线程并使线程进⼊就绪状态,⽽ run ⽅法只是 thread 的⼀个普通
⽅法调⽤,还是在主线程⾥执⾏。

三、synchronized 关键字

1、synchronized关键字最主要的三种使⽤⽅式

  • 修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁
  • 修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例,因为静态成员不属于任何⼀
    个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管new了多少个对象,只有
    ⼀份)。所以如果⼀个线程A调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程B需要调⽤
    这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态
    synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前
    实例对象锁。
  • 修饰代码块: 指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁。
    总结: synchronized 关键字加到 static 静态⽅法和 synchronized(class)代码块上都是是给 Class
    类上锁。synchronized 关键字加到实例⽅法上是给对象实例上锁。尽量不要使⽤
    synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

下⾯我以⼀个常⻅的⾯试题为例讲解⼀下 synchronized 关键字的具体使⽤。
⾯试中⾯试官经常会说:“单例模式了解吗?来给我⼿写⼀下!给我解释⼀下双重检验锁⽅式实现单例
模式的原理呗!”
双重校验锁实现对象单例(线程安全)

public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public synchronized static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进⼊加锁代码
if (uniqueInstance WX null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance WX null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

另外,需要注意 uniqueInstance 采⽤ volatile 关键字修饰也是很有必要。
uniqueInstance 采⽤ volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();
这段代码其实是分为三步执⾏:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址
    但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1i>3i>2。指令重排在单线程环境下不会出
    现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和
    3,此时 T2 调⽤ getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回
    uniqueInstance,但此时 uniqueInstance 还未被初始化。
    使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。

四、并发编程的三个重要特性

  1. 原⼦性 : ⼀个的操作或者多次操作,要么所有的操作全部都得到执⾏并且不会收到任何因素的
    ⼲扰⽽中断,要么所有的操作都执⾏,要么都不执⾏。 synchronized 可以保证代码⽚段的原
    ⼦性。
  2. **可⻅性 :**当⼀个变量对共享变量进⾏了修改,那么另外的线程都是⽴即可以看到修改后的最新
    值。 volatile 关键字可以保证共享变量的可⻅性。
  3. **有序性 :**代码在执⾏的过程中的先后顺序,Java 在编译器以及运⾏期间的优化,代码的执⾏顺
    序未必就是编写代码时候的顺序。 volatile 关键字可以禁⽌指令进⾏重排序优化。

五、说说 synchronized 关键字和 volatile 关键字的区别

synchronized关键字和volatile关键字比较

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定⽐synchronized关键字要好
    但是volatile关键字只能⽤于变量⽽synchronized关键字可以修饰⽅法以及代码块
  • synchronized关键字在JavaSE1.6之后进⾏了主要包括为了减少获得锁和释放锁带来的性能消耗
    ⽽引⼊的偏向锁和轻量级锁以及其它各种优化之后执⾏效率有了显著提升,实际开发中使⽤
    synchronized 关键字的场景还是更多⼀些。
  • 多线程访问volatile关键字不会发⽣阻塞,⽽synchronized关键字可能会发⽣阻塞
  • volatile关键字能保证数据的可⻅性,但不能保证数据的原⼦性。synchronized关键字两者都能**
    保证。
  • volatile关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽ synchronized关键字解决的是
    多个线程之间访问资源的同步性。

六、线程池

1、实现Runnable接⼝和Callable接⼝的区别

Runnable ⾃Java 1.0以来⼀直存在,但 Callable 仅在Java 1.5中引⼊,⽬的就是为了来处
理 Runnable 不⽀持的⽤例。 Runnable 接⼝不会返回结果或抛出检查异常,但是 Callable 接⼝可
以。所以,如果任务不需要返回结果或抛出异常推荐使⽤ Runnable 接⼝,这样代码看起来会更加简
洁。
⼯具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
( Executors.callable(Runnable task )或 Executors.callable(Runnable task,
Object resule) )。

2、执⾏execute()⽅法和submit()⽅法的区别是什么呢?
  1. execute() ⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与否;
  2. **submit() ⽅法⽤于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执⾏成功,**并且可以通过 Future 的 get() ⽅法来获取
    返回值, get() ⽅法会阻塞当前线程直到任务完成,⽽使⽤ get(long timeout,
    TimeUnit unit) ⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏
    完。
3. 如何创建线程池

《阿⾥巴巴Java开发⼿册》中强制线程池不允许使⽤ Executors 去创建,⽽是通过
ThreadPoolExecutor 的⽅式,这样的处理⽅式让写的同学更加明确线程池的运⾏规则,规避资源耗尽
的⻛险

Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列⻓度为 Integer.MAX_VALUE
    ,可能堆积⼤量的请求,从⽽导致OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE
    ,可能会创建⼤量线程,从⽽导致OOM。

⽅式⼀:通过构造⽅法实现

image-20201028163943751
image-20201028163943751

⽅式⼆:通过Executor 框架的⼯具类Executors来实现 我们可以创建三种类型的
ThreadPoolExecutor:

  • FixedThreadPool : 该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不
    变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被
    暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: ⽅法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线
    程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务
  • CachedThreadPool: 该⽅法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数
    量不确定,但若有空闲线程可以复⽤,则会优先使⽤可复⽤的线程。若所有线程均在⼯作,⼜有
    新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执⾏完毕后,将返回线程池进
    ⾏复⽤。

对应Executors⼯具类中的⽅法如图所示:

image-20201028164121977
image-20201028164121977

4. ThreadPoolExecutor 类分析

ThreadPoolExecutor 构造函数重要参数分析

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核⼼线程数线程数定义了最⼩可以同时运⾏的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运⾏的线程数
    量变为最⼤线程数。
  • workQueue : 当新任务来的时候会先判断当前运⾏的线程数量是否达到核⼼线程数,如果达到
    的话,新任务就会被存放在队列中。

ThreadPoolExecutor 其他常⻅参数:

  1. keepAliveTime :当线程池中的线程数量⼤于 corePoolSize 的时候,如果这时没有新的任
    务提交,核⼼线程外的线程不会⽴即销毁,⽽是会等待,直到等待的时间超过了keepAliveTime 才会被回收销毁;
  2. unit : keepAliveTime 参数的时间单位。
  3. threadFactory :executor 创建新线程的时候会⽤到。
  4. handler :饱和策略。关于饱和策略下⾯单独介绍⼀下。

ThreadPoolExecutor 饱和策略

ThreadPoolExecutor 饱和策略定义:
如果当前同时运⾏的线程数量达到最⼤线程数量并且队列也已经被放满了任
时, ThreadPoolTaskExecutor 定义⼀些策略:

  • ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException 来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy :调⽤执⾏⾃⼰的线程运⾏任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应⽤程序可以承受此延迟并且你不能任务丢弃任何⼀个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy : 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。
posted @ 2022-08-25 09:28  编程秀  阅读(6)  评论(0编辑  收藏  举报