- 1.SimpleDateFormat线程不安全
- 2.双重检查锁的漏洞
- 3.volatile的原子性
- 4.死锁
- 4.1 缩小锁的范围
- 4.2 保证锁的顺序
- 5.没释放锁
- 6.HashMap导致内存溢出
- 7.使用默认线程池
- 8.@Async注解的陷阱
- 9.自旋锁浪费cpu资源
- 10.ThreadLocal用完没清空
@Service
public class SimpleDateFormatService {
public Date time(String time) throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.parse(time);
}
}
如果dataFormat 抽取成常量
@Service
public class SimpleDateFormatService {
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public Date time(String time) throws ParseException {
return dateFormat.parse(time);
}
}
dateFormat对象被定义成了静态常量,这样就能被所有对象共用。
如果只有一个线程调用time方法,也不会出现问题。
但Serivce类的方法,往往是被Controller类调用的, 会出现多个线程会同时调用time方法的情况。
而time方法会调用SimpleDateFormat类的parse方法:
@Override
public Date parse(String text, ParsePosition pos) {
...
Date parsedDate;
try {
parsedDate = calb.establish(calendar).getTime();
...
} catch (IllegalArgumentException e) {
pos.errorIndex = start;
pos.index = oldStart;
return null;
}
return parsedDate;
}
establish():
Calendar establish(Calendar cal) {
...
//1.清空数据
cal.clear();
//2.设置时间
cal.set(...);
//3.返回
return cal;
}
其中的步骤1、2、3是非原子操作。
解决方案:
- SimpleDateFormat类的对象不要定义成静态的,可以改成方法的局部变量。
- 使用ThreadLocal保存SimpleDateFormat类的数据。
- 使用java8的DateTimeFormatter类。
饿汉模式:
public class SimpleSingleton {
//持有自己类的引用
private static final SimpleSingleton INSTANCE = new SimpleSingleton();
//私有的构造方法
private SimpleSingleton() {
}
//对外提供获取实例的静态方法
public static SimpleSingleton getInstance() {
return INSTANCE;
}
}
使用饿汉模式的好处是:没有线程安全的问题,但带来的坏处也很明显。白白造成资源浪费.
懒汉模式:
public class SimpleSingleton2 {
private static SimpleSingleton2 INSTANCE;
private SimpleSingleton2() {
}
public static SimpleSingleton2 getInstance() {
if (INSTANCE == null) {
INSTANCE = new SimpleSingleton2();
}
return INSTANCE;
}
}
假如有多个线程中都调用了getInstance方法,那么都走到 if (INSTANCE == null) 判断时,可能同时成立,因为INSTANCE初始化时默认值是null。这样会导致多个线程中同时创建INSTANCE对象,即INSTANCE对象被创建了多次,违背了只创建一个INSTANCE对象的初衷。
public class SimpleSingleton4 {
private volatile static SimpleSingleton4 INSTANCE;
private SimpleSingleton4() {
}
public static SimpleSingleton4 getInstance() {
if (INSTANCE == null) {
synchronized (SimpleSingleton4.class) {
if (INSTANCE == null) {
INSTANCE = new SimpleSingleton4();
}
}
}
return INSTANCE;
}
}
3.volatile的原子性
能保证变量在多个线程中的可见性,它也能禁止指令重排,但是不能保证原子性。
可见性主要体现在:一个线程对某个变量修改了,另一个线程每次都能获取到该变量的最新值。
public class VolatileTest extends Thread {
private boolean stopFlag = false;
public boolean isStopFlag() {
return stopFlag;
}
@Override
public void run() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
stopFlag = true;
System.out.println(Thread.currentThread().getName() + " stopFlag = " + stopFlag);
}
public static void main(String[] args) {
VolatileTest vt = new VolatileTest();
vt.start();
while (true) {
if (vt.isStopFlag()) {
System.out.println("stop");
break;
}
}
}
}
使用synchronized + volatie 保证原子性
public class VolatileTest {
public int count = 0;
public synchronized void add() {
count++;
}
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for (int i = 0; i < 20; i++) {
new Thread() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
test.add();
}
}
;
}.start();
}
while (Thread.activeCount() > 2) {
//保证前面的线程都执行完
Thread.yield();
}
System.out.println(test.count);
}
}
4.死锁
死锁可能是大家都不希望遇到的问题,因为一旦程序出现了死锁,如果没有外力的作用,程序将会一直处于资源竞争的假死状态中。
public class DeadLockTest {
public static String OBJECT_1 = "OBJECT_1";
public static String OBJECT_2 = "OBJECT_2";
public static void main(String[] args) {
LockA lockA = new LockA();
new Thread(lockA).start();
LockB lockB = new LockB();
new Thread(lockB).start();
}
}
class LockA implements Runnable {
@Override
public void run() {
synchronized (DeadLockTest.OBJECT_1) {
try {
Thread.sleep(500);
synchronized (DeadLockTest.OBJECT_2) {
System.out.println("LockA");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class LockB implements Runnable {
@Override
public void run() {
synchronized (DeadLockTest.OBJECT_2) {
try {
Thread.sleep(500);
synchronized (DeadLockTest.OBJECT_1) {
System.out.println("LockB");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
一个线程在获取OBJECT_1锁时,没有释放锁,又去申请OBJECT_2锁。而刚好此时,另一个线程获取到了OBJECT_2锁,也没有释放锁,去申请OBJECT_1锁。由于OBJECT_1和OBJECT_2锁都没有释放,两个线程将一起请求下去,陷入死循环,即出现死锁的情况。
4.1 缩小锁的范围class LockA implements Runnable {
@Override
public void run() {
synchronized (DeadLockTest.OBJECT_1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (DeadLockTest.OBJECT_2) {
System.out.println("LockA");
}
}
}
class LockB implements Runnable {
@Override
public void run() {
synchronized (DeadLockTest.OBJECT_2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (DeadLockTest.OBJECT_1) {
System.out.println("LockB");
}
}
}
在获取OBJECT_1锁的代码块中,不包含获取OBJECT_2锁的代码。同时在获取OBJECT_2锁的代码块中,也不包含获取OBJECT_1锁的代码。
4.2 保证锁的顺序出现死锁的情况说白了是,一个线程获取锁的顺序是:OBJECT_1和OBJECT_2。而另一个线程获取锁的顺序刚好相反为:OBJECT_2和OBJECT_1。
class LockA implements Runnable {
@Override
public void run() {
synchronized (DeadLockTest.OBJECT_1) {
try {
Thread.sleep(500);
synchronized (DeadLockTest.OBJECT_2) {
System.out.println("LockA");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class LockB implements Runnable {
@Override
public void run() {
synchronized (DeadLockTest.OBJECT_1) {
try {
Thread.sleep(500);
synchronized (DeadLockTest.OBJECT_2) {
System.out.println("LockB");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
两个线程,每个线程都是先获取OBJECT_1锁,再获取OBJECT_2锁。
5.没释放锁在java中除了使用synchronized关键字,给我们所需要的代码块加锁之外,还能通过Lock关键字加锁。
使用synchronized关键字加锁后,如果程序执行完毕,或者程序出现异常时,会自动释放锁。
但如果使用Lock关键字加锁后,需要开发人员在代码中手动释放锁。
public class LockTest {
private final ReentrantLock rLock = new ReentrantLock();
public void fun() {
rLock.lock();
try {
System.out.println("fun");
} finally {
rLock.unlock();
}
}
}
代码中先创建一个ReentrantLock类的实例对象rLock,调用它的lock方法加锁。然后执行业务代码,最后再finally代码块中调用unlock方法。
但如果你没有在finally代码块中,调用unlock方法手动释放锁,线程持有的锁将不会得到释放。
6.HashMap导致内存溢出HashMap在实际的工作场景中,使用频率还是挺高的,比如:接收参数,缓存数据,汇总数据等等。
但如果你在多线程的环境中使用HashMap,可能会导致非常严重的后果。
@Service
public class HashMapService {
private Map hashMap = new HashMap<>();
public void add(User user) {
hashMap.put(user.getId(), user.getName());
}
}
在HashMapService类中定义了一个HashMap的成员变量,在add方法中往HashMap中添加数据。在controller层的接口中调用add方法,会使用tomcat的线程池去处理请求,就相当于在多线程的场景下调用add方法。
想多线程环境中使用HashMap的话使用ConcurrentHashMap。
7.使用默认线程池jdk1.5之后,提供了ThreadPoolExecutor类,用它可以自定义线程池。
线程池的好处:
- 降低资源消耗:避免了频繁的创建线程和销毁线程,可以直接复用已有线程。而我们都知道,创建线程是非常耗时的操作。
- 提供速度:任务过来之后,因为线程已存在,可以拿来直接使用。
- 提高线程的可管理性:线程是非常宝贵的资源,如果创建过多的线程,不仅会消耗系统资源,甚至会影响系统的稳定。使用线程池,可以非常方便的创建、管理和监控线程。
Executors类,给我们快速创建线程池。
- newCachedThreadPool:创建一个可缓冲的线程
- newFixedThreadPool:创建一个固定大小的线程池
- newScheduledThreadPool:创建一个固定大小,并且能执行定时周期任务的线程池。
- newSingleThreadExecutor:创建只有一个线程的线程池,保证所有的任务安装顺序执行。
- newFixedThreadPool:允许请求的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- newSingleThreadExecutor:允许请求的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- newCachedThreadPool:允许创建的线程数是Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
优先推荐使用ThreadPoolExecutor类,我们自定义线程池
ExecutorService threadPool = new ThreadPoolExecutor(
8, //corePoolSize线程池中核心线程数
10, //maximumPoolSize 线程池中最大线程数
60, //线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue(500), //队列
new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略
8.@Async注解的陷阱
@Async注解,我们可以通过它即可开启异步功能:
@EnableAsync
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
执行异步调用的业务方法加上@Async注解
@Service
public class CategoryService {
@Async
public void add(Category category) {
//添加分类
}
}
在controller方法中调用这个业务方法
@RestController
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
@PostMapping("/add")
public void add(@RequestBody category) {
categoryService.add(category);
}
}
@Async注解开启的异步功能,会调用AsyncExecutionAspectSupport类的doSubmit方法。
protected void doExecute(Runnable task) {
Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
thread.start();
}
使用@Async注解开启的异步功能,默认情况下,每次都会创建一个新线程。
高并发下会OOM.
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;
}
如果在高并发的情况下,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用完没清空用空间换时间: ThreadLocal
ThreadLocal为每个使用变量的线程提供了一个独立的变量副本,这样每一个线程都能独立地改变自己的副本,而不会影响其它线程所对应的副本。
public class CurrentUser {
private static final ThreadLocal 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();
}
}
public void doSamething(UserDto userDto) {
UserInfo userInfo = convert(userDto);
CurrentUser.set(userInfo);
...
//业务代码
UserInfo userInfo = CurrentUser.get();
...
}
在业务代码的第一行,将userInfo对象设置到CurrentUser,这样在业务代码中,就能通过CurrentUser.get()获取到刚刚设置的userInfo对象。特别是对业务代码调用层级比较深的情况,这种用法非常有用,可以减少很多不必要传参。
但在高并发的场景下,这段代码有问题,只往ThreadLocal存数据,数据用完之后并没有及时清理。
ThreadLocal即使使用了WeakReference(弱引用)也可能会存在内存泄露问题,因为 entry对象中只把key(即threadLocal对象)设置成了弱引用,但是value值没有。
public void doSamething(UserDto userDto) {
UserInfo userInfo = convert(userDto);
try{
CurrentUser.set(userInfo);
...
//业务代码
UserInfo userInfo = CurrentUser.get();
...
} finally {
CurrentUser.remove();
}
}
需要在finally代码块中,调用remove方法清理没用的数据。



