Java 实现并发的几种方式小结
Java实现并发的几种方法
Java程序默认以单线程方式运行。
synchronized
Java 用过synchronized 关键字来保证一次只有一个线程在执行代码块。
public synchronized void code() { // TODO }
Volatile
Volatile 关键字保证任何线程在读取Volatile修饰的变量的时候,读取的都是这个变量的最新数据。
Threads 和 Runnable
public class MyRunnable implements Runnable { @Override public void run() { // TODO } }
import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { Runnable task = new MyRunnable(); Thread worker = new Thread(task); worker.setName('Myrunnable'); worker.start(); }
创建thread会有很多overhead,性能低且不易管理
Thread pools
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Main { private static final int NUMOFTHREDS = 5; public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(NUMOFTHREDS); for (int i = 0; i < 50; i++) { Runnable worker = new MyRunnable(i); executor.execute(worker); } // executor不接受新的threads executor.shutdown(); // 等待所有threads结束 executor.awaitTermination(); System.out.println("Finished all threads"); } }
Futures 和 Callables
因为Runnable对象无法向调用者返回结果,我们可以用Callable类来返回结果。
package de.vogella.concurrency.callables; import java.util.concurrent.Callable; public class MyCallable implements Callable<Long> { @Override public Long call() throws Exception { // TODO int sum = 1; return sum; } }
import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class CallableFutures { private static final int NUMOFTHREDS = 5; public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(NUMOFTHREDS); List<Future<Long>> list = new ArrayList<Future<Long>>(); for (int i = 0; i < 10; i++) { Callable<Long> worker = new MyCallable(); Future<Long> submit = executor.submit(worker); list.add(submit); } long sum = 0; for (Future<Long> future : list) { try { sum += future.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } System.out.println(sum); executor.shutdown(); } }
CompletableFuture
CompletableFuture 在Future的基础上增加了异步调用的功能。callback()函数Thread执行结束的时候会自动调用。
CompletableFuture既支持阻塞,也支持非阻塞的callback()
import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class CompletableFutureSimpleSnippet { public static void main(String[] args) { CompletableFuture<Integer> data = createCompletableFuture() .thenApply((Integer count) -> { int transformedValue = count * 10; return transformedValue; }); try { int count = futureCount.get(); } catch (InterruptedException | ExecutionException ex) { } } private static CompletableFuture<Integer> createCompletableFuture() { CompletableFuture<Integer> futureCount = CompletableFuture.supplyAsync( () -> { return 1; }); return futureCount; } }
补充:Java如何处理高并发的情况
为了更好的理解并发和同步,需要先明白两个重要的概念:同步和异步
所谓同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其它的命令。 同步就是一件事,一件事情一件事的做。
异步,执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。异步就是,做一件事情,不影响做其他事情。
同步关键字synchronized,假如这个同步的监视对象是类的话,那么如果当一个对象 访问类里面的同步方法的话,那么其它的对象如果想要继续访问类里面的这个同步方法的话,就会进入阻塞,只有等前一个对象 执行完该同步方法后当前对象才能够继续执行该方法。这就是同步。相反,如果方法前没有同步关键字修饰的话,那么不同的对象可以在同一时间访问同一个方法,这就是异步。
脏数据:就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据(Dirty Data),依据脏数据所做的操作可能是不正确的。
1、什么是并发问题
多个进程或线程同时(在同一段时间内)访问同一资源会产生并发问题。
比如A、B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户减去 50元,A先提交,B后提交。 最后实际账户余额为1000-50=950元,但本该为 1000+100-50=1050。这就是典型的并发问题。如何解决?
处理并发和同同步问题主要是通过锁机制。
2、如何处理并发和同步
一种是java中的同步锁,典型的就是同步关键字synchronized。
另外一种比较典型的就是悲观锁和乐观锁。
在java中有两种方式实现原子性操作(即同步操作):
1)使用同步关键字synchronized
2)使用lock锁机制其中也包括相应的读写锁
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自 外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。
乐观锁,大多是基于数据版本 Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
乐观锁机制是在我们的系统中实现,来自外部系统的用户 余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如 将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途 径,而不是将数据库表直接对外公开)。
【谨防在此,面试官会问到死锁的相关问题!!!关于死锁的问题,在其余某篇博客都有说明】
3、常见并发同步案例分析
案例一、订票系统案例
某航班只有一张机票,假定有1w个人打开你的网站来订票,问你如何解决并发问题(可扩展到任何高并发网站要考虑的并发读写问题)
假定我们采用了同步机制或者数据库物理锁机制,如何保证1w个人还能同时看到有票,显然会牺牲性能,在高并发网站中是不可取的。
采用乐观锁即可解决此问题。乐观锁意思是不锁定表的情况下,利用业务的控制来解决并发问题,这样即保证数据的并发可读性又保证保存数据的排他性,保证性能的同时解决了并发带来的脏数据问题。
如何实现乐观锁:
前提:在现有表当中增加一个冗余字段,version版本号, long类型
原理:
1)只有当前版本号>=数据库表版本号,才能提交
2)提交成功后,版本号version ++
案例二、股票交易系统、银行系统,大数据量你是如何考虑的
首先,股票交易系统的行情表,每几秒钟就有一个行情记录产生,一天下来就有(假定行情3秒一个) 股票数量×20×60*6 条记录,一月下来这个表记录数量多大? 一张表的记录数超过100w后 查询性能就很差了,如何保证系统性能?
再比如,中国移动有上亿的用户量,表如何设计?把所有用于存在于一个表?
所以,大数量的系统,必须考虑表拆分-(表名字不一样,但是结构完全一样),通用的几种方式:(视情况而定)
1)按业务分,比如 手机号的表,我们可以考虑 130开头的作为一个表,131开头的另外一张表 以此类推
2)利用表拆分机制做分表
3)如果是交易系统,我们可以考虑按时间轴拆分,当日数据一个表,历史数据弄到其它表。这里历史数据的报表和查询不会影响当日交易。
此外,我们还得考虑缓存
这里的缓存独立于应用,依然是内存的读取,假如我们能减少数据库频繁的访问,那对系统肯定大大有利的。比如一个电子商务系统的商品搜索,如果某个关键字的商品经常被搜,那就可以考虑这部分商品列表存放到缓存(内存中去),这样不用每次访问数据库,性能大大增加。
4、常见的提高高并发下访问的效率的手段
首先要了解高并发的的瓶颈在哪里?
1、可能是服务器网络带宽不够
2.可能web线程连接数不够
3.可能数据库连接查询上不去。
根据不同的情况,解决思路也不同。
1、像第一种情况可以增加网络带宽,DNS域名解析分发多台服务器。
2、负载均衡,前置代理服务器nginx、apache等等
3、数据库查询优化,读写分离,分表等等
最后复制一些在高并发下面需要常常需要处理的内容
1、尽量使用缓存,包括用户缓存,信息缓存等,多花点内存来做缓存,可以大量减少与数据库的交互,提高性能。
2、用jprofiler等工具找出性能瓶颈,减少额外的开销。
3、优化数据库查询语句,减少直接使用hibernate等工具的直接生成语句(仅耗时较长的查询做优化)。
4、优化数据库结构,多做索引,提高查询效率。
5、统计的功能尽量做缓存,或按每天一统计或定时统计相关报表,避免需要时进行统计的功能。
6、能使用静态页面的地方尽量使用,减少容器的解析(尽量将动态内容生成静态html来显示)。
7、解决以上问题后,使用服务器集群来解决单台的瓶颈问题。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。