搭建网站需要备案吗,上海专建贸易有限公司,app下载量统计查询,网站点赞怎么做前言对于从事后端开发的同学来说#xff0c;并发编程肯定再熟悉不过了。说实话#xff0c;在java中并发编程是一大难点#xff0c;至少我是这么认为的。不光理解起来比较费劲#xff0c;使用起来更容易踩坑。不信#xff0c;让继续往下面看。今天重点跟大家一起聊聊并发编… 前言对于从事后端开发的同学来说并发编程肯定再熟悉不过了。说实话在java中并发编程是一大难点至少我是这么认为的。不光理解起来比较费劲使用起来更容易踩坑。不信让继续往下面看。今天重点跟大家一起聊聊并发编程的10个坑希望对你有帮助。1. SimpleDateFormat线程不安全在java8之前我们对时间的格式化处理一般都是用的SimpleDateFormat类实现的。例如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);}
}如果你真的这样写是没问题的。就怕哪天抽风你觉得dateFormat是一段固定的代码应该要把它抽取成常量。于是把代码改成下面的这样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类调用的而Controller类的接口方法则会被tomcat的线程池调用。换句话说可能会出现多个线程调用同一个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是非原子操作。但如果cal对象是局部变量还好坏就坏在parse方法调用establish方法时传入的calendar是SimpleDateFormat类的父类DateFormat的成员变量public abstract class DateFormat extends Forma {....protected Calendar calendar;...
}这样就可能会出现多个线程同时修改同一个对象即dateFormat他的同一个成员变量即Calendar值的情况。这样可能会出现某个线程设置好了时间又被其他的线程修改了从而出现时间错误的情况。那么如何解决这个问题呢SimpleDateFormat类的对象不要定义成静态的可以改成方法的局部变量。使用ThreadLocal保存SimpleDateFormat类的数据。使用java8的DateTimeFormatter类。2. 双重检查锁的漏洞单例模式无论在实际工作还是在面试中都出现得比较多。我们都知道单例模式有饿汉模式和懒汉模式两种。饿汉模式代码如下public class SimpleSingleton {//持有自己类的引用private static final SimpleSingleton INSTANCE new SimpleSingleton();//私有的构造方法private SimpleSingleton() {}//对外提供获取实例的静态方法public static SimpleSingleton getInstance() {return INSTANCE;}
}使用饿汉模式的好处是没有线程安全的问题但带来的坏处也很明显。private static final SimpleSingleton INSTANCE new SimpleSingleton();一开始就实例化对象了如果实例化过程非常耗时并且最后这个对象没有被使用不是白白造成资源浪费吗还真是啊。这个时候你也许会想到不用提前实例化对象在真正使用的时候再实例化不就可以了这就是我接下来要介绍的懒汉模式。具体代码如下public class SimpleSingleton2 {private static SimpleSingleton2 INSTANCE;private SimpleSingleton2() {}public static SimpleSingleton2 getInstance() {if (INSTANCE null) {INSTANCE new SimpleSingleton2();}return INSTANCE;}
}示例中的INSTANCE对象一开始是空的在调用getInstance方法才会真正实例化。嗯不错不错。但这段代码还是有问题。假如有多个线程中都调用了getInstance方法那么都走到 if (INSTANCE null) 判断时可能同时成立因为INSTANCE初始化时默认值是null。这样会导致多个线程中同时创建INSTANCE对象即INSTANCE对象被创建了多次违背了只创建一个INSTANCE对象的初衷。为了解决饿汉模式和懒汉模式各自的问题于是出现了双重检查锁。具体代码如下public class SimpleSingleton4 {private static SimpleSingleton4 INSTANCE;private SimpleSingleton4() {}public static SimpleSingleton4 getInstance() {if (INSTANCE null) {synchronized (SimpleSingleton4.class) {if (INSTANCE null) {INSTANCE new SimpleSingleton4();}}}return INSTANCE;}
}需要在synchronized前后两次判空。但我要告诉你的是这段代码有漏洞的。有什么问题public static SimpleSingleton4 getInstance() {if (INSTANCE null) {//1synchronized (SimpleSingleton4.class) {//2if (INSTANCE null) {//3INSTANCE new SimpleSingleton4();//4}}}return INSTANCE;//5
}getInstance方法的这段代码我是按1、2、3、4、5这种顺序写的希望也按这个顺序执行。但是java虚拟机实际上会做一些优化对一些代码指令进行重排。重排之后的顺序可能就变成了1、3、2、4、5这样在多线程的情况下同样会创建多次实例。重排之后的代码可能如下public static SimpleSingleton4 getInstance() {if (INSTANCE null) {//1if (INSTANCE null) {//3synchronized (SimpleSingleton4.class) {//2INSTANCE new SimpleSingleton4();//4}}}return INSTANCE;//5
}原来如此那有什么办法可以解决呢答可以在定义INSTANCE是加上volatile关键字。具体代码如下public class SimpleSingleton7 {private volatile static SimpleSingleton7 INSTANCE;private SimpleSingleton7() {}public static SimpleSingleton7 getInstance() {if (INSTANCE null) {synchronized (SimpleSingleton7.class) {if (INSTANCE null) {INSTANCE new SimpleSingleton7();}}}return INSTANCE;}
}volatile关键字可以保证多个线程的可见性但是不能保证原子性。同时它也能禁止指令重排。双重检查锁的机制既保证了线程安全又比直接上锁提高了执行效率还节省了内存空间。此外如果你想了解更多单例模式的细节问题可以看看我的另一篇文章《单例模式真不简单》3. volatile的原子性从前面我们已经知道volatile是一个非常不错的关键字它能保证变量在多个线程中的可见性它也能禁止指令重排但是不能保证原子性。使用volatile关键字禁止指令重排前面已经说过了这里就不聊了。可见性主要体现在一个线程对某个变量修改了另一个线程每次都能获取到该变量的最新值。先一起看看反例public class VolatileTest extends Thread {private boolean stopFlag false;public boolean isStopFlag() {return stopFlag;}Overridepublic 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;}}}
}上面这段代码中VolatileTest是一个Thread类的子类它的成员变量stopFlag默认是false在它的run方法中修改成了true。然后在main方法的主线程中用vt.isStopFlag()方法判断如果它的值是true时则打印stop关键字。那么如何才能让stopFlag的值修改了在主线程中通过vt.isStopFlag()方法能够获取最新的值呢正例如下public class VolatileTest extends Thread {private volatile boolean stopFlag false;public boolean isStopFlag() {return stopFlag;}Overridepublic 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;}}}
}用volatile关键字修饰stopFlag即可。下面重点说说volatile的原子性问题。使用多线程给count加1代码如下public class VolatileTest {public volatile int count 0;public void add() {count;}public static void main(String[] args) {final VolatileTest test new VolatileTest();for (int i 0; i 20; i) {new Thread() {Overridepublic void run() {for (int j 0; j 1000; j) {test.add();}};}.start();}while (Thread.activeCount() 2) {//保证前面的线程都执行完Thread.yield();}System.out.println(test.count);}
}执行结果每次都不一样但可以肯定的是count值每次都小于20000比如19999。这个例子中count是成员变量虽说被定义成了volatile的但由于add方法中的count是非原子操作。在多线程环境中count的数据可能会出现问题。由此可见volatile不能保证原子性。那么如何解决这个问题呢答使用synchronized关键字。改造后的代码如下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() {Overridepublic 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 {Overridepublic 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 {Overridepublic 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 {Overridepublic 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 {Overridepublic 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 {Overridepublic 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 {Overridepublic 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 MapLong, Object hashMap new HashMap();public void add(User user) {hashMap.put(user.getId(), user.getName());}
}在HashMapService类中定义了一个HashMap的成员变量在add方法中往HashMap中添加数据。在controller层的接口中调用add方法会使用tomcat的线程池去处理请求就相当于在多线程的场景下调用add方法。在jdk1.7中HashMap使用的数据结构是数组链表。如果在多线程的情况下不断往HashMap中添加数据它会调用resize方法进行扩容。该方法在复制元素到新数组时采用的头插法在某些情况下会导致链表会出现死循环。死循环最终结果会导致内存溢出。此外如果HashMap中数据非常多会导致链表很长。当查找某个元素时需要遍历某个链表查询效率不太高。为此jdk1.8之后将HashMap的数据结构改成了数组链表红黑树。如果同一个数组元素中的数据项小于8个则还是用链表保存数据。如果大于8个则自动转换成红黑树。为什么要用红黑树答链表的时间复杂度是O(n)而红黑树的时间复杂度是O(logn)红黑树的复杂度是优于链表的。既然这样为什么不直接使用红黑树答树节点所占存储空间是链表节点的两倍节点少的时候尽管在时间复杂度上红黑树比链表稍微好一些。但是由于红黑树所占空间比较大HashMap综合考虑之后认为节点数量少的时候用占存储空间更多的红黑树不划算。jdk1.8中HashMap就不会出现死循环答错它在多线程环境中依然会出现死循环。在扩容的过程中在链表转换为树的时候for循环一直无法跳出从而导致死循环。那么如果想多线程环境中使用HashMap该怎么办呢答使用ConcurrentHashMap。7. 使用默认线程池我们都知道jdk1.5之后提供了ThreadPoolExecutor类用它可以自定义线程池。线程池的好处有很多比如降低资源消耗避免了频繁的创建线程和销毁线程可以直接复用已有线程。而我们都知道创建线程是非常耗时的操作。提供速度任务过来之后因为线程已存在可以拿来直接使用。提高线程的可管理性线程是非常宝贵的资源如果创建过多的线程不仅会消耗系统资源甚至会影响系统的稳定。使用线程池可以非常方便的创建、管理和监控线程。当然jdk为了我们使用更便捷专门提供了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()); //拒绝策略顺便说一下如果是一些低并发场景使用Executors类创建线程池也未尝不可也不能完全一棍子打死。在这些低并发场景下很难出现OOM问题所以我们需要根据实际业务场景选择。8. Async注解的陷阱之前在java并发编程中实现异步功能一般是需要使用线程或者线程池。线程池的底层也是用的线程。而实现一个线程要么继承Thread类要么实现Runnable接口然后在run方法中写具体的业务逻辑代码。开发spring的大神们为了简化这类异步操作已经帮我们把异步功能封装好了。spring中提供了Async注解我们可以通过它即可开启异步功能使用起来非常方便。具体做法如下1.在springboot的启动类上面加上EnableAsync注解。EnableAsync
SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}2.在需要执行异步调用的业务方法加上Async注解。Service
public 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方法。默认情况会走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的用法大致是这样的先创建一个CurrentUser类其中包含了ThreadLocal的逻辑。public class CurrentUser {private static final ThreadLocalUserInfo 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();}
}在业务代码中调用CurrentUser类。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方法清理没用的数据。