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





