作者:书友48919914 | 来源:互联网 | 2023-09-16 09:09
前言
哈喽,大家好,我是Java选手牛皮糖。本周也是个值得兴奋的日子,没有征兆的下起雪来了。下了一整天的雪,可惜是在上班,不然定要约上三五好友去搓上一顿火锅。吃着火锅唱着歌,赏着雪不要太爽。
正文
上回一块学习了项目中十分常用的集合处理等,那这回我们就一块来看看并发处理。
并发处理
1、 【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
说明:资源驱动类、工具类、单例工厂类都需要注意。
单例相关:
定义:单例类只允许一个实例存在。
适用场景:
- 需要生成唯一序列的环境
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 方便资源相互通信的环境。
项目中使用的场景:
2、【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给
whatFeatureOfGroup
public class UserThreadFactory implements ThreadFactory { private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
UserThreadFactory(String whatFeatureOfGroup) {
namePrefix = "From UserThreadFactory's " + whatFeatureOfGroup + "-Worker-";
}
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement(); Thread thread = new Thread(null, task, name, 0, false); System.out.println(thread.getName());
return thread;
} }
3、 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
创建和销毁线程是非常浪费资源的事情,内存资源对Java来说是十分重要的,因此在使用线程的时候让线程池帮助解决这个问题。
4、【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 - CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
Executors部分源码
public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}
5、【强制】SimpleDateFormat 是线程不安全的类&#xff0c;一般不要定义为 static 变量&#xff0c;如果定义为 static&#xff0c;
必须加锁&#xff0c;或者使用 DateUtils 工具类。
正例:注意线程安全&#xff0c;使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df &#61; new ThreadLocal<DateFormat>(){ &#64;Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");
} };
static背景
static只能修饰内部类&#xff0c;且static在内存中只存储一份数据。被static修饰的内部类或变量是属于这个类的&#xff0c;而不是实例的。因此多线程情况下会存在数据不一致的问题。
SimpleDateFormat背景
SimpleDateFormat类中通过变量calendar存储时间&#xff0c;且SimpleDateFormat是线程不安全的类。如果使用static修饰SimpleDateFormat&#xff0c;则calendar将变成全局变量&#xff0c;则当多个线程同时进行SimpleDateFormat#parse、SimpleDateFormat#format方法时&#xff0c;就会出现线程不安全问题。
6、 【强制】必须回收自定义的 ThreadLocal 变量&#xff0c;尤其在线程池场景下&#xff0c;线程经常会被复用&#xff0c; 如果不清理自定义的 ThreadLocal 变量&#xff0c;可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用 try-finally 块进行回收。
正例:
objectThreadLocal.set(userInfo); try {
// …
} finally { objectThreadLocal.remove();
}
ThreadLocal背景
ThreadLocal是解决线程不安全的一种方式。它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题&#xff0c;是属于线程的。当线程结束的时候&#xff0c;ThreadLocal也会被回收&#xff0c;如果线程被复用了&#xff0c;则ThreadLocal不会被回收就造成了内存泄露。
7、【强制】高并发时&#xff0c;同步调用应该去考量锁的性能损耗。能用无锁数据结构&#xff0c;就不要用锁;能 锁区块&#xff0c;就不要锁整个方法体;能用对象锁&#xff0c;就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能的小&#xff0c;避免在锁代码块中调用 RPC 方法。最小加锁原则。
8. 【强制】对多个资源、数据库表、对象同时加锁时&#xff0c;需要保持一致的加锁顺序&#xff0c;否则可能会造 成死锁。
说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作&#xff0c;那么线程二的加锁顺序也必须是 A、 B、C&#xff0c;否则可能出现死锁。
9. 【强制】在使用阻塞等待获取锁的方式中&#xff0c;必须在 try 代码块之外&#xff0c;并且在加锁方法与 try 代 码块之间没有任何可能抛出异常的方法调用&#xff0c;避免加锁成功后&#xff0c;在 finally 中无法解锁。
说明一:如果在 lock 方法与 try 代码块之间的方法调用抛出异常&#xff0c;那么无法解锁&#xff0c;造成其它线程无法成功 获取锁。
说明二:如果 lock 方法在 try 代码块之内&#xff0c;可能由于其它方法抛出异常&#xff0c;导致在 finally 代码块中&#xff0c;unlock 对未加锁的对象解锁&#xff0c;它会调用 AQS 的 tryRelease 方法(取决于具体实现类)&#xff0c;抛出 IllegalMonitorStateException 异常。
说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常&#xff0c;产生的后果与说明二相同。
正例:
Lock lock &#61; new XxxLock();
lock.lock();
try {
doSomething();
doOthers(); } finally {
lock.unlock(); }
反例:
Lock lock &#61; new XxxLock();
try {
doOthers();
} finally { lock.unlock();
}
10.【强制】在使用尝试机制来获取锁的方式中&#xff0c;进入业务代码块之前&#xff0c;必须先判断当前线程是否
持有锁。锁的释放规则与锁的阻塞等待方式相同。
说明:Lock 对象的 unlock 方法在执行时&#xff0c;它会调用 AQS 的 tryRelease 方法(取决于具体实现类)&#xff0c;如果
当前线程不持有锁&#xff0c;则抛出 IllegalMonitorStateException 异常。
正例
Lock lock &#61; new XxxLock();
boolean isLocked &#61; lock.tryLock(); if (isLocked) {
try { doSomething();
doOthers(); } finally {
lock.unlock(); }
}
11.【强制】并发修改同一记录时&#xff0c;避免更新丢失&#xff0c;需要加锁。要么在应用层加锁&#xff0c;要么在缓存加锁&#xff0c;要么在数据库层使用乐观锁&#xff0c;使用 version 作为更新依据。
说明:如果每次访问冲突概率小于 20%&#xff0c;推荐使用乐观锁&#xff0c;否则使用悲观锁。乐观锁的重试次数不得小于 3 次。
乐观锁&#xff1a;每次写数据的时候&#xff0c;认为不会被别的线程操作&#xff0c;但是更新的时候会判断下数据有没有被改过。&#xff08;适用于多读少写的场景&#xff09;
悲观锁&#xff1a;每次写数据都认为会有别的线程操作&#xff0c;然后加锁。别的线程会一直被阻塞&#xff0c;直到拿到锁为止。&#xff08;适用于一致性比较高的场景)
12.【强制】多线程并行处理定时任务时&#xff0c;Timer 运行多个 TimeTask 时&#xff0c;只要其中之一没有捕获抛 出的异常&#xff0c;其它任务便会自动终止运行&#xff0c;使用 ScheduledExecutorService 则没有这个问题。
总结
高并发场景一致是项目中的难点&#xff0c;同时也是面试中面试官最喜欢面的地方。上文中每条规则都可以延伸很多&#xff0c;限于篇幅&#xff0c;这里就不做过多的分析。后面抽时间在和大家深入分下高并发的场景。