JAVA中多线程-单例双重锁(DCL(Double Check Lock)双重锁检查)

  • Post author:
  • Post category:java




为什么使用多线程?

  1. 单核CPU:提高CPU和IO的利用率
  2. 多核CPU:提高CPU的利用率
  3. 互联网:高并发的需求
  4. 耗时的操作使用线程,提高应用程序响应
  5. 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求
  6. 多CPU系统中,使用线程提高CPU利用率
  7. 改善程序结构。一个既长又

    复杂的进程

    可以考虑

    分为多个线程

    ,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。


作用

:可以解决负载均衡问题,充分利用cpu资源。为了提高CPU的使用率,采用多线程的方式去同时完 成几件事情而互不干扰,



线程和进程的区别?


1.进程:


进程为程序的一次执行过程,是系统运行程序的基本单位。


2.线程:


线程是进程划分成的更小的运行单位。


3.区别:


Java main方法启动:执行了一个JVM进程,main方法所在的线程为主线程。

* 线程 进程
是否独立 不独立 独立
资源
资源管理 不方便 方便



Java内存模型(JMM)

MM定义程序中各个变量的访问规则,保证多个线程间可以有效地、正确地协同工作。

eg. 共享变量:volatile(使得变量成为共享变量)

非共享变量:不需volatile修饰

多线程三个特性:原子性、可见性和有序性。

在这里插入图片描述



一 .实现多线程

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

重写run方法,创建对象,调用start()方法启动线程

在这里插入图片描述


1,新生状态

– 用new关键字建立一个线程后,该线程对象就处于新生状态。

– 处于新生状态的线程有自己的内存空间,通过调用start()方法进入就绪状态。


2,就绪状态

– 处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU。

– 当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为 CPU “ 调 度”。


3,运行状态

– 在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞戒完成任何而死亡。

– 如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。


4,阻塞状态

– 处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,戒等待I/O设备等资源,将让

出CPU并暂时停止自己运行,进入阻塞状态。

– 在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,戒等

待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从

原来停止的位置开始继续执行。


5,死亡状态

死亡状态是线程生命周期中的最后一个阶段。

线程死亡的原因有三个

:

  1. 正常运行的线程完成了它的全部工作;
  2. 线程被强制性地终止,如通过stop方法来终止一个线程【不推荐使用】;
  3. 线程抛出未捕获的异常。


6,线程操作的相关方法

在这里插入图片描述


7,阻塞状态(sleep/yield/join方法)

1,有三种方法可以

暂停Thread执行


(1),sleep



不会释放锁,Sleep时别的线程也不可以访问锁定对象。


(2),yield

:

让出CPU的使用权,从运行态直接进入就绪态。让CPU重新挑选哪一个线程进入运行状态。


(3),join

:

当某个线程等待另一个线程执行结束后,才继续执行时,使调用该方法的线程在此之前执行完毕,也就是等待调用该方法的线程执行完毕后再往下继续执行



一、继承 Thread 类

  1. 创建 MyThread 类,让其继承 Thread 类并重写 run() 方法。
  2. 创建 MyThread 类的实例对象,即创建一个新线程。
  3. 调用 start() 方法,启动线程。
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("我是通过继承 Thread 类创建的多线程,我叫" + Thread.currentThread().getName());
    }
}

class TestMyThread {
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        myThread1.setName("Thread-1");
        MyThread myThread2 = new MyThread();
        myThread2.setName("Thread-2");
        MyThread myThread3 = new MyThread();
        myThread3.setName("Thread-3");

        myThread1.start();
        myThread2.start();
        myThread3.start();
    }
}

线程的执行顺序和代码中编写的顺序没有关系,线程的执行顺序是具有随机性的。



二、实现 Runnable 接口

Runnable 接口只有一个 run() 方法,源码如下:

public interface Runnable {
    public abstract void run();
}
  1. 创建 MyRunnable 类实现 Runnable 接口。
  2. 创建 MyRunnable 类的实例对象 myRunnable 。
  3. 把实例对象 myRunnable 作为参数来创建 Thread 类的实例对象 thread,实例对象 thread 就是一个新线程。
  4. 调用 start() 方法,启动线程。
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我是通过实现 Runnable 接口创建的多线程,我叫" + Thread.currentThread().getName());
    }
}

class TestMyRunnable {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

相比于继承 Thread 类的方法来说,

实现 Runnable 接口是一个更好地选择

,因为 Java 不支持多继承,但是可以实现多个接口。

有一点值得注意的是 Thread 类也实现了 Runnable 接口,这意味着构造函数 Thread(Runnable target) 不仅可以传入 Runnable 接口的对象,而且可以传入一个 Thread 类的对象,这样就可以将一个 Thread 对象中的 run() 方法交由其他线程进行调用。



三、线程池


超详细的线程池使用解析


SSM配置和使用ThreadPoolTaskExecutor线程池


SpringBoot使用@Async注解创建使用多线程


线程池的使用场景

Executors是一个

线程池工厂类

,里面有许多静态方法,供开发者调用。

Java 中线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。合理的使用线程池可以带来多个好处:

  1. 降低资源消耗。通过重复利用已创建的线程降低线程在创建和销毁时造成的消耗。
  2. 提高响应速度。当处理执行任务时,任务可以不需要等待线程的创建就能立刻执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。


线程池的实现原理

在这里插入图片描述

Java通过Executors创建线程池,分别为:


1.Executors.newCachedThreadPool()

/* 
 * 该方法返回一个可根据实际情况调整线程数量的线程池。
 * 线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。
 * 若所有线程均在工作,又有新任务的提交,则会创建新的线程处理任务。
 * 所有线程在当前任务执行完毕后,将返回线程池进行复用
 */
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程, 适用于服务器负载较轻,执行很多短期异步任务

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}


2,Executors.newFixedThreadPool(3)

/* 该方法返回一个固定线程数量的线程池,该线程池池中的线程数量始终不变。
 * 当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。
 * 若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务 
 * 默认等待队列长度为Integer.MAX_VALUE
 */
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制。

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


3.Executors.newSingleThreadExecutor()

/* 该方法返回一个只有一个线程的线程池。
 * 若多余一个任务被提交到线程池,任务会被保存在一个任务队列中,等待线程空闲,按先入先出顺序执行队列中的任务
 * 默认等待队列长度为Integer.MAX_VALUE
 */
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();


4.Executors.newScheduledThreadPool(1)

/*
 * 该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量
 */
ExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(1);


线程池核心的参数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }

  1. corePoolSize 线程池核心线程大小
  2. maximumPoolSize 线程池最大线程数量
  3. keepAliveTime空闲线程存活时间
  4. unit 空闲线程存活时间单位
  5. workQueue 工作队列
  6. threadFactory 线程工厂
  7. handler 拒绝策略


线程池四种拒绝任务策略


1、直接丢弃(DiscardPolicy)

2、丢弃队列中最早的任务(DiscardOldestPolicy)。

3、抛异常(AbortPolicy)

4、将任务分给调用线程来执行(CallerRunsPolicy)。


线程池使用

  1. 线程池的

    常规使用

    :创建线程池,调用execute(Runnable),submit(Runnable,Callable)
  2. 线程池的

    SpringBoot使用

    :使用@EnableAsync和@Async

在这里插入图片描述

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor; //JDK1.8提供的并发包
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
 *  线程池的常规使用:创建线程池,调用execute(Runnable),submit(Runnable,Callable)
 *  线程池的SpringBoot使用:使用@EnableAsync和@Async
 */
@Configuration
public class ExecutorConfig {
    private static final Logger logger= LoggerFactory.getLogger(ExecutorConfig.class);

    @Bean
    public Executor asynfServiceExecutor(){
        Executors.newCachedThreadPool();
        logger.info("start asyncService");
        ThreadPoolTaskExecutor threadPoolTaskExecutor=new ThreadPoolTaskExecutor();
        //核心线程数
        threadPoolTaskExecutor.setCorePoolSize(0);
        //最大线程数
        threadPoolTaskExecutor.setMaxPoolSize(Integer.MAX_VALUE);
        //队列最大长度
        threadPoolTaskExecutor.setQueueCapacity(0);
        //线程前缀
        threadPoolTaskExecutor.setThreadNamePrefix("gen-");
        //设置拒绝策略
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //设置关闭线程池策略
        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        return threadPoolTaskExecutor;
    }


}


应用举例:

一个火车站购票系统,有100张车票,由多个窗口售卖,直到卖完为止,这时程序中,需要使用到多线程,如果不对其进行限制的话,就会出现同一个座位卖两张票的情况。

单线程:简单来说,就是一个窗口售卖100张票

多线程:多个窗口,同事售卖100张票。



二.单例双重锁


描述


为了

保证线程的安全性

,往往要

以牺牲性能为代价

。为了兼得二者,前人进行了多番尝试,也确实创造出诸多有效方案,双重检查锁就是其中的一种。


DCL

:Double Check Lock(双重检查锁)。令人哭笑不得的是,其闻名原因不是因为有效性,而是行业标杆级的错误性。双重检查锁同时体现了同步中的独占性与可见性同等的重要性,因此成为多线程学习中必学的经典案例。


为什么需要第二次检查?

首先我们来分析没有第二次检查的情况:当userBean为null时,两个线程可以并发地进入if语句内部。 然后,

一个线程进入

synchronized块来初始化userBean,而另一个线程则被阻断。当第一个线程退出synchronized块时,等待着的线程进入并创建另一个DBHelper对象。当

第二个线程进入

synchronized 块时,它并没有检查 instance 是否非 null。因此我们需要对instance进行第二次非空检查,这也是“双重检查加锁”名称的由来。


例子

public class UserBean {

    private static UserBean userBean=null;

    private UserBean() {
    }

    //线程锁
    public static UserBean getInstance(){


        /** 采用双重检查加锁实例化单件*/
        //第一次检查
        if (Objects.isNull(userBean)){
            synchronized (UserBean.class){
                //第二次检查
                if (Objects.isNull(userBean)){
                    userBean=new UserBean();
                }
            }
        }

        return userBean;
    }
}



版权声明:本文为MyBlogHiHi原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。