Java多线程知识

本文围绕Java线程展开,介绍了进程和线程的概念,阐述Java实现多线程的方法及相关方法、类和修饰符。讲解线程安全概念及易出现问题的类和解决方案。还介绍线程池,包括优点、Java线程池类型、相关参数、拒绝策略,以及Spring boot和Tomcat线程池配置。

一、进程和线程

进程(Process):是正在运行的程序的实例。

线程(Thread):是操作系统能够进行运算调度和分配资源的最小单位。

一个进程中可以并发多个线程,每条线程并行执行不同的任务。

二、Java的线程

1. Java实现多线程的方法:

1.1 继承Thread类

public class BusinessWindow extends Thread{
    //窗口编号
    private final int windowNum; 
    /**
     * @param windowNum 窗口编号
     */
    public BusinessWindow(int windowNum) {
        this.windowNum = windowNum;
    }
    @Override
    public void run() {
        Integer ticketNum = 0;
        while ((ticketNum = BusinessHall.getSeqNum()) != null) {
            System.out.println("【" + windowNum + "】号窗口 - 处理业务号码" + ticketNum);
        }
    }
}

执行线程:

BusinessWindow win1 = new BusinessWindow(1);
win1.start();

通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪状态,等待系统调度(等待CPU分配执行时间),并没有立即运行,一旦得到cpu时间片,就开始执行run()方法,run()方法是这个线程的主体内容,Run方法运行结束,此线程随即终止。run()方法只是thread类中的一个普通方法调用,还是在主线程里执行。其程序执行路径还是只有一条,还是要顺序执行

1.2、 实现Runnable接口

public class BusinessWindowRunnable implements Runnable {
    @Override
    public void run() {
        Integer ticketNum = 0;
        while ((ticketNum = BusinessHall.getSeqNum()) != null) {
            System.out.println("【" + Thread.currentThread().getName() + "】号窗口 - 处理业务号码" + ticketNum);
        }
    }

}

执行线程:

BusinessWindowRunnable winRunnable = new BusinessWindowRunnable();
t1.start();

2. 线程的方法、类、修饰符

wait()/wait(long timeout)

Object中的方法,调用此方法会使当前线程进组阻塞,直到有其他线程调用了Object的 notify 或者 notifyAll方法才能将其唤醒,或者祖师时间到达了 timeout而自动唤醒。wait方法必须在同步方法中使用

notify

用于唤醒wait方法阻塞的线程。必须在同步方法中使用。

sleep

与wait方法一样,都可以是当前线程进入阻塞状态。sleep是Thread特有的方法,但sleep不需要在同步方法中执行。sleep方法在休眠之后会主动退出阻塞,而wait方法则需要被其他线程中断后才能退出阻塞。

interrupt

打断当前线程的阻塞状态(wait,sleep,join等都会使当前线程进入阻塞状态)。线程内部存在这名为interrupt flag的标识, 如果一个线程调用了interrupt,flag将被设置。

isInterrupted

是Thread的一个成员方法,它主要判断当前线程是否被中断,仅仅是对interrupt标识的一个判断,并不会影响标识发生任何改变。

interrupted

是一个静态方法,虽然其他也用于判断当前线程是否被中断,但是他和成员方法isInterrupted有很大区别。调用该方法会直接擦除线程的interrupt标识。

join

join某个线程A,会使当前线程B进入等待,直到线程A结束生命周期,或者到达给定的时间,此期间线程B是出于阻塞状态。

ThreadLocal(线程局部变量)

为每个使用该变量的线程都提供一个该变量值的副本,每个线程都可以独立地改变此副本,而不会和其它线程的副本冲突。

volatile(修饰符)

多线程中用来修饰某个变量,这个变量只有一份,多个线程对它的读和写是唯一的,不会存在不一致的情况。

CountDownLatch

这个类能够使一个线程等待其他线程完成各自的工作后再执行。适用于对某个任务适用多线程拆分,每个线程执行一部分任务(子任务)。使其并行执行,挺高执行效率,减少整个任务执行时间。

四、线程安全

概念:线程安全就是当多线程访问资源(数据)时,为了防止出现数据不一致、错乱的情况(脏读)。采用对资源加锁的形式,使得同一时刻只有一个线程可以访问此资源,直到这个线程对资源的操作结束之后,其他线程才能对其访问。

常被提到的线程安全或不安全的类

JDK 1.5 增加了java.util.concurrent 包,这个包里有一系列能够让 Java 的并发编程变得更加简单的类。

五、容易出现的线程安全问题

SimpleDateFormat类

public class TimeUtils {
	
	/** 错误的使用方式,有线程安全问题 */
	private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	/**
	 * 错误的使用方式,有线程安全问题
	 */
	public static String formatNoFafe(Date date) {
		return sdf.format(date);
	}
}

解决方案:用jdk1.8的日期格式化类

public class TimeUtils {
	
	private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
	/**
	 * 使用1.8提供的日期时间API
	 */
	public static String formatSafe(Date date) {
		LocalDateTime ldt = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
		return ldt.format(dtf);
	}
}

六、线程池

线程池,通俗的理解就是有一个池子,里面存放着已经创建好的线程,当有任务提交给线程池执行时,池子中的某个线程会主动执行此任务。任务执行结束后,线程池中的线程被自动回收,释放资源。如果池子中的线程数量不够应付数量众多的任务时,则需要自动扩充新的线程到池子中,但是扩充的数量是有限的。

线程池的优点:

降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
提高响应速度:任务到达时,无需等待线程创建即可立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

Java线程池(Executor)

Excutor是线程池的顶级接口,真正线程池的接口是ExcutorService

Executors.newCachedThreadPool():核心线程为0,最大线程数为Integer. MAX_VALU
Executors.newFixedThreadPool(nThreads):固定大小的线程池。
Executors.newSingleThreadExecutor():单个线程的线程池。
Executors.newScheduledThreadPool():指定核心线程数的定时线程池 

线程池相关参数
corePoolSize:线程池的核心线程数。是预创建线程数,即在没有任务到来之前就创建corePoolSize个线程。当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
maximumPoolSize:线程池所能容纳的最大线程数
keepAliveTime:控制线程闲置时的超时时长,超过则终止该线程。

线程池的拒绝策略

  线程池中,有三个重要的参数,决定影响了拒绝策略:

corePoolSize - 核心线程数,也即最小的线程数。

workQueue - 阻塞队列 。 

maximumPoolSize - 最大线程数

  当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到 maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。

 

线程池共包括4种拒绝策略

ThreadPoolExecutor.AbortPolicy:拒绝任务并抛出异常 
ThreadPoolExecutor.DiscardPolicy:拒绝任务但不做任何动作 
ThreadPoolExecutor.CallerRunsPolicy:拒绝任务,并在调用者的线程中直接执行该任务 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃任务队列中的第一个(最老的)任务,然后把这个任务加进队列。 

ThreadPoolExecutor executor = new ThreadPoolExecutor(size, maxSize, 1, TimeUnit.DAYS, queue);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

Spring boot中的线程池

1. 在启动类上加注解@EnableAsync

@EnableAsync
@SpringBootApplication
public class ThreadSampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(ThreadSampleApplication.class, args);
	}

}

2. 在想要使用异步执行的方法上加注解@Async

    /**
     * 使用默认线程池
     */
    @Async
    public void doAsyncJob() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
                System.out.println(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

这样spring会使用默认线程池,默认情况下,最大线程数没有限制(Integer.MAX_VALUE),会来一个任务就创建一个线程,如果系统不断的创建任务,且线程没有及时销毁,有可能会使内存占用过高,导致OOM。

使用自定义线程池

/*
 * 线程池配置类
 */
@Data
@Component
public class ThreadPoolConfig {
    /** 线程池的名字 */
    public static final String THREAD_POOL_NAME = "Thread-Pool-1";
    private Integer corePoolSize = 10;//核心线程数
    private Integer maxPoolSize = 20;//最大线程数
    private Integer queueCapacity = 10;//队列大小
    private Integer keepAliveSeconds = 3600;//活跃时间
    private String threadNamePrefix = "test";//线程名字前缀
    /**
     * 如果queueCapacity满了,还有任务就会启动更多的线程,直到线程数达到maxPoolSize。如果超过此值,则根据拒绝策略进行处理。
     * 拒绝策略有多种:由任务调用线程执行,抛异常,多余的直接抛弃,根据FIFO(先进先出)抛弃队列里任务
     * @author zg
     * @date 2021-3-13 
     * @return
     */
    @Bean(name = THREAD_POOL_NAME)
    public Executor asyncServiceThreadPool() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);//线程池核心线程,正常情况下开启的线程数
        executor.setQueueCapacity(queueCapacity);//线程队列容量。当核心线程数都被占用,多余的任务会存到此处
        executor.setMaxPoolSize(maxPoolSize);
        executor.setKeepAliveSeconds(keepAliveSeconds);//设置线程活跃时间(线程池中空闲线程等待工作的超时时间)
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());// 设置拒绝策略
        executor.initialize();
        return executor;
    }
}
    /**
     * 使用自定义线程池
     */
    @Async(ThreadPoolConfig.THREAD_POOL_NAME)  //myTaskAsynPool即配置线程池的方法名,此处如果不写自定义线程池的方法名,会使用默认的线程池
    public void doJobWithPool() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
                System.out.println(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

 

Tomcat线程池

maxConnections、maxThreads、acceptCount三个主要配置,分别表示最大连接数,最大线程数、最大的等待数。在spring boot项目中,可以通过application.yml配置文件来修改

server:
  port: 8080
  tomcat:
    max-connections: 10000 #最大连接数
    accept-count: 100 #等待队列长度
    threads:
      max: 1000 #最大线程数
      min-spare: 100 #最小工作线程数

accept-count:等待队列长度
当所有的请求处理线程都在使用时,所能接收的连接请求的数量。当队列已满时,任何的连接请求都将被拒绝。默认值为100。

也就是说,当Tomcat处理的请求数达到最大线程数时,再有新的HTTP请求到来,这时tomcat会将该请求放在等待队列中,这个acceptCount就是指能够接受的最大等待数。如果等待队列满了,再来新的请求就会被tomcat拒绝(connection refused)。

maxThreads:最大线程数

每一次HTTP请求到达Web服务,tomcat都会创建一个线程来处理该请求,所以maxThreads决定了Web容器可以同时处理多少个请求,默认值200。不过增加线程是有成本的,太多的线程,会带来更多的线程上下文切换成本,同事带来更多的内存消耗。JVM中默认情况下在创建新线程时会分配大小为1M的线程栈,所以,更多的线程异味着需要更多的内存。根据经验:4核8g内存,线程数可以设置为800-1000。具体情况已实际性能测试为准。

maxConnections:最大连接数

同一时间tomcat能够接受的最大连接数。BIO模式,默认值是maxthreads的值;NIO模式,maxConnections 默认值是10000。

 

文章示例代码:https://gitee.com/funtaster/examples/tree/master/thread-sample

 

评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值