前言
控制并发的方法很多,从最基础的synchronized,juc中的lock,到数据库的行级锁,乐观锁,悲观锁,再到中间件级别的redis,zookeeper分布式锁。特别是初级程序员,对于所谓的锁一直都是听的比用的多,第一篇文章不深入探讨并发,更多的是一个入门介绍,适合于初学者,主题是“根据并发出现的具体业务场景,使用合理的控制并发手段”。
什么是并发
由一个大家都了解的例子引入我们今天的主题:并发
类共享变量遭遇并发
public class Demo { public Integer count = 0; public static void main(String[] args) { final Demo demo = new Demo(); Executor executor = Executors.newFixedThreadPool(10); for(int i=0;i<1000;i++){ executor.execute(new Runnable() { @Override public void run() { demo.count++; } }); } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("final count value:"+demo1.count); } }
final count value:973
本例中创建了一个初始化时具有10个线程的线程池,多线程对类变量count进行自增操作。这个过程中,自增操作并不是线程安全的,happens-before原则并不会保障多个线程执行的先后顺序,导致了最终结果并不是想要的1000
下面,我们把并发中的共享资源从类变量转移到数据库中。
充血模型遭遇并发
@Component public class Demo2 { @Autowired TestNumDao testNumDao; @Transactional public void test(){ TestNum testNum = testNumDao.findOne("1"); testNum.setCount(testNum.getCount()+1); testNumDao.save(testNum); } }
依旧使用多线程,对数据库中的记录进行+1操作
Demo2 demo2; public String test(){ Executor executor = Executors.newFixedThreadPool(10); for(int i=0;i<1000;i++){ executor.execute(new Runnable() { @Override public void run() { demo2.test(); } }); } return "test"; }
数据库的记录
id | count 1 | 344
初窥门径的程序员会认为事务最基本的ACID中便包含了原子性,但是事务的原子性和今天所讲的并发中的原子操作仅仅是名词上有点类似。而有点经验的程序员都能知道这中间发生了什么,这只是暴露了项目中并发问题的冰山一角,千万不要认为上面的代码没有必要列举出来,我在实际项目开发中,曾经见到有多年工作经验的程序员仍然写出了类似于上述会出现并发问题的代码。
贫血模型遭遇并发
@RequestMapping("testSql") @ResponseBody public String testSql() throws InterruptedException { final CountDownLatch countDownLatch = new CountDownLatch(1000); long start = System.currentTimeMillis(); Executor executor = Executors.newFixedThreadPool(10); for(int i=0;i<1000;i++){ executor.execute(new Runnable() { @Override public void run() { jdbcTemplate.execute("update test_num set count = count + 1 where id = '1'"); countDownLatch.countDown(); } }); } countDownLatch.await(); long costTime =System.currentTimeMillis() - start; System.out.println("共花费:"+costTime+" s"); return "testSql"; }
数据库结果: count : 1000 达到了预期效果
这个例子我顺便记录了耗时,控制台打印:共花费:113 ms
简单对比一下二,三两个例子,都是想对数据库的count进行+1操作,唯一的区别就是,后者的+1计算发生在数据库,而前者的计算依赖于事先查出来的值,并且计算发生在程序的内存中。而现在大部分的ORM框架,导致了写充血模型的程序员变多,不注意并发的话,就会出现问题。下面我们来看看具体的业务场景。
业务场景
- 修改个人信息
- 修改商品信息
- 扣除账户余额,扣减库存
业务场景分析
第一个场景,互联网如此众多的用户修改个人信息,这算不算并发?答案是:算也不算。
- 算,从程序员角度来看,每一个用户请求进来,都是调用的同一个修改入口,具体一点,就是映射到controller层的同一个requestMapping,所以一定是并发的。
- 不算,虽然程序是并发的,但是从用户角度来分析,每个人只可以修改自己的信息,所以,不同用户的操作其实是隔离的,所以不算“并发”。这也是为什么很多开发者,在日常开发中一直不注意并发控制,却也没有发生太大问题的原因,大多数初级程序员开发的还都是CRM,OA,CMS系统。
回到我们的并发,第一种业务场景,是可以使用如上模式的,对于一条用户数据的修改,我们允许程序员读取数据到内存中,内存计算修改(耗时操作),提交更改,提交事务。
//Transaction start User user = userDao.findById("1"); user.setName("newName"); user.setAge(user.getAge()+1); ...//其他耗时操作 userDao.save(user); //Transaction commit
这个场景变现为:几乎不存在并发,不需要控制,场景乐观。
为了严谨,也可以选择控制并发,但我觉得这需要交给写这段代码的同事,让他自由发挥。
第二个场景已经有所不同了,同样是修改一个记录,但是系统中可能有多个操作员来维护