线程:
创建线程的方法: 继承Thread类,实现Runable接口,重写run()方法,
线程的5种状态:新建、就绪、运行、阻塞、死亡状态。
wait()/ notify()/ notifayAll()三者的区别:
wait():线程处于等待状态,
notify():唤醒当前等待的线程
notify All():唤醒所有处于等待的线程。
线程同步:
线程同步保证数据的原子性, 使数据不受其他线程的干扰( 一次只允许一个线程执行, 其他线程处于等待, 等待该线程执行完其他线程才可继续执行)
Synchronized与lock的区别:
两者都是属于同步锁,Synchronized代码执行完成以后会自动释放锁,lock需要手动开启同步,同时也需要手动释放锁。
synchronized同步锁的执行原理:
首先有一个线程已经拿到了锁,其他线程已经有cup执行权,一直排队,等待释放锁。
当代码执行完毕或者程序抛出异常都会被释放掉。
锁已经被释放掉的话,其他线程开始进行抢锁(资源竞争),谁抢到谁进入同步中去,其他线程继续等待。
synchronized自动释放锁:当代码执行结束、抛出异常、遇到wait方法会自动释放锁。
同步锁:synchronized 单机锁, 在分布式环境下会导致锁失效,
死锁:
synchronized在内存不足的情况下、交叉锁和遇到数据库行级锁(比如某个线程执行for update 语句退出了事物,其它线程访问该数据库时都将陷入死锁)
等情况都会产生死锁。
交叉锁示例:
synchronized(true){
synchronized(true){
//*****
}
}
lock的底层实现原理:
lock锁底层使用的原理是用的AQS实现,是一种重量级锁
lock的存储结构由两部分组成:1一个是锁的状态变更,一个是链表,存放正在等待的线程。
lock加锁的过程:通过CAS获取锁,如果没有获取到则将线程放入等待链表中,当锁释放的时候修改锁的状态,
AQS的核心思想:
AQS是一个队列式的同步器,如果当前锁被占用获取不到的锁放在队列中,踩用的是CLH 队列锁(CLH 锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性)
分布式锁:
分布式锁有redis中的redisson已经zeekeper的分布式锁。
redisson底层原理:
获取锁对象:RLock lock = redisson.getLock(“anyLock”);
getLock(“anyLock”)anylock这个key会作为一把锁存放在redis的集群中,当第二个线程想要获取这把锁,首先会看这把锁在redis中存不存在,
如果存在则处于等待状态,否则继续加锁执行后续逻辑。每加一次锁,锁的value加1, 释放锁value值则减1,直到value等于0时,释放锁。
redisson的优势:
redisson底层使用lua脚本,且redisson底层会设置一个锁失效时间(默认30秒),当程序执行异常,导致无法执行lock.unlock()(释放锁),超过失效时间redisson会自动释放锁, redisson底层还有一个锁续命的逻辑, 当30S程序还没执行完,锁还被占用,则会再延长锁失效时间。
redisson的劣势:
redisson容易出现主从架构锁失效的问题, 当redis集群环境下, redisson加锁成功后会存放在redis集群中, redis会将锁从主服务同步到从服务,
如果redis在同步从服务的时( 并未同步从服务成功),主服务挂了,redis内部会选举一个从服务充当主服务, 若一个新线程进入加锁(因主服务挂了并未同步从服务成功)会导致重复加锁, 无法保证数据的原子性。
zookeeper的分布式锁解决了,redisson主从架构锁失效的问题。如下zookeeper详解。
springboot引入redisson的分布式锁
1、pom.xml引入redisson的包
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.2.12</version>
</dependency>
2、springboot的启动类中加上redisson服务
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379") //redis的服务地址
.setPassword("123456"); //redis的密码
return Redisson.create(config);
}
3、实现加锁
RLock lock = redisson.getLock("anyLock"); //获取锁对象
// 最常见的使用方法
lock.lock(); //加锁 底层使用lua脚本实现
lock.unlock()//释放锁
zookeeper的分布式锁底层原理:
zookeeper解决锁主从架构锁失效的情况,zookeeper加锁时会同步到其他从服务,同步成功后返回客户端加锁成功,继续执行后续逻辑, 保证每台从服务都能加到锁。
zookeeper的加锁原理:
zookeeper加锁底层用到了“临时顺序节点”,如客户A需要加锁“my_lock”,zookeeper会在锁的节点下创建一个有序的节点序号(“临时顺序节点”)依次向上递增。
创建完节点序号后, 会查“my_lock”锁节点下所有节点序号,判断当前创建的节点序号是否是最小的, 如果是则加锁成功, 失败则在它的上个节点序号上创建一个监听器,监听该节点是否有删除(释放锁)操作,如监听到删除操作,客户B会重新尝试加锁,重新查询锁节点的所有顺序节点, 如果当前是最小序号,则加锁成功。 否则继续等待。
加锁相似于一个排队的过程
流程图如下:
流程图
springboot引入zookeeper分布式锁
pom.xml
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<exclusions>
<!--因为 zk 包使用的是 log4j 日志,和 springboot 的logback 日志冲突 -->
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
SpringBoot 的 applicaiton 配置文件
zookeeper:
address: 127.0.0.1:2181 #zookeeper Server地址,如果有多个,使用","隔离
#例如 ip1:port1,ip2:port2,ip3:port3
retryCount: 5 #重试次数
elapsedTimeMs: 5000 #重试间隔时间
sessionTimeoutMs: 30000 #Session超时时间
connectionTimeoutMs: 10000 #连接超时时间
创建连接zookeeper客户端
@Configuration
public class ZookeeperConfig {
/**
* 创建 CuratorFramework 对象并连接 Zookeeper
* @param ZookeeperProperties 是读取 applicaiton 配置文件zookeeper的配置参数
* @return CuratorFramework
*/
@Bean(initMethod = "start")
public CuratorFramework curatorFramework(ZookeeperProperties zookeeperProperties) {
return CuratorFrameworkFactory.newClient(
zookeeperProperties.getAddress(),
zookeeperProperties.getSessionTimeoutMs(),
zookeeperProperties.getConnectionTimeoutMs(),
new RetryNTimes(zookeeperProperties.getRetryCount(),
zookeeperProperties.getElapsedTimeMs()));
}
创建分布式锁的工具类
@Slf4j
@Component
public class LockUtil {
@Autowired
CuratorFramework curatorFramework;
/**
* 节点名称
*/
public static final String NODE_PATH = "/lock-space/%s";
/**
* 尝试获取分布式锁
* @param key 分布式锁 key
* @param expireTime 超时时间
* @param timeUnit 时间单位
* @return 超时时间单位
*/
public InterProcessMutex tryLock(String key, int expireTime, TimeUnit timeUnit) {
try {
InterProcessMutex mutex = new InterProcessMutex(curatorFramework, String.format(NODE_PATH, key));
boolean locked = mutex.acquire(expireTime, timeUnit);
if (locked) {
log.info("申请锁(" + key + ")成功");
return mutex;
}
} catch (Exception e) {
log.error("申请锁(" + key + ")失败,错误:{}", e);
}
log.warn("申请锁(" + key + ")失败");
return null;
}
/**
* 释放锁
* @param key 分布式锁 key
* @param lockInstance InterProcessMutex 实例
*/
public void unLock(String key, InterProcessMutex lockInstance) {
try {
lockInstance.release();
log.info("解锁(" + key + ")成功");
} catch (Exception e) {
log.error("解锁(" + key + ")失败!");
}
}
创建 Controller 类并使用分布式锁
@Slf4j
@RestController
public class LockController {
/**
* curatorFramework对象
*/
@Autowired
private LockUtil lockUtil;
/**
* 线程池
*/
ExecutorService executor = Executors.newFixedThreadPool(10);//创建线程池
@GetMapping("/lock")
public void lockTest() {
for (int i = 0; i < 1000; i++) {
executor.submit(() -> { //启动线程
try {
String key = "test";
// 获取锁
InterProcessMutex lock = lockUtil.tryLock(key, 10, TimeUnit.SECONDS);
if (lock != null) {
// 如果获取锁成功,则执行对应逻辑
*******
// 释放锁
lockUtil.unLock(key, lock);
}
} catch (Exception e){
log.error("", e);
}
});
}
}
线程池:
线程池相当于一个池子, 容纳多个线程,重复利用以及创建好的线程。
线程优势:
降低资源消耗,减少线程频繁创建和销毁。
提高响应效率、不能等待创建线程的时间可立即执行
防止内存溢出、CPU耗尽
java里面的线程池的接口是Executor,Executor并不是一个线程池,而只是一个执行线程的工具,而真正的线程池是ExecutorService。
4种线程的创建
newCachedThreadPool创建一个可缓存线程池
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
线程池处理过程:
线程池判断核心线程池中是否有任务或者线程池中的线程是否都在执行任务,如果是则创建一个新的线程,
如线程池已满会进入阻塞队列等待执行
当阻塞队列满了且没有多余的空闲线程来执行,如果继续提交该任务,必须选择一种处理策略。线程池提供4种处理策略
处理线程的4种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。注:默认策略!!!!!!
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
使用线程池的风险:
例如同步错误导致死锁使线程迟迟不释放导致资源不足和线程泄露。
资源不足:如果线程池太大, 那么被消耗的线程会影响系统性能,在线程之中切换会浪费时间
线程泄露:如线程池中有一个线程去执行任务的过程中发生死锁导致线程迟迟没有回归池中, 会导致线程泄露
请求过载:同步的请求数量太多, 导致排在队列中等待执行的线程过多, 导致队列已满,线程池提供4种处理策略(见上述处理线程的4种策略)
配置线程池:
写一个线程配置类, 使用@Configuration注解,在spring启动时会扫描到这个配置累类,
类里面写一个处理方法,使用@bean对象标注,在spring启动时会初始化这个bean对象, 方法里面配置线程池的参数
线程池所用的参数:
线程池最小线程数、最大线程数、 线程池拒绝的处理策略(见上述处理线程的4种策略),空闲时间。
使用线程使用 @Autowired 注入线程对象, 即可使用调用submit方法或许excuter方法。
线程池的执行原理:
创建线程池:
/**
* 线程池配置
* @author: Chenkf
* @create: 2021/4/25
**/
@Configuration
public class ThreadPoolConfig {
@Value("${asyncPool.corePoolSize}")
private transient int corePoolSize; // 线程池维护线程的最少数量
@Value("${asyncPool.maxPoolSize}")
private transient int maxPoolSize; // 线程池维护线程的最大数量
@Value("${asyncPool.queueCapacity}")
private transient int queueCapacity; // 线程池所使用的缓冲队列
@Value("${asyncPool.keepAliveSeconds}")
private transient int keepAliveSeconds; // 线程池维护线程所允许的空闲时间
@Bean(name = "asyncTaskExecutor")
public TaskExecutor threadPoolTaskExecutor() {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池名的前缀
executor.setThreadNamePrefix("asyncExecutor-");
//线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new CallerRunsPolicy());
//调度器shutdown被调用时等待当前被调度的任务完成
executor.setWaitForTasksToCompleteOnShutdown(true);
//等待调度的任务完成的时长,单位秒
executor.setAwaitTerminationSeconds(300);
//线程池执行线程时,将Log MDC传递给子线程,以保证子线程日志能正常打印MDC中存储的内容
executor.setTaskDecorator(runnable -> {
Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
//在执行前执行MDC.setContextMap 传递给子线程
if (context != null) {
MDC.setContextMap(context);
}
try {
runnable.run();
} finally {
//需要清理
MDC.clear();
}
};
});
return executor;
}
}
使用线程池:
@Autowired
@Qualifier("asyncTaskExecutor")
private TaskExecutor taskExecutor;
taskExecutor.execute(() -> threadHandler(finalFileName, finalLocalFile, sxrsOrgInfo, transNo));