聊聊并发编程的10个坑( 五 )


开发spring的大神们,为了简化这类异步操作,已经帮我们把异步功能封装好了 。spring中提供了@Async注解,我们可以通过它即可开启异步功能,使用起来非常方便 。
具体做法如下:
1.在springboot的启动类上面加上@EnableAsync注解 。
@EnableAsync@SpringBootApplicationpublic class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}}2.在需要执行异步调用的业务方法加上@Async注解 。
@Servicepublic class CategoryService {@Asyncpublic void add(Category category) {//添加分类}}3.在controller方法中调用这个业务方法 。
@RestController@RequestMapping("/category")public class CategoryController {@Autowiredprivate CategoryService categoryService;@PostMapping("/add")public void add(@RequestBody category) {categoryService.add(category);}}这样就能开启异步功能了 。
是不是很easy?
但有个坏消息是:用@Async注解开启的异步功能,会调用AsyncExecutionAspectSupport类的doSubmit方法 。

聊聊并发编程的10个坑

文章插图
 
默认情况会走else逻辑 。
而else的逻辑最终会调用doExecute方法:
protected void doExecute(Runnable task) {Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));thread.start();}我去,这不是每次都会创建一个新线程吗?
没错,使用@Async注解开启的异步功能,默认情况下,每次都会创建一个新线程 。
如果在高并发的场景下,可能会产生大量的线程,从而导致OOM问题 。
建议大家在@Async注解开启的异步功能时,请别忘了定义一个线程池 。
9. 自旋锁浪费cpu资源在并发编程中,自旋锁想必大家都已经耳熟能详了 。
自旋锁有个非常经典的使用场景就是:CAS(即比较和交换),它是一种无锁化思想(说白了用了一个死循环),用来解决高并发场景下,更新数据的问题 。
而atomic包下的很多类,比如:AtomicInteger、AtomicLong、AtomicBoolean等,都是用CAS实现的 。
我们以AtomicInteger类为例,它的incrementAndGet没有每次都给变量加1 。
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}它的底层就是用的自旋锁实现的:
public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;}在do...while死循环中,不停进行数据的比较和交换,如果一直失败,则一直循环重试 。
如果在高并发的情况下,compareAndSwapInt会很大概率失败,因此导致了此处cpu不断的自旋,这样会严重浪费cpu资源 。
那么,如果解决这个问题呢?
答:使用LockSupport类的parkNanos方法 。
具体代码如下:
private boolean compareAndSwapInt2(Object var1, long var2, int var4, int var5) {if(this.compareAndSwapInt(var1,var2,var4, var5)) {return true;} else {LockSupport.parkNanos(10);return false;} }当cas失败之后,调用LockSupport类的parkNanos方法休眠一下,相当于调用了Thread.Sleep方法 。这样能够有效的减少频繁自旋导致cpu资源过度浪费的问题 。
10. ThreadLocal用完没清空在java中保证线程安全的技术有很多,可以使用synchroized、Lock等关键字给代码块加锁 。
但是它们有个共同的特点,就是加锁会对代码的性能有一定的损耗 。
其实,在jdk中还提供了另外一种思想即:用空间换时间 。
没错,使用ThreadLocal类就是对这种思想的一种具体体现 。
ThreadLocal为每个使用变量的线程提供了一个独立的变量副本,这样每一个线程都能独立地改变自己的副本,而不会影响其它线程所对应的副本 。
ThreadLocal的用法大致是这样的:
  1. 先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑 。
public class CurrentUser {private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();public static void set(UserInfo userInfo) {THREA_LOCAL.set(userInfo);}public static UserInfo get() {THREA_LOCAL.get();}public static void remove() {THREA_LOCAL.remove();}}
  1. 在业务代码中调用CurrentUser类 。
public void doSamething(UserDto userDto) {UserInfo userInfo = convert(userDto);CurrentUser.set(userInfo);...//业务代码UserInfo userInfo = CurrentUser.get();...}


推荐阅读