为什么使用多线程?
- 单核CPU:提高CPU和IO的利用率
- 多核CPU:提高CPU的利用率
- 互联网:高并发的需求
- 耗时的操作使用线程,提高应用程序响应
- 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求
- 多CPU系统中,使用线程提高CPU利用率
-
改善程序结构。一个既长又
复杂的进程
可以考虑
分为多个线程
,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
作用
:可以解决负载均衡问题,充分利用cpu资源。为了提高CPU的使用率,采用多线程的方式去同时完 成几件事情而互不干扰,
线程和进程的区别?
1.进程:
进程为程序的一次执行过程,是系统运行程序的基本单位。
2.线程:
线程是进程划分成的更小的运行单位。
3.区别:
Java main方法启动:执行了一个JVM进程,main方法所在的线程为主线程。
* | 线程 | 进程 |
---|---|---|
是否独立 | 不独立 | 独立 |
资源 | 少 | 多 |
资源管理 | 不方便 | 方便 |
Java内存模型(JMM)
MM定义程序中各个变量的访问规则,保证多个线程间可以有效地、正确地协同工作。
eg. 共享变量:volatile(使得变量成为共享变量)
非共享变量:不需volatile修饰
多线程三个特性:原子性、可见性和有序性。
一 .实现多线程
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
- 线程池
重写run方法,创建对象,调用start()方法启动线程
1,新生状态
– 用new关键字建立一个线程后,该线程对象就处于新生状态。
– 处于新生状态的线程有自己的内存空间,通过调用start()方法进入就绪状态。
2,就绪状态
– 处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU。
– 当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为 CPU “ 调 度”。
3,运行状态
– 在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞戒完成任何而死亡。
– 如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。
4,阻塞状态
– 处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,戒等待I/O设备等资源,将让
出CPU并暂时停止自己运行,进入阻塞状态。
– 在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,戒等
待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从
原来停止的位置开始继续执行。
5,死亡状态
死亡状态是线程生命周期中的最后一个阶段。
线程死亡的原因有三个
:
- 正常运行的线程完成了它的全部工作;
- 线程被强制性地终止,如通过stop方法来终止一个线程【不推荐使用】;
- 线程抛出未捕获的异常。
6,线程操作的相关方法
7,阻塞状态(sleep/yield/join方法)
1,有三种方法可以
暂停Thread执行
:
(1),sleep
:
不会释放锁,Sleep时别的线程也不可以访问锁定对象。
(2),yield
:
让出CPU的使用权,从运行态直接进入就绪态。让CPU重新挑选哪一个线程进入运行状态。
(3),join
:
当某个线程等待另一个线程执行结束后,才继续执行时,使调用该方法的线程在此之前执行完毕,也就是等待调用该方法的线程执行完毕后再往下继续执行
一、继承 Thread 类
- 创建 MyThread 类,让其继承 Thread 类并重写 run() 方法。
- 创建 MyThread 类的实例对象,即创建一个新线程。
- 调用 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();
}
- 创建 MyRunnable 类实现 Runnable 接口。
- 创建 MyRunnable 类的实例对象 myRunnable 。
- 把实例对象 myRunnable 作为参数来创建 Thread 类的实例对象 thread,实例对象 thread 就是一个新线程。
- 调用 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线程池
Executors是一个
线程池工厂类
,里面有许多静态方法,供开发者调用。
Java 中线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。合理的使用线程池可以带来多个好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程在创建和销毁时造成的消耗。
- 提高响应速度。当处理执行任务时,任务可以不需要等待线程的创建就能立刻执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
线程池的实现原理
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);
}
- corePoolSize 线程池核心线程大小
- maximumPoolSize 线程池最大线程数量
- keepAliveTime空闲线程存活时间
- unit 空闲线程存活时间单位
- workQueue 工作队列
- threadFactory 线程工厂
- handler 拒绝策略
线程池四种拒绝任务策略
1、直接丢弃(DiscardPolicy)
2、丢弃队列中最早的任务(DiscardOldestPolicy)。
3、抛异常(AbortPolicy)
4、将任务分给调用线程来执行(CallerRunsPolicy)。
线程池使用
-
线程池的
常规使用
:创建线程池,调用execute(Runnable),submit(Runnable,Callable) -
线程池的
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;
}
}