线程池—治理线程的法宝
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。
对特定数量的线程创建一个线程专用的池,那么我们来很多请求的时候,就可以通过线程池来分配,每次按线程池的数量进行处理,处理完之后,继续处理下一个线程请求,那么我们就不需要创建和销毁线程,也就是复用我们的线程,而且可以控制线程的总量。
如果不使用线程池,用for循环创建线程,当线程数量达到一定程度,那么我们反复创建并销毁线程所带来的开销就非常大,我们希望有固有数量的线程来执行这些。
为什么要用线程池
问题一:反复创建线程开销大
问题二:过多的线程会占用太多内存
解决上面两个问题的思路:
1.用少量的线程——避免内存占用过多
2.让这部分线程都保持工作,且可以反复执行任务——避免生命周期的损耗
线程池的好处:
1.加快响应速度
2.合理利用CPU和内存
3.统一管理资源
线程池适合应用的场合:
1.服务器接受大量请求时,使用线程池技术可以大大减少线程的创建和销毁次数,提高服务器的工作效率
2.在实际开发中,如果需要创建5个以上的线程,那么久可以使用线程池来管理
创建和停止线程池
线程池构造函数
corePoolSize:线程池在完全初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,再创建新线程去执行任务,这时会让线程池保持一定的默认线程数,这就是核心线程数。
maxPoolSize:线程池有可能在核心线程数的基础上,额外增加一些线程,但是线程数量有一个上限,这就是maxPoolSize,最大线程数。
添加线程规则:
1.如果线程小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来运行新任务。
2.如果线程数量大于等于corePoolSize,但小于任务队列的数量(maximumPoolSize),那么就会将任务放入队列。
3.如果队列已经满了,并且线程数小于maxPoolSize,则创建一个新线程来运行。
4.如果队列已经满了,并且线程数量大于等于maxPoolSize,则会拒绝该任务,用Handler。
所以是否需要增加线程的判断顺序是:corePoolSize, workQueue, maxPoolSize
增减线程的特点:
1.通过设置corePoolSize和maximumPoolSize相同,就可以创建固定大小的线程池
2.线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它
3.通过设置maximumPoolSize很高的值,可以允许线程池容纳任意数量的并发任务
4.如果队列是无界队列(LinkedBlockingQueue),那么线程数就不会超过corePoolSize,那么设置maxPoolSize也就毫无意义。
keepAliveTime:如果线程池当前的线程数多于corePoolSize,那么如果多余的线程空闲时间超过keepAliveTime,那么多余的部分就会被回收终止。
ThreadFactory:新的线程是由ThreadFactory创建的,默认使用Executors.defaultThreadFactory(),创建出来的线程都在同一线程组,并且拥有同样的NORM_PRIORITY优先级并且都不是守护线程,如果自己指定ThreadFactory,那么就可以改变线程名,线程组,优先级,是否是守护线程等等。
源码为:
/**
* Thread factory capturing access control context and class loader
*/
static class PrivilegedThreadFactory extends DefaultThreadFactory {
private final AccessControlContext acc;
private final ClassLoader ccl;
PrivilegedThreadFactory() {
super();
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Calls to getContextClassLoader from this class
// never trigger a security check, but we check
// whether our callers have this permission anyways.
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
// Fail fast
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
this.acc = AccessController.getContext();
this.ccl = Thread.currentThread().getContextClassLoader();
}
public Thread newThread(final Runnable r) {
return super.newThread(new Runnable() {
public void run() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
Thread.currentThread().setContextClassLoader(ccl);
r.run();
return null;
}
}, acc);
}
});
}
}
工作队列(work BlockingQueue):
1.直接交接:SynchronousQueue,这种线程无法存储任务,那么只要有新的任务进来,就直接创建新的线程去执行,那这种情况下,maxPoolSize就要设置稍微大一些,防止很快就到达最大值。
2.无界队列:LinkedBlockingQueue,这种线程没有边界,也就是上面说的只能保持在corePoolSize情况,好处是可以保证很多请求进来,不会出现拒绝任务的情况,但是也要能处理过来才可以,要不然很容易出现oom等错误。
3.有界队列:ArrayBlockingQueue,这种就是普通队列的情况。
线程池创建
手动创建线程池要比自动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源耗尽的风险。
自动创建线程池可能会带来的问题:
1.newFixedThreadPool
如果请求很多的情况下,jvm内存堆满了,那么就会报OOM错误
/**
* 代码演示newFixedThreadPool内存过多出错的情况
*/
public class FixedThreaedPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(4);
for (int i = 0; i < 1000000000; i++) {
executorService.execute(new Task());
}
}
}
class Task implements Runnable {
@Override
public void run() {
try {
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
原因是因为源码:
newFixedThreadPool使用无界队列,并且线程数量和最大数量相等(这里就算最大线程数量为更大也没有意义)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
2.newSingleThreadExecutor
/**
* newSingleThreadExecutor代码演示
*/
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 1000; i++) {
executorService.execute(new Task());
}
}
}
源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
可以看出和newFixedThreadPool很类似,只不过线程数量设置为1,但是这样也还是会和newFixedThreadPool出现一样的问题。
3.newCachedThreadPool
可缓存线程池,无界线程池,具有自动回收多余线程的功能
源码为:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
根据源码可以看到,之所以叫CacheThreadPool,是因为60s之后,清理掉空余的线程,但是问题在于,使用的是SynchronousQueue,那么就会直接创建新的线程,且最大线程为Integer.MAX_VALUE,那么线程数量达到一定程度,很可能会造成OOM。
4.newScheduledThreadPool
源码为:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
代码演示:
/**
* newScheduledThreadPool代码演示
*/
public class ScheduledExecutorService {
public static void main(String[] args) {
java.util.concurrent.ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
// 第一个参数是要执行的命令,第二个是延迟时间,第三个是延迟时间的单位
//threadPool.schedule(new Task(), 5, TimeUnit.SECONDS);
// 第一个参数是要执行的命令,第二个是初始延迟时间,也就是第一个执行的延迟时间,第三个是之后的任务间隔延迟时间,第四个是延迟时间的单位
threadPool.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS);
}
}
由于有DelayedWorkQueue,可以使用各种延迟应对方法来使线程延迟执行,也就是支持定时及周期性任务执行的线程池
经过上面的自动创建线程的方式,可以得知,正确创建线程池的方式,是使用手动创建,因为自动创建线程池会并不是完全符合自己业务需要,而且有时会出现一些问题,所以我们要根据不同的业务场景,自己设置线程池参数,比如内存多大,想给线程取什么名字等等。
首先线程池的线程数量的设定:
CPU密集型(加密,计算Hash等):最佳线程数为CPU核心数的1-2倍
耗时IO型(读写数据库,文件,网络读写等):最佳线程数一般会大于CPU核心数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法:
线程数 = CPU核心数 * (1 + 平均等待时间/平均工作时间)
停止线程池
1.shutdown()
shutdown()并不会立刻停止线程的进行,而是拒绝新的请求进来,将进行中的线程和队列中的任务都执行完毕后才会退出。
可通过isShutDown()来判断是否被关闭了
如果想查看线程池是否完全已经结束了,可以用isTerminated()来判断。
还有就是awaitTermination方法,来判断设置的时间内,是否结束,如果没结束就是false,反之true
代码演示:
/**
* shutdown,isShutDown,isTerminated,awaitTerminated用法演示
*/
public class ShutDownTask {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executorService.execute(new ShutDownTaskRunnable());
}
System.out.println(executorService.isShutdown());
Thread.sleep(1500);
executorService.shutdown();
System.out.println(executorService.isTerminated());
System.out.println(executorService.awaitTermination(1500, TimeUnit.MILLISECONDS));
System.out.println(executorService.isShutdown());
System.out.println(executorService.awaitTermination(10000, TimeUnit.MILLISECONDS));
Thread.sleep(10000);
System.out.println(executorService.isTerminated());
}
}
class ShutDownTaskRunnable implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
2.shutdownNow()
立刻关闭线程池,使用这个方法的时候,会对每个线程执行interrupted方法来通知线程关闭,然后返回队列中等待执行的请求,那我们接收到这个队列之后就可以进行之后的处理,比如放到数据库中,然后过段时间再进行处理之类的。
/**
* shutdownNow用法演示
*/
public class ShutDownTask {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executorService.execute(new ShutDownTaskRunnable());
}
Thread.sleep(1500);
List<Runnable> runnables = executorService.shutdownNow();
}
}
class ShutDownTaskRunnable implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "被中断了");
}
}
}
使用钩子来停止,暂停之类的操作
拒绝策略:
1.AbortPolicy
抛出异常,表示没有提交成功
2.DiscardPolicy
默默的将请求丢弃,且不会通知
3.DiscardOldestPolicy
丢弃掉现在执行的线程当中执行时间最长的线程,以便于来执行你所提交的请求
4.CallerRunsPolicy
让提交线程去执行,比如主线程来提交,那么就让主线程来执行,这种的好处就是没有业务数据损失,前面3种要么抛异常,要么丢弃,都会有损失,而且这种的方式可以延缓提交线程的次数,因为主线程在运行,那么主线程暂时不能提交线程,那么对于线程池来说就可以利用这个时间来执行任务,等到主线程再次提交的时候,线程池也有空间可以去执行。
利用钩子来暂停线程池的代码演示:
/**
* 线程池钩子演示
*/
public class PauseableThreadPool extends ThreadPoolExecutor {
// 暂停的标志位
private boolean isPaused;
private final Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
// 请求执行前的操作
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
lock.lock();
try {
while (isPaused) {
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void pause() {
lock.lock();
try {
isPaused = true;
} finally {
lock.unlock();
}
}
public void resume() {
lock.lock();
try {
isPaused = false;
condition.signalAll();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
PauseableThreadPool pauseableThreadPool = new PauseableThreadPool(20, 20, 10L, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("我被执行了");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10000; i++) {
pauseableThreadPool.execute(runnable);
}
Thread.sleep(1500);
pauseableThreadPool.pause();
System.out.println("线程池被暂停了");
Thread.sleep(1500);
pauseableThreadPool.resume();
System.out.println("线程池被启用了了");
}
}
Executor家族的解析
线程池的组成部分:
1.线程池管理器
2.工作线程
3.任务列队
4.任务接口(Task)
ThreadPoolExecutor,ExecutorService,Executor,Executors之间的关系:
Executor是一个顶层接口,里面只有一个方法 execute来执行任务
ExecutoerService是继承了Executor, 并且多了几个方法,比如shutdown之类的
Executors是一个工具类,里面可以直接创建出自动线程池之类的,类似于Collections
ThreadPoolExecutor就是我们的线程池了,但是返回的却是ExecutoerService,因为ThreadPoolExecutor是实现ExecutoerService,所以他俩是紧密相关的。
线程池的状态:
RUNNING:接受新任务并处理排队任务
SHUTDOWN:不接受新任务,但处理排队任务
STOP:不接受新任务,也不处理排队任务,并中断正在进行的任务
TIDYING:所有的任务都已经终止,workCount为0时,线程会转换到TIDYING状态,并将运行teminated()钩子方法
TERMINATED:terminate()运行完成
ThreadLocal
使用场景:
1.每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
每个Thread内有自己的实例副本,不共享,以SimpleDateFormat为例
1).2个线程分别用自己的SimpleDateFormat,这没什么问题
/**
* 两个线程打印日期
*/
public class ThreadLocalNomalUse {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNomalUse().date(10);
System.out.println(date);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNomalUse().date(104707);
System.out.println(date);
}
}).start();
}
public String date(int seconds) {
// 参数的单位是毫秒,从1970.1.1 00:00:00开始计时
Date date = new Date(seconds * 1000);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return simpleDateFormat.format(date);
}
}
2).后来延伸到1000个,那么必然要用到线程池(否则消耗内存太多)
3).所有的线程都用一个SimpleDateFormat对象,并且上锁
/**
* 1000个线程打印日期,用线程池来执行
*/
public class ThreadLocalNomalUse2 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNomalUse2().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
// 参数的单位是毫秒,从1970.1.1 00:00:00开始计时
Date date = new Date(seconds * 1000);
String format;
synchronized (ThreadLocalNomalUse2.class) {
format = simpleDateFormat.format(date);
}
return format;
}
}
但是更好的解决方案是用ThreadLocal,虽然可以用sychronized,但是会有性能问题。
用ThreadLocal,那么就会对每个线程new SimpleDateFormat,那么线程之间就不会互相争抢变量,而且也不会因为任务的数量来创建,且每个线程都会进行复用,既保证了唯一性,又节省了内存空间。
/**
* 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用了内存
*/
public class ThreadLocalNomalUse2 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNomalUse2().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
// 参数的单位是毫秒,从1970.1.1 00:00:00开始计时
Date date = new Date(seconds * 1000);
SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return simpleDateFormat.format(date);
}
}
class ThreadSafeFormatter {
// 普通写法
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
// Lambada表达式
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}
2.每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
当一个方法被一个service实现后,作为参数传导其他的service,但是如果层层传递,会导致代码冗余且不易维护。
那么在多线程的时候,我们希望每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦。而且每个线程自己的user对象是不一样的,并不能共用一个user对象,那么就需要用ThreadLocal保存一些业务内容。
/**
* 演示ThreadLocal用法2,避免传递参数的麻烦
*/
public class ThreadLocalNomalUseage {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
public void process() {
User user = new User("超哥");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("service2拿到" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("service3拿到" + user.name);
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
使用ThreadLocal带来的好处:
1.达到线程安全
2.不需要额外加锁,提高执行效率
3.更高效的利用内存,节省开销:相比于每个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
4.避免各种传参的繁琐,任何地方直接通过ThreadLocal拿到,不需要每次都传递参数,使得代码耦合度更低
ThreadLocal原理:
ThreadLocal主要方法:
1.initialValue()
该方法会返回当前线程对应的初始值,这是一个延迟加载的方法,只有在调用get的时候,才会触发
当线程第一个使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,这种情况下,不会为线程调用本initialValue方法
通常,每个线程最多调用一个此方法,但如果已经调用了remove后,再调用get,则可以再次调用此方法
2.void set(T t):为这个线程设置一个新值
源码为:
3.get():得到这个线程对应的value,如果是首次调用get(),则会调用initialize来得到这个值
get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
注意点:这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中,因为要根据Thread的key和value来从ThreadLocalMap中找到ThreadLocal
源码为:
4.remove():删除对应这个线程的值
源码为:
通过源码分析可以看出,setInitialValue和直接set最后都是利用map.set()方法来设置值,也就是说最后都会对应到ThreadLocalMap的一个Entry,只不过是起点和入口不一样。
ThreadLocalMap类
也就是Thread.threadLocals,ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,这里的键:这个ThreadLocal,值:实际需要的成员变量,比如user,simpleDateFormat对象。
ThreadLocalMap类发生冲突的时候,不会拉取拉链,而是找下一个空的地方进行保存。
TheadLocal注意点
1.内存泄漏
内存泄漏:某个对象不再有用,但是占用的内存却不能被回收
ThreadLocalMap中的Entry继承自WeakReference,是弱引用,弱引用的特点是,如果这个对象只被弱引用关联,那么这个对象就可以被回收,但是value是直接强引用,那么这个value就不会被回收,就可能会导致内存泄漏。
正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。
如果线程不终止,那么key对应的value就不能被回收,因为线程池的线程是反复使用的,那么很可能这个线程就会一直运行,那么这个Thread中的ThreadLocalMap里的ThreadLocal就一直被引用着,就会导致内存泄漏。
那么最后随着ThreadLocal的增加,导致回收不掉的内存越来越多,就会出现OOM,所以JDK考虑到这个问题,所以在set,remove,rehash方法中,扫描key == null的Entry,并把对应的value设置为null,这样value对象就可以被回收了。
但是如果一个ThreadLocal不被使用,那么实际上set,remove等方法也不会被调用,那么调用链还是可能会存在的。
所以避免内存泄漏的方法就是(阿里规约):
调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,调用remove方法。
或者在使用拦截器,在方法执行完毕之后,对应的ThreadLocal进行remove。
2.空指针异常
空指针异常的原因是因为如果你用基础数据类型来返回ThreadLocal的话会出现这种情况,因为ThreadLocal的泛型都是引用数据类型,所以要记住使用get时候用引用数据类型获取,然后进行判断即可,就不会抛异常了。
3.共享对象
如果在每个线程中ThreadLocal.set进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get取得的还是这个共享对象本身,还是有并发访问问题。
但是static对象本身就可以共用,这时候不应该放入到ThreadLocal里面去使用,这是不对的。
如果可以不使用ThreadLocal就可以解决问题,那么就用强行使用。
4.优先使用框架的支持
例如Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为很可能会忘记调用remove方法等问题。
每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景。
锁
Lock接口
锁是一种控制对共享资源的访问。
Lock和synchronized,是两种最常见的锁,他们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同。
Lock并不是用来代替synchronized的,而是当使用synchronized不适合或不足以满足要求的使用,用来提供高级功能的。
Lock接口最常见的实现类是ReentrantLock,通常情况下,Lock只允许一个线程来访问这个共享资源,但是也有特殊情况可以允许并发访问,比如ReadWriteLock里面的ReadLock。
Lock与synchronized一样,具有可见性的保证。
为什么synchronized不够用:
1.效率低:锁的释放情况比较少,试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程。
2.不够灵活(读写锁更灵活):加锁和释放锁的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。
3.无法知道是否成功获取到锁。
Lock主要方法介绍
1.lock()
lock()就是最普通的获取锁,如果锁已经被其他线程获取,则要进行等待,Lock不会像synchronized一样在异常时自动释放锁,因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定会被释放。
基本使用代码演示:
/**
* Lock不会像synchronized一样,异常的时候自动释放锁,所以在finally中释放锁
*/
public class MustUnlock {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
// 获取本锁保护的资源
System.out.println(Thread.currentThread().getName() + "开始执行任务");
} finally {
lock.unlock();
}
}
}
lock方法不能被中断,这会带来很大的隐患,一旦陷入死锁,lock就会陷入永久等待。所以就需要有一个可以尝试获取锁的方法,tryLock()
2.tryLock()
tryLock()尝试获取锁,如果没有被其他线程占用,则获取成功返回true,否则返回false,相比于lock,这样的方法显然功能更强大,我们可以根据是否能获取到锁来决定后续程序的行为。
该方法会立即返回,即便在拿不到锁时不会一直在那等。
还有一个兄弟方法tryLock(long time, TimeUnit unit):超时就放弃,会等待这个时候内如果拿到锁返回true,如果超时之后还是没有拿到锁,就会返回false。
代码演示:
/**
* 用tryLock来避免死锁
*/
public class TryLockDeadLock implements Runnable{
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
TryLockDeadLock thread1 = new TryLockDeadLock();
TryLockDeadLock thread2 = new TryLockDeadLock();
thread1.flag = 1;
thread2.flag = 2;
new Thread(thread1).start();
new Thread(thread2).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程1获取到了锁1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程1获取到了锁2");
System.out.println("线程1成功获取到两把锁");
break;
} finally {
lock2.unlock();
}
} else {
System.out.println("线程1获取锁2失败,已重试");
}
} finally {
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程1获取锁1失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 2) {
try {
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程2获取到了锁2");
Thread.sleep(new Random().nextInt(800));
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("线程2获取到了锁1");
System.out.println("线程2成功获取到两把锁");
break;
} finally {
lock1.unlock();
}
} else {
System.out.println("线程2获取锁1失败,已重试");
}
} finally {
lock2.unlock();
Thread.sleep(new Random().nextInt(800));
}
} else {
System.out.println("线程2获取锁2失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
还有一个类似tryLock的方法,lockIntertuptibly(),相当于设置时间为无限,在等待锁的过程中,线程可以被中断
代码演示:
public class LockInterrupt implements Runnable{
private Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
LockInterrupt lockInterrupt = new LockInterrupt();
Thread thread0 = new Thread((lockInterrupt));
Thread thread1 = new Thread((lockInterrupt));
thread0.start();
thread1.start();
Thread.sleep(2000);
thread0.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
Thread.sleep(4000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "等锁期间被中断了");
}
}
}
3.unlock
用来解锁的,但是要注意的是,我们对一个线程进行lock操作之后,一定要先对其try finally,然后在finally中对这个锁进行解锁操作。
锁的分类
锁的分类是从各种不同角度出发看的,这些分类并不是互斥的,也就是多个类型可以并存:有可能一个锁同时属于两种类型,比如ReentrantLock既是互斥锁,又是可重入锁。
乐观锁和悲观锁
这里乐观锁也叫做非互斥同步锁,悲观锁叫做互斥同步锁
互斥同步锁的劣势:
1.阻塞和唤醒带来的性能劣势
2.永久阻塞:如果持有锁的线程被永久阻塞,那么等待该线程释放锁的线程也将永远得不到执行
3.优先级反转:如果优先级低的线程持有锁之后但是运行很慢,那么优先级高的线程就需要长时间等待优先级低的线程完成。
**悲观锁:**如果我不锁住这个资源,别人就会来争抢,可能会导致数据结果错误,那么为了保证结果的正确性,每次获取并修改数据时,就会把数据锁住,让别人无法访问该数据,这样就可以用确保数据内容万无一失。
典型的例子就是synchronized和Lock相关类
**乐观锁:**认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作的对象,所以在更新的时候,会去对比在我修改期间数据有没有被其他人改变过,如果没被改变过,那么就说明真的是只有我自己在操作,那我就正常去修改数据。
如果数据和我一开始拿到的不一样了,那么就说明其他人改过数据,那我会选择放弃,重试,报错等策略,一般都是利用CAS算法来实现的。
乐观锁的典型例子就是原子类,并发容器等。
开销对比:
悲观锁的原始靠小要高于乐观锁,但是特点就是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响,相反,乐观锁一开始开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多。
使用场景:
悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗:
1.临界区有IO操作
2.临界区代码复杂或者循环量大
3.临界区竞争非常激烈
乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。
可重入锁和非可重入锁
可重入锁使用案例:
/**
* 演示多线程预定电影院座位
*/
public class CimemaBookSeat implements Runnable{
private static ReentrantLock lock = new ReentrantLock();
private static void bookSeat() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "开始预定座位");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "完成预定座位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new CimemaBookSeat()).start();
}
}
@Override
public void run() {
bookSeat();
}
}
Reentrant就是可重入的意思,可重入的意思是可以多次获取,也就是同一个线程可以多次获取同一把锁,synchronized也属于可重入锁,好处是可以避免死锁的发生,可以重复获取锁,如果无法重复获取,那么下一次再次获取的时候就会发生死锁,第二个好处是可以提高封装性,可以只关注使用。
代码演示:
/**
* 可重入锁演示
*/
public class GetHoldCount {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 可以重复的加锁,然后得到锁上的次数
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}
}
/**
* 递归调用
*/
public class RecursionDemo {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource() {
lock.lock();
try {
System.out.println("已经对资源进行了处理" + lock.getHoldCount());
if (lock.getHoldCount() < 5) {
accessResource();
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
源码分析:
ReentrantLock的其他方法:
isHeldByCurrentThread:可以看出锁是否被当前线程持有
getQueueLength:可以返回当前正在等待这把锁的队列有多长
这两个方法一般用于开发和调试,上线后用的不多。
公平锁和非公平锁
公平是指按照线程请求的顺序来分配锁,非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队。
这里的非公平同样不提倡插队,这里是指在合适的时机插队,而不是盲目插队
为什么要有非公平锁,因为这么设计的目的是为了提高效率,避免唤醒带来的空档期
公平的情况(以ReentrantLock为例):
如果在创建ReeentrantLock对象时,参数填写为true,那么这就是个公平锁。
假设线程1234按顺序调用lock()的:
如果是不公平的情况:
代码演示:
/**
* 演示公平和不公平两种情况
*/
public class FairLock {
public static void main(String[] args) throws InterruptedException {
PrintQueue printQueue = new PrintQueue();
Thread[] thread = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue));
}
for (int i = 0; i < 10; i++) {
thread[i].start();
Thread.sleep(100);
}
}
}
class Job implements Runnable {
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
PrintQueue printQueue;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始打印");
printQueue.printJob(new Object());
System.out.println(Thread.currentThread().getName() + "打印完毕");
}
}
class PrintQueue {
// 这里通过true, false来控制是否公平的情况
private Lock queueLock = new ReentrantLock(false);
public void printJob(Object document) {
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration + "秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
queueLock.lock();
try {
Long duration = (long) Math.random() * 10000;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration / 1000 + "秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
特例:
针对tryLock()方法,它不遵守设定的公平的规则,比如当有线程执行tryLock的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他正在等待队列里。
优缺点:
共享锁和排它锁
排它锁,又称独占锁,独享锁
共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据。
共享锁和排它锁的典型就是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁。
读写锁的作用:
在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源,因为多个读操作同时进行,是没有线程安全问题
在读的地方使用读锁,写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率。
读写锁的规则:
1.多个线程只申请读锁,都可以申请到
2.如果有一个线程已经占用了读锁,则其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
3.如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或读锁,则申请的线程会一直等待释放写锁
ReentrantReadWriteLock具体用法:
/**
* 利用电影院场景,实现读写锁
*/
public class CinemaReadWrite {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "我得到了读锁,正在读取");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放了读锁");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "我得到了写锁,正在读取");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放了写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(() -> read(), "Thread1").start();
new Thread(() -> read(), "Thread2").start();
new Thread(() -> read(), "Thread3").start();
new Thread(() -> write(), "Thread4").start();
new Thread(() -> write(), "Thread5").start();
new Thread(() -> write(), "Thread6").start();
}
}
读写锁插队策略:
ReentrantReadWriteLock与ReentrantLock一样,设置true,false就可以设置是否公平,那么也就是公平时候需要排队,不能插队,非公平时候有两种策略:
1.如果前面都是读锁,然后后面线程3是写锁,等待前面读锁结束,结果线程4,5都是读锁,那么因为读锁之间是可以并发的,所以让读锁直接去读取,那么线程3就还会继续在它们后面等待,这样并发读的性能提高了,但是会造成饥饿,如果一直有读的请求,那么这个线程3就会一直等待。
2.就算后面来了读请求,也会排在线程3之后,按照顺序去执行,那么就会避免出现饥饿的情况,但是如果是写锁,那么就会去尝试是否能获取锁,如果获取不到的情况下才会排队。
ReentrantReadWriteLock选择的是策略2,这样会避免出现饥饿的情况。
非公平锁中,写锁可以随时插队,但是读锁仅在等待队列头结点不是想获取写锁的线程的时候,才可以进行插队。
代码演示:
/**
* 描述: 演示非公平和公平的ReentrantReadWriteLock的策略
*/
public class NonfairBargeDemo {
// 这里通过true,false来演示公平和非公平
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}
private static void write() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->write(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->read(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
new Thread(()->read(),"Thread5").start();
new Thread(new Runnable() {
@Override
public void run() {
Thread thread[] = new Thread[1000];
for (int i = 0; i < 1000; i++) {
thread[i] = new Thread(() -> read(), "子线程创建的Thread" + i);
}
for (int i = 0; i < 1000; i++) {
thread[i].start();
}
}
}).start();
}
}
锁的升降级:
支持锁得到降级,不支持升级,代码演示:
/**
* 描述: 演示ReentrantReadWriteLock可以降级,不能升级
*/
public class Upgrading {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void readUpgrading() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
Thread.sleep(1000);
System.out.println("升级会带来阻塞");
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了写锁,升级成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}
private static void writeDowngrading() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
Thread.sleep(1000);
readLock.lock();
System.out.println("在不释放写锁的情况下,直接获取读锁,成功降级");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("先演示降级是可以的");
Thread thread1 = new Thread(() -> writeDowngrading(), "Thread1");
thread1.start();
thread1.join();
System.out.println("------------------");
System.out.println("演示升级是不行的");
Thread thread2 = new Thread(() -> readUpgrading(), "Thread2");
thread2.start();
}
}
为什么不能升级锁,因为有可能发生死锁,当两个线程同时都想升级成写锁,但是又都拿着读锁,需要对方把读锁给释放掉,那么就会出现死锁。
ReentrantLock适用于一般场合,ReentrantReadWriteLock适用于读多写少的情况,合理使用可以进一步提高并发效率。
自旋锁和阻塞锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
如果同步代码块中的内容过于简单,状态转换消耗的时间可能比代码执行的时间还要长,那么为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。
如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁,而为了让当前线程稍等一下,我们需要让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁。
阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒
自旋锁的缺点:
如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器资源
在自旋的过程中,一直消耗CPU,所以虽然自旋锁的其实开销低于悲观锁,但随着自旋时间的增长,开销也是线性增长的。
java1.5以上的并发框架java.util.concurrent的atoimc包下的类基本都是自旋锁的实现
AtomicInteger的实现:自旋锁的实现原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中,do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环,直到修改成功。
自旋锁代码演示:
/**
* 自旋锁
*/
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
System.out.println("自旋获取失败,重新尝试");
}
}
public void unlock() {
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
自旋锁适用场景:
自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
自旋锁适用于临界区较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不合适的
可中断锁
在Java总,synchronized就不是可中断锁,而Lock是可中断锁,因为tryLock和lockIntertuptibly都能响应中断
如果某一线程A正在执行锁中的代码,另一个线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其它事情,我们可以中断它,这种就是可中断锁。
public class LockInterrupt implements Runnable{
private Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
LockInterrupt lockInterrupt = new LockInterrupt();
Thread thread0 = new Thread((lockInterrupt));
Thread thread1 = new Thread((lockInterrupt));
thread0.start();
thread1.start();
Thread.sleep(2000);
// 中断线程来抛出异常
thread0.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
try {
// 这里就是可中断锁
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
Thread.sleep(4000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "等锁期间被中断了");
}
}
}
锁优化
Java虚拟机对锁的优化:
1.自旋锁和自适应
如果Java虚拟机发现自旋锁的自旋次数过多,那么就会让这个自旋锁变为阻塞锁,以防止浪费资源
2.锁消除
如果Java虚拟机发现这个地方不需要加锁就可以完成,且没有线程安全的问题,那么就会消除这个锁
3.锁粗化
如果Java虚拟机发现几个相邻锁的对象都是同一个,那么就会将这几个锁合为一个,减少申请锁和释放锁了
我们写代码时优化锁和提高并发性:
1.减小同步代码块
2.尽量不要锁住方法
3.减少请求锁的次数
4.避免人为制造“热点”
5.锁中尽量不要再包含锁
6.选择合适的锁类型或合适的工具类
原子类
一个操作是不可中断的,即便是多线程的情况下也可以保证
java.util.concurrent.atomic下的类基本都是原子类
原子类的作用和锁类似,是为了保证并发情况下线程安全,不过原子类相比于锁,有一定的优势:
粒度更细:原子变量可以把竞争范围缩小到变量级别,这时我们可以获得的最细粒度的情况了,通常锁的粒度都要大于原子变量的粒度
效率更高:通常,使用原子类的效率会比使用锁的效率更高,除了高度竞争的情况外
Atomic*基本原子类型
以AtomicInteger为例:
常用方法:
public final int get() // 获取当前的值
public final int getAndSet(int newValue) // 获取当前的值,并设置新的值
public final int getAndIncrement() // 获取当前的值,并自增
public final intgetAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) // 获取当前值,并加上预期的值
boolean compareAndSet(int expect, int update) // 如果当前的数值等于预期值,则以原子方式将该值设置为输入值(update)
代码演示:
/**
* 演示AtomicInteger的基本用法, 对比非原子类的线程安全问题
* 使用了原子类之后,不需要加锁,也可以保证线程安全
*/
public class AtomicIntegerDemo1 implements Runnable{
private static final AtomicInteger atomicInteger = new AtomicInteger();
public void incrementAtomic() {
atomicInteger.getAndAdd(-50);
// atomicInteger.getAndDecrement();
//atomicInteger.getAndIncrement();
}
private static volatile int basicCount = 0;
public synchronized void incrementBasic() {
basicCount++;
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerDemo1 r = new AtomicIntegerDemo1();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("原子类的结果是:" + atomicInteger.get());
System.out.println("普通类的结果是:" + basicCount);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
incrementAtomic();
incrementBasic();
}
}
}
Atomic*Array数组类型原子类
代码演示:
/**
* 演示原子数组的使用方法
*/
public class AtomicArray {
public static void main(String[] args) throws InterruptedException {
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(1000);
Increment increment = new Increment(atomicIntegerArray);
Decrement decrement = new Decrement(atomicIntegerArray);
Thread[] threadIncrement = new Thread[100];
Thread[] threadDecrement = new Thread[100];
for (int i = 0; i < 100; i++) {
threadIncrement[i] = new Thread(increment);
threadDecrement[i] = new Thread(decrement);
threadIncrement[i].start();
threadDecrement[i].start();
}
for (int i = 0; i < 100; i++) {
threadIncrement[i].join();
threadDecrement[i].join();
}
for (int i = 0; i < atomicIntegerArray.length(); i++) {
if (atomicIntegerArray.get(i) != 0) {
System.out.println("发现了错误" + i);
}
}
System.out.println("运行结束");
}
}
class Decrement implements Runnable {
private AtomicIntegerArray array;
public Decrement(AtomicIntegerArray array) {
this.array = array;
}
@Override
public void run() {
for (int i = 0; i < array.length(); i++) {
// array.getAndDecrement(i);
array.getAndAdd(i, -1);
}
}
}
class Increment implements Runnable {
private AtomicIntegerArray array;
public Increment(AtomicIntegerArray array) {
this.array = array;
}
@Override
public void run() {
for (int i = 0; i < array.length(); i++) {
array.getAndIncrement(i);
}
}
}
Atomic*Reference引用类型原子类
Atomic*Reference类的作用,和AtomicInteger并没有本质区别,只是保证的是一个对象的原子性,功能要比AtomicInteger强大,因为一个对象里可以包含很多属性,但是用法和AtomicInteger类似。
/**
* 自旋锁
*/
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
System.out.println("自旋获取失败,重新尝试");
}
}
public void unlock() {
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
把普通变量升级为具有原子功能
当偶尔需要原子操作的时候,那么就可以利用AtomicIntegerFieldUpdater对普通变量进行升级。
代码演示:
/**
* 演示AtomicIntegerFieldUpdater的用法
*/
public class AtomicIntegerFieldUpdaterDemo implements Runnable{
static Candidate tom;
static Candidate peter;
public static AtomicIntegerFieldUpdater<Candidate> scoreUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
peter.score++;
scoreUpdater.getAndIncrement(tom);
}
}
public static class Candidate {
volatile int score;
}
public static void main(String[] args) throws InterruptedException {
tom = new Candidate();
peter = new Candidate();
AtomicIntegerFieldUpdaterDemo runnable = new AtomicIntegerFieldUpdaterDemo();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("peter:" + peter.score);
System.out.println("tom:" + tom.score);
}
}
注意点:
1.变量一定是要可见的,如果是private,那么就不行了
2.不支持static
累加器
1.Adder累加器
Java8引入,相对比较新的一个类,高并发下LongAdder比AtomicLong效率高,不过本质是空间换时间
竞争激烈的时候,LongAdder把不同线程对应到不同的Cell上进行修改,降低了冲突的概率,是多段锁的概念,提高了并发性
AtomicLong代码演示:
/**
* 演示高并发场景下,LongAdder比AtomicLong性能好
*/
public class AtomicLongDemo {
public static void main(String[] args) throws InterruptedException {
AtomicLong atomicLong = new AtomicLong(0);
ExecutorService executorService = Executors.newFixedThreadPool(20);
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
executorService.submit(new Task(atomicLong));
}
executorService.shutdown();
while (!executorService.isTerminated()) {
}
long end = System.currentTimeMillis();
System.out.println(atomicLong.get() + ",时间为:" + (end - start));
}
private static class Task implements Runnable {
private AtomicLong counter;
public Task(AtomicLong counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
counter.incrementAndGet();
}
}
}
}
LongAdder代码演示:
/**
* 演示高并发场景下,LongAdder比AtomicLong性能好
*/
public class LongAdderDemo {
public static void main(String[] args) throws InterruptedException {
LongAdder longAdder = new LongAdder();
ExecutorService executorService = Executors.newFixedThreadPool(20);
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
executorService.submit(new Task(longAdder));
}
executorService.shutdown();
while (!executorService.isTerminated()) {
}
long end = System.currentTimeMillis();
System.out.println(longAdder.sum() + ",时间为:" + (end - start));
}
private static class Task implements Runnable {
private LongAdder counter;
public Task(LongAdder counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
}
}
}
相同的内容下,LongAdder整整比AtomicLong快了一个数量级,LongAdder大概是2000多ms,AtomicLong 300ms。
原因为:
AtomicLong每次加的时候,都需要将加完的数字flush到主内存或L3 Cache中,然后其他的线程才能够看到并且refresh到自己的核中进行计算,反复这样的操作很耗费资源。
LongAdder就不是这样的操作,每个线程会有自己的一个计数器,仅用来在自己线程内计数,这样就不会核其他线程的计数器干扰,也不用flush核refresh,那么速度当然会提升。
LongAdder引入了分段累加的概念,内部有一个base变量和一个Cell[]数组共同参与计数,竞争不激烈的时候直接累加到base变量上,如果竞争激烈,各个线程分散累加到自己的槽Cell[i]中,最后sum合到一起,sum源码为:
对比分析:
在低争用的时候,AtomicLong和LongAdder具有相似特征,但是竞争激烈的情况下,LongAdder的预期吞吐量要高得多,但要消耗更多得空间
LongAdder适合得场景是统计求和计数得场景,而且LongAdder基本只提供了add方法,而AtomicLong还具有cas方法。
2.Accumulator累加器
代码演示:
/**
* Accumulatorl累加器
*/
public class LongAccumulatorDemo {
public static void main(String[] args) {
// x是赋予的值,之后调用accumulate时,x得值变为y,然后执行x * y
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x * y, 1);
ExecutorService executorService = Executors.newFixedThreadPool(8);
IntStream.range(1, 10).forEach(i -> executorService.submit(() -> longAccumulator.accumulate(i)));
executorService.shutdown();
while (!executorService.isTerminated()) {
}
System.out.println(longAccumulator.getThenReset());
}
}
使用场景:
1.大量并行计算时,可以提高并行计算效率,利用多核的优势并行计算
2.计算先后顺序无关的情况,如果一定要用前面的值,那么Accumulator累加器就不适合使用
CAS原理
CAS运用场景是并发,Compare And Swap,实现不能被打断的数据交换操作,从而避免数据发生与预期不同的问题。
思路:当我读取到的值A经过更改后B,然后修改前会查看这个值是否还是原来的A,如果不是A,那么就是修改B的时候已经被别人更改了,那么就不更改成B了,避免多人同时修改导致出错
CAS有三个操作数:内存值V,预期值A,要修改的值B,并且当预期值A与内存值V相同时,才将内存值修改为B,否则什么都不做,最后返回现在的V
CAS是由CPU的特殊指令来保证不会发生线程安全,CPU进行比较然后在交换,如果CPU比较指令后不相等,那么就不会执行交换操作。
应用场景:
乐观锁,并发容器,原子类
Java中式如何利用CAS实现原子操作的:
1.AtomicInteger加载Unsafe工具,用来直接操作内存数据
2.用Unsafe来实现底层操作
3.用volatile修饰value字段,保证可见性
4.getAndAddInt方法分析
源码为:
其中Unsafe类是:Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问,不过尽管如此,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的,这样我们就能通过unsafe来实现CAS了。
Unsafe类中的compareAndSwapInt方法:
其中x就是要更新的值,e就是原值,通过addr进行汇编指令的互换操作。
那么步骤就是:先拿到变量value在内存中的地址,然后通过Atomic::cmpxchg实现原子性的比较和替换,其中参数x是即将更新的值,参数e是原内存的值,至此,最终完成了CAS的全过程。
CAS的缺点:
1.ABA问题
如果一个值从A变成B,但是又被另一个线程变成A,那么我这个线程更新的时候,发现原来的值A没有发生变化,那么认为可以更新了,其实值发生了两次变化,最后变回原来的值,那么这个情况我进行更新其实是有问题的,解决方法就是利用一个其他的变量来判断,比如数据库的版本号,每次更新时候对版本号比较,就不是对值进行比较,那么就会避免ABA的问题。
2.自旋时间过长
如果过于激烈,导致这个线程一直没办法更新,原来的值和预期的值一直被其他线程更改,那么就一直循环自旋重新更改,那么这个时候就会消耗CPU性能,浪费一些CPU资源。
不变性(Immutable)
如果对象在被创建后,状态就不能被修改,那么它就是不可变的。
具有不变性的对象一定是线程安全的,我们不需要对其采取任何额外的安全措施,也能保证线程安全。
final的作用
类防止被继承,方法防止被重写,变量防止被修改
天生是线程安全的,而不需要额外的同步开销
1.final修饰变量
被final修饰的变量,意味着值不能被修改,如果变量是对象,那么对象的引用不能变,但是对象自身的内容依然可以变化。
1).类中的final属性
2).类中的static final属性
3).方法中的final变量
他们的区别就是赋值的时机:
1).类中的final属性
直接再生命变量的等号右边直接赋值
在构造函数中赋值
在类的初始代码块中赋值(不常用)
必须采用其中一种赋值,而不能不赋值,这是final语法规定的
2).类中的static final属性
两种赋值时机:除了在声明变量的等号右边直接赋值外,还能在static初始代码块赋值,但是不能用普通的代码块赋值。
3).方法中的final变量
不会要求一定要赋值,但是在使用前必须要赋值,这与普通变量的赋值时机是一样的。
为什么要这样赋值,因为final的原则就是不变性,如果可以后续赋值,那么就是从null变为你的赋值,这样就不符合final的原则。
2.final修饰方法
构造方法不允许final修饰
不可被重写,也就是不能被override,即便是子类有同样名字的方法,也不是重写。
引申:static方法也不能被重写
但是static可以写重名的方法,虽然不能被重写,但是final同样名字的都不行
3.final修饰类
不能被继承,最常见的就是String
不变形和final的关系
不变性并不意味着,简单地用final修饰就是不可变
对于基本数据类型,确实被final修饰后就具有不变性
但对于对象类型,需要该对象保证自身被创建后,状态永远不会变才可以,比如一个对象里面有可变的属性,那么它还是可变的。
如果一个对象,你不可改变其内容,那么就可以说这个对象是不可变的
所以满足对象不可变的条件为:
1.对象创建后,其状态就不能修改
2.所有属性都是final修饰的
3.对象创建过程中没有发生逸出
栈封闭:
在方法里新建的局部变量,实际上是存储在每个线程私有的栈空间,而每个栈的空间是不能被其他线程所访问的,所以不会有线程安全,这就是栈封闭技术,是线程封闭技术的一种情况。
并发容器
ConcurrentHashMap:线程安全的HashMap
CopyOnWriteArrayList:线程安全的List
BlockingQueue:这是一个接口,表示阻塞队列,非常适用于作为数据共享的通道
ConcurrentLinkedQueue:高效的非阻塞并发队列,使用链表实现,可以看做一个线程安全的LinkedList
ConcurrentSkipListMap:是一个Map,使用跳表的数据结构进行快速查找
集合类的历史
Vector和Hashtable,也是线程安全的,但是线程并发性不够好,因为里面的方法几乎都是synchronized来修饰保护的,那么多线程下,线程虽然安全,但是性能就会大大折扣。
后来的ArrayList和HashMap,虽然线程不是安全的,但是可以用Collections.synchronizedList(new ArrayList())和Collections.synchronizedMap(new HashMap<K,V>())使之变成线程安全的。用synchronized代码块来实现,但是对比前两个,性能也没有快很多。
源码为:
所以ConcurrentHashMap和CopyOnWriteArrayList出现了,用来代替前面的情况,绝大部分并发情况,这两个的性能都更好,但是CopyOnWriteArrayList适合的是读多写少的情况,如果大部分都是更新,由于CopyOnWriteArrayList每次写入都会完整复制整个数组链表,所以这种情况性能并没有之前的好。
ConcurrentHashMap
Map
Map接口的实现类有:
HashMap:根据HashCode为键进行查找,且键可以为null,线程非安全
Hashtable:基本不使用,由ConcurrentHashMap代替
LinkedHashMap:保存了插入顺序,那么遍历的顺序就是和插入的顺序一致
TreeMap:可以按照键进行排序
HashMap是最常用的,因为满足了大部分的使用场景
为什么HashMap是线程不安全的:
1.同时put碰撞可能会导致数据丢失
因为两个都要put到一个地方,那么就会有一个的值丢失
2.同时put扩容导致数据丢失
扩容之后的数组只有一个能保留下来,那么另一个就会丢失
3.死循环造成的CPU100%(JDK 1.7以及之前出现)
由于大小是2,扩容因子是1.5f,那么前三个的时候还不会扩容,但是第四个put的时候就会触发扩容,
在多个线程同时扩容的时候,会造成链表的死循环,所以会造成CPU100%的情况,CoolShell有具体的分析。
HashMap当相同位置冲突到8的时候,会展开成红黑树,红黑树是一种平衡策略,因为随着链越长,检索效率就越低,所以展开成红黑树来提高搜索时间,红黑树会自动平衡,防止极端不平衡从而影响查找效率的情况发生。
红黑树具有以下四个特性:
每个节点要么是红色,要么是黑色,但是根节点永远是黑色的
红色节点不能连续、
从任一节点到其子树中每个叶子节点的路径都包含数量相同的黑色节点
所有的叶节点都是黑色的
HashMap关于并发的特点:
非线程安全
迭代时不允许修改内容
只读的并发是安全的
如果一定要把HashMap用在并发环境,要用Collections.synchronizedMap(new HashMap())
ConcurrentHashMap
1.7版本的时候ConcurrentHashMap结构是由初始化的Segment数量,然后每个Segment里面都有一个HashMap,那么多线程的时候对应在自己的Segment操作,就不会发生并发安全问题。
ConcurrentHashMap默认有16个Segments,所以最多可以同时支持16个线程并发写,初始化之后是不可以扩容的。
1.8版本之后和HashMap的结构非常相似:
源码分析:
1.put()
判断key value不为空
计算hash值
根据对应位置节点的类型来赋值,或者helpTransfer,或者增长链表,或者给红黑树增加节点
检查满足阈值就“红黑树化”
返回oldVal
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 首先判断key和value是否为null
if (key == null || value == null) throw new NullPointerException();
// 得到key对应的hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果table还没初始化,那就进行初始化操作
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 如果初始化并且还没有冲突,那么就直接放入值
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 利用CAS操作,里面用到了Unsafe类
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// MOVED代表正在扩容
else if ((fh = f.hash) == MOVED)
// 进行扩容操作
tab = helpTransfer(tab, f);
// 如果正好这个hash的地方有值的话
else {
V oldVal = null;
// 用synchronized锁住保证并发安全
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 根据hashkey得到对应的旧值
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 如果下一个没有值,就直接链到链尾处
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 这里是红黑树的相关操作
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// 放到红黑树里
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
// 并返回旧的值
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 完成添加
if (binCount != 0) {
// 判断是否要转成红黑树,TREEIFY_THRESHOLD为8
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
// 返回旧的值
return oldVal;
break;
}
}
}
// 没有旧值就返回null
addCount(1L, binCount);
return null;
}
2.get()
计算hash值
找到对应的位置,根据情况进行:
1.直接取值
2.红黑树里找值
3.遍历链表取值
返回找到的结果
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
// 判断是否初始化了,如果没初始化就直接返回null了
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 槽点的hash值符合,就直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果是负数,那么可能是红黑树,来查找红黑树对应的值
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 如果是链表,就查找链表返回对应的值
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
1.7 -> 1.8的变化
1.数据结构发生了变化,原来是由Segment给予并发能力,现在是根据每个Node,原来的有线程并发限制,但是1.8之后并发能力更好。
2.Hash碰撞上,1.7是拉链法,1.8是先是拉链法,然后达到一定长度转换成红黑树,提高了查询性能。
3.保证并发安全不同,1.7是通过不同的Segment,使用分段锁来保证,而1.8之后是通过CAS和synchronized来保证并发安全
4.查询复杂度
转为红黑树之后,查询时间复杂度为O(LogN)
为什么在8的时候转为红黑树?
因为链表的查询速度在在数量小的时候返回也是很快的,但是超过一定数量的时候,为了保证ConcurrentHashMap的查询效率,就转换成红黑树提高效率,但是红黑树占用的空间是链表的两倍,所以少的情况尽量不使用,由于泊松分布,当冲突到8的时候的概率为0.00000006,几乎很难发生,除非Hash算法有误,那么即便在极端情况下,为了保证效率,所以就会使用红黑树。
组合操作:
replace代码演示:
/**
* 组合操作并不保证线程安全,用其他操作来代替,比如replace
*/
public class OptionsNotSafe implements Runnable{
private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>();
public static void main(String[] args) throws InterruptedException {
scores.put("小明", 0);
Thread thread1 = new Thread(new OptionsNotSafe());
Thread thread2 = new Thread(new OptionsNotSafe());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(scores.get("小明"));
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
while (true) {
// 我们只能保证单个操作安全,并不能保证get之后再put是安全的
Integer score = scores.get("小明");
Integer newScore = score + 1;
//scores.put("小明", newScore);
// 这个方法用来保证线程安全的同时进行尝试替换旧的值,如果没有替换成功,那么就会返回false,否则返回true
boolean result = scores.replace("小明", score, newScore);
if (result) {
break;
}
}
}
}
}
除了replace之外还有putIfAbsent之类的方法,相当于:
CopyOnWriteArrayList
代替Vector和SynchronizedList,因为他们的锁的粒度太大,并发效率相对比较低,并且迭代时无法编辑
Copy-On-Write,还有CopyOnWriteArraySet来代替同步Set
CopyOnWriteArrayList适用场景为,读操作可以尽可能地快,而写即使慢一些也可以地情况,且读多写少,比如黑名单,每日更新,监听器等等
CopyOnWriteArrayList地读写规则:
根据读写锁规则地升级:读取完全被用加锁,并且写入也不会阻塞读取操作,只有写入和写入之间需要同步等待。
代码演示:
删除了5,但是最后还会输出出来,也就是输出和删除不是一起的
/**
* 演示CopyOnWriteArrayList可以再迭代地过程中修改数组内容,ArrayList不行
*/
public class CopyOnwriteDemo1 {
public static void main(String[] args) {
// ArrayList<String> list = new ArrayList<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println("list is " + list);
String next = iterator.next();
System.out.println(next);
if ("2".equals(next)) {
list.remove("5");
}
if ("3".equals(next)) {
list.add("3 found");
}
}
}
}
CopyOnWrite的含义:
每次写的时候创建新的副本,并把指针指向新的副本地址
那么对于旧的容器来说就是不可变的,因为没人对这个进行操作,迭代的时候也不会报错,只是时效性不是立即的。
ArrayList源码:
它会比较现在的数量和之前的数量,如果不一致,那么就会抛异常,所以迭代的时候会抛异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
CopyOnWriteArrayList的时效性是iterator()创建迭代器的时间,代码如下:
/**
* 对比两个迭代器
*/
public class CopyOnwriteDemo2 {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(new Integer[]{1, 2, 3});
System.out.println(list);
// 创建迭代器的时候,iterator1就不会变化了
Iterator<Integer> iterator1 = list.iterator();
list.add(4);
System.out.println(list);
Iterator<Integer> iterator2 = list.iterator();
iterator1.forEachRemaining(System.out::println);
iterator2.forEachRemaining(System.out::println);
}
}
CopyOnWrite的缺点:
数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,但是不能保证数据的实时一致性,所以如果你希望写入的数据能够马上读到,那么请不要使用CopyOnWrite容器
内存占用问题:因为CopyOnWrite的写是复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,那么内存占用上就要比普通的方法要多一倍
源码分析:
上锁方式是通过ReentrantLock进行上锁的。
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
add方法:
public boolean add(E e) {
// 首先进行上锁,保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
// 获取原来数组的长度
int len = elements.length;
// 进行复制操作
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 数组的最后一个位置设置成新的值
newElements[len] = e;
// 设置新的数组为我们的数组
setArray(newElements);
return true;
} finally {
// 最后解锁
lock.unlock();
}
}
get()方法:
直接返回,没有加锁的,所以不会出现阻塞
private E get(Object[] a, int index) {
return (E) a[index];
}
并发队列
用队列可以在线程间传递数据,考虑锁等线程安全问题的重任给到了队列上。
java提供了线程安全的队列,这就是并发队列,并发队列还分为阻塞队列(BlockingQueue)和非阻塞队列(ConcurrentLinkedQueue)等。
BlockingQueue
阻塞队列是具有阻塞功能的队列,所以它首先是一个队列,其次是具有阻塞功能,通常,阻塞队列的一端是给生产者放数据用,另一端是给消费者拿数据用,阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的。
take()方法:获取并溢出队列的头节点,队列无数据,则阻塞,直到队列里有数据。
put()方法:插入元素,但是如果队列已满,则无法继续插入,阻塞住,直到队列里有了空闲空间。
是否有界:无界队列里面可以容纳非常多:Integer.MAX_VALUE,可以近似无限容量,需要有界队列也可以手动创建有界的队列
阻塞队列也是线程池的重要组成部分之一
BlockingQueue主要方法:
1.put,take
2.add(满了会抛出异常),remove(空了也会抛出异常),element(返回头节点,如果空也会抛出异常)
3.offer,poll,peek
放入offer,如果满了就会返回false,poll是拿出数据并且删除,如果没有则返回null,peek则是拿数据,并不会删除,也一样没有返回null
1.ArrayBlockingQueue
有界,指定容量,公平,还可以指定是否需要保证公平,如果想保证公平,那么等待最长时间的线程会被优先处理,不过会带来一定的性能损耗
代码演示:
/**
* ArrayBlockingQueue代码演示
*/
public class ArrayBlockingQueueDemo {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
Interviewer interviewer1 = new Interviewer(queue);
Consumer consumer1 = new Consumer(queue);
new Thread(interviewer1).start();
new Thread(consumer1).start();
}
}
class Interviewer implements Runnable{
BlockingQueue<String> queue;
public Interviewer(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
System.out.println("候选人都来了");
for (int i = 0; i < 10; i++) {
String cadidate = "Cadidate" + i;
try {
queue.put(cadidate);
System.out.println("安排好了" + cadidate);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
queue.put("stop");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable{
BlockingQueue<String> queue;
public Consumer(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String msq;
try {
while (!(msq = queue.take()).equals("stop")) {
System.out.println("到了" + msq);
}
System.out.println("所有候选人都结束了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
源码分析:
public void put(E e) throws InterruptedException {
// 首先检查值是否为null
checkNotNull(e);
// 取得ReentrantLock锁
final ReentrantLock lock = this.lock;
// 设置锁的时候是可以被中断的
lock.lockInterruptibly();
try {
// 循环判断是否等于最大长度了
while (count == items.length)
// 进行等待
notFull.await();
// 没有等于最大长度则往队列里放值
enqueue(e);
} finally {
// 最后解锁
lock.unlock();
}
}
2.LinkedBlockingQueue
无界,容量为Integer最大值,内部结构为Node,两把锁
源码:
这里面take和put分别用了不同的锁,那么它们是互不干扰的
public void put(E e) throws InterruptedException {
// 检查是否为null
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
// 获取put锁
final ReentrantLock putLock = this.putLock;
// count是具有原子性的
final AtomicInteger count = this.count;
// 进行上锁
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
// 如果已经到达了最大值
while (count.get() == capacity) {
// 进行等待
notFull.await();
}
// 要么加入节点
enqueue(node);
// 返回旧的值
c = count.getAndIncrement();
// 如果 + 1之后还是没满,那么就释放之前的锁
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
3.PriorityBlockingQueue
支持优先级,自然顺序(而不是先进先出),可以根据CompareTo来实现排序规则。
无界队列,相当于PriorityQueue的线程安全版本
4.SynchronizedQueue
它的容量为0,因为SynchronizedQueue不需要去持有元素,它所做的就是直接传递,效率很高
SynchronizedQueue没有peek等函数,因为容量是0,同理没有iterate相关方法,是一个极好的用来直接传递的并发数据结构,也是线程池CacheThreadPool的阻塞队列。
5.DelayQueue
延迟队列,根据延迟时间排序,元素需要实现Delayed接口,规定排序规则
非阻塞并发队列
并发包中的非阻塞队列只有ConcurrentLinkedQueue,是使用链表作为其数据结构的,使用CAS非阻塞算法来实现线程安全,适合用在对性能要求较高的并发场景,用的相对较少一些
源码的offer方法的CAS思想,内有p.casNext方法,用了UNSAFE。compareAndSwapObject。
选择适合自己的队列
1.边界:我们的数据大小是否需要有边界的,没有边界的是否更好一些
2.空间:ArrayBlockingQueue的空间更加整齐
3.吞吐量:LinkedBlockingQueue方法丰富之外,锁的粒度更小,吞吐量比较高
如果只需要交换,那么就可以用SunchronizedQueue
如果从功能上考虑,需要延迟就用DelayQueue,需要排序就用PriorityBlockingQueue等等
按照业务场景来选择适合本业务的队列
控制并发流程
控制并发流程的工具类作用就是,保住我们程序员更容易得让线程之间合作,来满足业务逻辑。
比如让线程A等待线程B执行完毕后再执行等合作策略等
常见得并发流程工具类:
CountDownLatch
倒数门闩,数量到0才会再执行,之前一直会处于等待状态。
主要方法:
CountDownLatch(int count) :仅有一个构造函数,参数count为需要倒数的数值。
await():调用await()方法的线程会被挂起,会等到count为0才会继续执行
countDown():将count值减1,直到为0时,等待的线程会被唤醒
两个典型用法:
1.一个线程等待多个线程都执行完毕,再继续自己的工作
代码演示:
/**
* 工厂中,质检,5个工人检查,所有人都认为通过,那么才会通过
*/
public class CountDownLatchDemo1 {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(5);
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
final int no = i + 1;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep((long) Math.random() * 10000);
System.out.println(no + "完成了检查");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
};
executorService.submit(runnable);
}
System.out.println("等待5个人检查完....");
try {
countDownLatch.await();
System.out.println("所有人完成了检查工作");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.多个线程等待某一线程的信号,同时开始执行
/**
* 模拟100米跑步,5名选手都准备好了,只要等裁判一声令下,所有人同时开始跑步
*/
public class CountDownLatchDemo2 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
final int no = i + 1;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println(no + "准备起跑");
countDownLatch.await();
System.out.println(no + "开始跑");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executorService.submit(runnable);
}
Thread.sleep(1000);
System.out.println("准备开枪");
System.out.println("bang!");
countDownLatch.countDown();
}
}
3.结合两种一起使用的例子:
/**
* 模拟100米跑步,5名选手都准备好了,只要等裁判一声令下,所有人同时开始跑步
* 当所有人到达终点后,比赛结束
*/
public class CountDownLatchDemo1And2 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(5);
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
final int no = i + 1;
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println(no + "准备起跑");
countDownLatch.await();
System.out.println(no + "开始跑");
Thread.sleep((long) Math.random() * 10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
end.countDown();
System.out.println(no + "到达了终点");
}
}
};
executorService.submit(runnable);
}
Thread.sleep(1000);
System.out.println("准备开枪");
System.out.println("bang!");
countDownLatch.countDown();
end.await();
System.out.println("比赛结束");
}
}
拓展用法:多个线程等多个线程完成执行后,再同时执行
但是CountDownLatch是不能够重用的,如果需要重新计数,那么可以考虑使用CyclicBarrier或者创建新的CountDownLatch实例。
Semaphore信号量
用来限制或管理数量有限的资源的使用情况。
信号量的作用是维护一个许可证的计数,线程可以获取许可证,那信号量剩余的许可证就减1,线程也可以释放一个许可证,那信号量剩余的许可证就加1,当信号量所拥有的许可证数量为0,那么下一个还想要获取许可证的线程,就需要等待,直到有另外的线程释放了许可证。
正常情况下获取许可证,并且许可证数量减1,当减到0之后,下一个线程来的时候,就会被阻塞住,等待许可证的释放,之后有线程调用release()释放许可证,那么这个阻塞的线程就可以拿到许可证去执行。
流程为:
1.初始化Semaphore并指定许可证的数量
2.在需要被现在的代码前面加acquire()或者acquireUninterruptibly()方法
3.在任务执行结束后,调用release()来释放许可证
主要方法为:
new Semaphore(int permits, boolean fair):这里可以设置是否要使用公平策略,如果传入true,那么Semaphore会把之前等待的线程放到FIFO的队列里,以便于有了新的许可证,可以分发给之前等了最长时间的线程
acquire():可以响应中断
acquireUninterruptibly():不响应中断异常
tryAcquire():看看现在有没有空闲的许可证,有就获取,没有就去做其他的事情
tryAcquire(timeout):在timeout时间内能不能获取到空闲的许可证,能就获取,没有就去做其他的事情
release():归还许可证
代码演示:
/**
* 演示Semaphore用法
*/
public class SemaphoreDemo {
static Semaphore semaphore = new Semaphore(3, true);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(50);
for (int i = 0; i < 100; i++) {
executorService.submit(new Task());
}
executorService.shutdown();
}
static class Task implements Runnable {
@Override
public void run() {
try {
// semaphore.acquire();
// 可以拿多个许可证
semaphore.acquire(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到了许可证");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "释放了许可证");
// semaphore.release();
// 释放也要释放多个许可证
semaphore.release(3);
}
}
}
这里需要注意的是:
1.获取和释放要一致,否则释放的数量不够用,最后会导致程序卡死。
2.一半设置true会更合理,防止插队,出现饥饿,因为使用信号量一般都是比较慢的程序。
3.并不是必须由获取许可证的线程释放那个许可证,事实上,获取和释放许可证对线程并无要求,也许是A获取了,然后由B来释放,只要逻辑合理即可。
4.信号量的作用,除了控制临界区最多同时有N个线程访问外,另一个作用是可以实现“条件等待”,例如线程1需要再线程2完成准备工作后才能开始,那么线程1 acquire(),而线程2完成任务后 release(),这样的话相当于轻量级的CountDownLatch。
Condition接口(又称条件对象)
signal和signalAll的区别:
signalAll()会唤醒所有正在等待的线程
signal()是公平的,会唤醒等待时间最长的线程
代码演示:
/**
* 演示Condition的基本用法
*/
public class ConditionDemo1 {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
void method1() {
lock.lock();
try {
System.out.println("条件不满足,开始await");
condition.await();
System.out.println("条件满足了,开始执行后续的任务");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
void method2() {
lock.lock();
try {
System.out.println("准备工作完成,开始唤醒其他的线程");
condition.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ConditionDemo1 conditionDemo1 = new ConditionDemo1();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
conditionDemo1.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
conditionDemo1.method1();
}
}
用Condition实现生产者消费者模式:
/**
* 用Condition实现生产者消费者模式
*/
public class ConditionDemo2 {
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<>(queueSize);
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public static void main(String[] args) {
ConditionDemo2 conditionDemo2 = new ConditionDemo2();
Producer producer = conditionDemo2.new Producer();
Consumer consumer = conditionDemo2.new Consumer();
producer.start();
consumer.start();
}
class Consumer extends Thread {
@Override
public void run() {
try {
consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void consume() throws InterruptedException {
while (true) {
lock.lock();
try {
while (queue.size() == 0) {
System.out.println("队列空,等待数据");
notEmpty.await();
}
queue.poll();
notFull.signal();
System.out.println("从队列里取走了一个数据,队列剩余:" + queue.size());
} finally {
lock.unlock();
}
}
}
}
class Producer extends Thread {
@Override
public void run() {
try {
produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void produce() throws InterruptedException {
while (true) {
lock.lock();
try {
while (queue.size() == queueSize) {
System.out.println("队列满了,等待消费");
notFull.await();
}
queue.offer(1);
notEmpty.signal();
System.out.println("向队列里插入一个数据,队列剩余空间:" + (queueSize - queue.size()));
} finally {
lock.unlock();
}
}
}
}
}
如果说Lock用来代替synchronized,那么Condition就是用来代替对应的Object.wait/notify的,所以在用法和性质上,几乎一样,Codition是建立在Lock的基础上的。
CyclicBarrier
CyclicBarrier和CountDownLatch很类似,都能够阻塞一组线程。
当有大量线程相互配合,分别计算不同任务,并且需要最后统一汇总的时候,我们可以使用CyclicBarrier,CyclicBarrier可以构造一个集结点,当某一线程执行完毕,它就会到集结点等待,直到所有线程都到了集结点,那么该栅栏就会被撤销,所有线程统一出发,继续执行剩下的任务。
CyclicBarrier是对象是一个一个的线程,而CountDownLatch的对象是一个一个的事件。
CyclicBarrier可以重用,但是CountDownLatch不能重复使用
CyclicBarrier都到了之后会执行一个Runnable。
代码演示:
/**
* 演示CyclicBarrier
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("所有人都到场了,大家统一出发!");
}
});
for (int i = 0; i < 10; i++) {
new Thread(new Task(i + 1, cyclicBarrier)).start();
}
}
static class Task implements Runnable {
private int id;
private CyclicBarrier cyclicBarrier;
public Task(int id, CyclicBarrier cyclicBarrier) {
this.id = id;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("线程" + id + "前往集合地点");
try {
Thread.sleep((long) Math.random() * 10000);
System.out.println("线程" + id + "到了集合地点,等待其他人到达");
cyclicBarrier.await();
System.out.println("线程" + id + "出发了");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
AQS
锁和协作类有共同点:闸门
它们都有这样那样类似的协作,或者同步功能,因为它们底层都用了一个共同的基类,这就是AQS
那么上面的协作类,如果能提取出一个工具类,那么它们就只关注自己的业务逻辑就可以了。
协作类和AQS的关系
比如Semaphore内部有一个Sync类,Sync类继承了AQS。
源码为:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
Sync(int permits) {
setState(permits);
}
final int getPermits() {
return getState();
}
可以看到AQS的全称是AbstractQueuedSynchronizer
AQS的作用:
AQS是一个用于构件锁,同步器,协作工具类的工具类,有了AQS之后,更多的协作工具类都可以很方便得被写出来。
同步状态的原子性管理
线程的阻塞与解除阻塞
队列的管理
在并发场景下,这些正确且高效实现内容,如果自己写是非常有难度的,所以我们用AQS来帮我们搞定,我们只关注自己的逻辑就行。
AbstractQueuedSynchronizer是Doug Lea写的,从JDK1.5之后加入的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,我们用IDE看AQS的实现类,可以发现实现类有以下这些:
AQS三大部分
state,控制线程强锁和配合的FIFO队列,期望协作工具类去实现的获取/释放等重要方法
1.state状态
AQS中,有一个成员变量,是state,这里的state具体含义根据实现类的不同而不同,比如在Semaphore,表示剩余的许可证的数量,而CountDownLoatch里表示还需要倒数的数量
state是volatile修饰的,会被并发地修改,所以所有修改state方法都要保证线程安全,比如getState,setState,compareAndSetState操作来读取和更新这个状态,这些方法都依赖于j.u.c.atomic包的支持
2.控制线程强锁和配合的FIFO队列
这个队列用来存放等待的线程,AQS就是排队管理器,当多个线程争用同一把锁时,必须有排队机制将这些没能拿到锁的线程串在一起,当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁。
AQS会维护一个等待的线程队列,把线程都放到这个队列里,是一个双向形式的队列
3.期望协作工具类去实现的获取/释放等重要方法
这里的获取和释放方法,是利用AQS的协作工具类里最重要的方法,是由协作类自己去实现的,并且含义各不相同。
获取方法:
获取操作会依赖state变量,经常会阻塞(比如获取不到锁的时候)
在Semaphore中,获取就是acquire方法,作用就是获取一个许可证
比如在ContDownLatch,获取就是await方法,作用是等待,直到倒数结束
释放方法:
释放操作不会阻塞
在Semaphore中,获取就是release方法,作用就是释放一个许可证
CountDownLatch里面,释放就是countDown方法,作用是倒数1个数
AQS应用
应用步骤:
第一步:写一个类,想好协作的逻辑,实现获取/释放方法
第二步:内部写一个Sync类继承AbstractQueuedSynchronizer
第三步:根据是否独占来重写tryAcquire/tryRelease或tryAcquireShared(int acquires) 和tryReleaseShared(int release)等方法,在之前写的获取/释放方法中调用AQS的acquire/release或者Shard方法。
AQS在CountDownLatch的总结:
1.调用CountDownLatch的await方法时,便会尝试获取“共享锁”,不过一开始是获取不到该锁的,于是线程被阻塞
2.而共享锁可获取到的条件就是锁计数器的值为0
3.而锁计数器的初始值为count,每当一个线程调用该CountDownLatch对象的countDown方法时,才将锁计数器 -1
4.count个线程调用countDown之后,锁计数器才为0,而前面提到的等待获取共享锁的线程才能继续运行。
AQS在Semaphore的总结:
1.在Semaphore中,state表示许可证的剩余数量
2.看tryAcquire方法,判断nonfaireTryAcquireShared大于等于 0 的话,代表成功
3.这里会先检查剩余许可证数量够不够这次需要的,用减法来计算,如果直接不够,那就返回负数,代表失败,如果够了,就用自旋加compareAndSetState来改变state状态,直到改变成功就返回正数,或者是期间如果被其他人修改了导致剩余数量不够了,那也返回负数代表获取失败
AQS在ReentrantLock的总结:
分析释放锁的方法tryRelease:
由于是可重入锁,所以state代表重入的次数,每次释放锁先判断是不是当前持有锁的线程释放的,如果不是就抛异常,如果是重入锁次数就减1,如果减到0,说明完全释放了,于是free就是true,并把state设置为0
加锁的方法:
会将线程放入到队列中等待,直到state为0的时候,进行唤醒
DIY一个简单的门闩:
/**
* 自己用AQS实现简单的门栓
*/
public class OnshotLatch {
public static void main(String[] args) throws InterruptedException {
OnshotLatch onshotLatch = new OnshotLatch();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "尝试获取latch,获取失败那就等待");
onshotLatch.await();
System.out.println(Thread.currentThread().getName() + "继续运行");
}
}).start();
}
Thread.sleep(2000);
onshotLatch.signal();
}
private final Sync sync = new Sync();
public void await() {
sync.acquireShared(0);
}
public void signal() {
sync.releaseShared(0);
}
private class Sync extends AbstractQueuedSynchronizer {
@Override
protected int tryAcquireShared(int arg) {
return getState() == 1 ? 1 : -1;
}
@Override
protected boolean tryReleaseShared(int arg) {
setState(1);
return true;
}
}
}
Future和Callable
Runnable的缺陷
1.不能返回一个返回值
2.不能抛出checked Exception
因为接口的定义就没有定义这些
那么针对无法抛出异常的补救措施就是使用Callable接口。
Callable
Callable实现的是使用call方法,然后是有返回值的。
源码:
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Future类
Future的核心思想就是,对于计算耗时的方法,利用子线程来运行,然后想要获取结果的时候,返回子线程的结果,那我我这个线程就不用一直等待方法的结束,可以继续往下执行其他的事情。
Callable和Future的关系:
我们可以用Future.get来获取Callable接口返回的执行结果,还可以通过Future.isDone来判断任务是否已经执行完,以及取消这个任务,限时获取任务的结果等
在call()未执行完毕之前,调用get的线程会被阻塞,直到call方法返回了结果,这时候get才会得到该结果,并且切换到runnable状态
Future是一个存储器,它存储了call这个任务的结果,而这个任务的执行时间是无法提前确定的,因为这完全取决于call方法执行的情况
Future主要方法
1.get()方法:获取结果
get方法去觉得Callable任务的状态,有以下五种情况:
任务正常完成:get方法立刻放回结果
任务尚未完成(任务还没开始或进行中):get将阻塞直到任务完成
任务执行过程中抛出异常,get方法会抛出ExecutionException,这里的异常是call执行时产生的异常,但是无论call执行时抛出的异常类型是什么,最后get方法抛出的异常类型都是ExecutionException。
任务被取消:get方法会抛出CancellationException
任务超时:get方法有一个重载方法,传入一个延迟时间,如果超时就会抛出TimeoutException
如果需要用到超时的需求,在规定时间内没有返回后,既然不获取了,那么就需要把这个任务进行取消
2.cancel方法:取消任务的执行
3.isDone():判断线程是否执行完毕
如果有中断产生,也会执行true,代表线程执行完之后不再会执行,即使抛出异常也算完毕。
4.isCancelled():判断是否被取消
Future的应用演示
1.线程池的submit方法返回Future对象
首先,我们要给线程池提交我们的任务,提交时线程池会立刻返回给我们一个空的Tuture容器,当线程的任务一旦执行完毕,也就是当我们可以获取结果的时候,线程池便会把该结果填入到之前给我们的那个Future里去,而不是创建一个新的Future,我们就可以从该Future中获取任务执行的结果。
代码演示:
/**
* 演示一个Future的使用方法
*/
public class OneFuture {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
Future<Integer> future = service.submit(new CallableTask());
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
service.shutdown();
}
static class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return new Random().nextInt();
}
}
}
2.多个任务,用Future数组来获取
/**
* 演示批量提交任务时,用List来批量接收结果
*/
public class MutiFuture {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService service = Executors.newFixedThreadPool(2);
ArrayList<Future> futures = new ArrayList<>();
for (int i = 0; i < 20; i++) {
Future<Integer> submit = service.submit(new CallableTask());
futures.add(submit);
}
for (int i = 0; i < futures.size(); i++) {
System.out.println(futures.get(i).get());
}
}
static class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return new Random().nextInt();
}
}
}
3.执行任务抛出异常和isDone的演示
/**
* 演示get方法过程中抛出异常,for循环为了演示抛出Exception的时机:
* 并不是说一产生异常就抛出,直到调用get的时候才会抛出
*/
public class GetException {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(20);
Future<Integer> submit = service.submit(new CallableTask());
try {
for (int i = 0; i < 5; i++) {
System.out.println(i);
Thread.sleep(500);
}
// 就算抛出异常,还是认为完成,打印true
System.out.println(submit.isDone());
// 直到我调用的时候,才会抛出
submit.get();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("抛出InterruptedException异常");
} catch (ExecutionException e) {
e.printStackTrace();
System.out.println("抛出ExecutionException异常");
}
}
static class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 虽然我抛出的是InterruptedException,但是接收的还是ExecutionException
throw new InterruptedException("抛出异常了");
}
}
}
4.获取任务超时
代码演示:
/**
* 演示get的超时方法,需要注意超时之后,要进行处理,调用future.cancel()
* 演示cancel传入true和false的区别,代表是否中断正在执行的任务
*/
public class TimeoutGet {
private static final Ad DEFAULT_AD = new Ad("无网络默认广告");
private static final ExecutorService service = Executors.newFixedThreadPool(10);
static class Ad {
String name;
public Ad(String name) {
this.name = name;
}
@Override
public String toString() {
return "Ad{" +
"name='" + name + '\'' +
'}';
}
}
static class FetchAdTask implements Callable<Ad> {
@Override
public Ad call() throws Exception {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("sleep期间被中断了");
// 中断之后,就算返回也不会使用了
return new Ad("中断时候的广告");
}
return new Ad("旅游订票哪家强,找某程");
}
}
public void printAd() {
Future<Ad> f = service.submit(new FetchAdTask());
Ad ad;
try {
ad = f.get(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
ad = new Ad("中断时候的广告");
} catch (ExecutionException e) {
ad = new Ad("异常时候的广告");
} catch (TimeoutException e) {
ad = new Ad("超时时候的广告");
System.out.println("超时,未获取到广告");
// true代表如果这个任务还在执行,那么我就中断它,相当于interrupt
// false代表这个call任务就会继续执行
boolean cancel = f.cancel(true);
System.out.println("cancel结果:" + cancel);
}
service.shutdown();
System.out.println(ad);
}
public static void main(String[] args) {
TimeoutGet timeoutGet =new TimeoutGet();
timeoutGet.printAd();
}
}
5.取消任务的执行
如果这个任务还没有执行,那么这种情况任务会被正常的取消,未来也不会被执行,方法返回true
如果任务已完成,或者已取消,那么cancel方法会执行失败,返回false
如果这个任务已经开始执行了,那么这个取消方法将不会直接取消该任务,而是根据我们填的参数mayInterruptIfRunning做判断。
那么为什么我们想取消任务的时候还可以传入fasle,因为有的时候业务需要已经开始的任务就让它执行,而不是中断逻辑,但是对于还没有执行的任务,不让他执行,所以才有传入false这个方式。
cancel(true)适用于:任务能够处理interrupt异常
cancel(false)适用于:未能处理interrupt的任务,不清楚任务是否支持取消,需要等待已经开始的任务执行完成等情况
6.用FutureTask来创建Future
用FutureTask来获取Future和任务的结果
FutureTask是一种包装器,可以把Callable转化成Future和Runnable,它同时实现二者的接口
用法:把Callable实例作为参数,生成FutureTask的对象,然后把这个对象当作一个Runnable对象,最后通过FutureTask获取刚才执行的结果
代码演示:
/**
* 演示FutureTask的用法
*/
public class FutureTaskDemo {
public static void main(String[] args) {
Task task = new Task();
FutureTask<Integer> futureTask = new FutureTask<>(task);
// new Thread(futureTask).start();
ExecutorService service = Executors.newFixedThreadPool(10);
service.submit(futureTask);
try {
Integer integer = futureTask.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
static class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("子线程正在计算");
Thread.sleep(1000);
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
}
Future的注意点:
1.当for循环批量获取future的结果时,容易发生一部分线程很慢的情况,get方法调用时应使用timeout限制
2.Future的生命周期不能后退:生命周期只能前进,不能后退,就和线程池的生命周期一样,一旦完全完成了任务,就永久停在了"已完成"的状态,不能重头再来
实战缓存项目
从最简单的HashMap入手,一步步提高我们的缓存性能
/**
* 最简单的缓存形式:HashMap
*/
public class CacheHashMap {
private final HashMap<String, Integer> cache = new HashMap<>();
public Integer compute(String userId) throws InterruptedException {
// 先从cache中查找是否有之前的计算结果
Integer result = cache.get(userId);
if (null == result) {
// 如果缓存中找不到,那么就需要现在计算以下结果,并且保存到HashMap中
result = doCompute(userId);
cache.put(userId, result);
}
return result;
}
private Integer doCompute(String userId) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
return new Integer(userId);
}
public static void main(String[] args) throws InterruptedException {
CacheHashMap cacheHashMap = new CacheHashMap();
System.out.println("开始计算了");
Integer result = cacheHashMap.compute("13");
System.out.println("第一次计算结果:" + result);
result = cacheHashMap.compute("13");
System.out.println("第二次计算结果:" + result);
}
}
用装配者模式来进行解耦
我们假设用ExpensiveFunction类时耗时计算的实现类,实现了Computable接口,但是本身不具备缓存功能,我们的缓存功能是由MoodCache来提供的,那么以后需要其他计算的时候,只需要调用MoodCache类,并往里传入具体的计算实现,就可以完成缓存功能。
代码演示:
/**
* 用装饰者模式,给计算器自动添加缓存功能
*/
public class MoodCache<A, V> implements Computbale<A, V> {
private final Map<A, V> cache = new HashMap();
private final Computbale<A, V> computbale;
public MoodCache(Computbale<A, V> computbale) {
this.computbale = computbale;
}
@Override
public synchronized V compute(A arg) throws Exception {
System.out.println("进入缓存机制");
V result = cache.get(arg);
if (result == null) {
result = computbale.compute(arg);
cache.put(arg, result);
}
return result;
}
public static void main(String[] args) throws Exception {
MoodCache<String, Integer> expensiveCompute = new MoodCache<>(new ExpensiveFunction());
System.out.println("开始计算了");
Integer result = expensiveCompute.compute("666");
System.out.println("第一次计算结果:" + result);
result = expensiveCompute.compute("666");
System.out.println("第二次计算结果:" + result);
}
}
并发安全保证
在并发情况下,可能会造成冲突丢失某个值,那么就需要相对应的线程安全的操作,但是用synchronized性能太差,每次只能有一个对象读取,而且代码复用能力差,因为我们写的方法可能会在其他业务上也要复用,那么效率就会很低,每个线程进来都要慢慢计算等待,因为计算的key往往是不相同的。
给HashMap加final关键字,那么变量只能被赋值一次,提高安全性。
那为了保障线程安全,我们可以将HashMap替换成ConcurrentHashMap来保障线程安全。
重复计算问题
由于多个线程在要求计算相同值得请求到来时,还是会导致计算多遍,这和缓存想避免多次计算得初衷是恰恰相反的。
代码演示:
/**
* 利用Future避免重复计算
*/
public class ConcurrentCache<A, V> implements Computbale<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computbale<A, V> computbale;
public ConcurrentCache(Computbale<A, V> computbale) {
this.computbale = computbale;
}
@Override
public V compute(A arg) throws Exception {
Future<V> future = cache.get(arg);
if (future == null) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return computbale.compute(arg);
}
};
FutureTask<V> futureTask = new FutureTask<>(callable);
future = futureTask;
cache.put(arg, future);
System.out.println("从FutureTask调用了计算逻辑");
futureTask.run();
}
return future.get();
}
public static void main(String[] args) throws Exception {
ConcurrentCache<String, Integer> expensiveCompute = new ConcurrentCache<>(new ExpensiveFunction());
ExecutorService service = Executors.newFixedThreadPool(10);
System.out.println("开始计算了");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.compute(String.valueOf(111));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(8000);
System.out.println("开始第二次的缓存取值");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.compute(String.valueOf(111));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
依然存在重复的可能
如果有多个线程同时计算同时带哦用cache.get方法,还是会返回null,那么还是会创建多个任务去计算相同的值。
代码演示:
/**
* 利用putIfAbsent避免重复放入到ConcurrentHashMap中
*/
public class ConcurrentCache<A, V> implements Computbale<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computbale<A, V> computbale;
public ConcurrentCache(Computbale<A, V> computbale) {
this.computbale = computbale;
}
@Override
public V compute(A arg) throws Exception {
Future<V> future = cache.get(arg);
if (future == null) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return computbale.compute(arg);
}
};
FutureTask<V> futureTask = new FutureTask<>(callable);
// 往里放, 如果这个key不存在的话,相反存在就会将存在的值进行返回
future = cache.putIfAbsent(arg, futureTask);
if (future == null) {
future = futureTask;
System.out.println("从FutureTask调用了计算逻辑");
futureTask.run();
}
}
return future.get();
}
public static void main(String[] args) throws Exception {
ConcurrentCache<String, Integer> expensiveCompute = new ConcurrentCache<>(new ExpensiveFunction());
ExecutorService service = Executors.newFixedThreadPool(10);
System.out.println("开始计算了");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.compute(String.valueOf(111));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(8000);
System.out.println("开始第二次的缓存取值");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.compute(String.valueOf(111));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
计算抛出异常——ExcecutionException
计算过程并不是一帆风顺,假设有一个计算类,有一定概率计算失败,应该如何处理。
如果是CancellationException和InterruptedException是人为取消的,那么我们应该立即终止任务,但是如果是计算错误,且我们明确直到多试几次就可以得到答案,那么我们的逻辑应该是重试,重试多次直到正确的结果出现为止。
那么在这里,我们加上while来保证计算出错不会影响我们的逻辑,然后如果是计算错误,就进入下一个循环,重新计算,直到计算成功,如果是认为取消,那么就抛出异常结束运行。
计算类代码:
/**
* 耗时计算的实现类, 有一定概率计算失败
*/
public class MayFail implements Computbale<String, Integer> {
@Override
public Integer compute(String arg) throws Exception {
double random = Math.random();
if (random > 0.5) {
throw new IOException("读取文件出错");
}
Thread.sleep(3000);
return Integer.valueOf(arg);
}
}
实现类代码:
/**
* 利用重试将可能发生计算错误的类在循环中重新计算
*/
public class ConcurrentCache<A, V> implements Computbale<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computbale<A, V> computbale;
public ConcurrentCache(Computbale<A, V> computbale) {
this.computbale = computbale;
}
@Override
public V compute(A arg) throws Exception{
while (true) {
Future<V> future = cache.get(arg);
if (future == null) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return computbale.compute(arg);
}
};
FutureTask<V> futureTask = new FutureTask<>(callable);
// 往里放, 如果这个key不存在的话,相反存在就会将存在的值进行返回
future = cache.putIfAbsent(arg, futureTask);
if (future == null) {
future = futureTask;
System.out.println("从FutureTask调用了计算逻辑");
futureTask.run();
}
}
try {
return future.get();
} catch (InterruptedException e) {
System.out.println("被中断了");
e.printStackTrace();
throw e;
} catch (ExecutionException e) {
System.out.println("计算错误,重新计算");
} catch (CancellationException e) {
System.out.println("被取消了");
e.printStackTrace();
throw e;
}
}
}
public static void main(String[] args) throws Exception {
ConcurrentCache<String, Integer> expensiveCompute = new ConcurrentCache<>(new MayFail());
ExecutorService service = Executors.newFixedThreadPool(10);
System.out.println("开始计算了");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.compute(String.valueOf(finalI));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(8000);
System.out.println("开始第二次的缓存取值");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.compute(String.valueOf(111));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
但是会出现一直计算错误的情况,那么这个原因是因为缓存污染。由于往map放完之后,进行计算过程的时候出现异常,但是并没有清理掉map中对应的值,那么这个值还是错误的情况,也就是值不为null,然后走future.get();,结果里面还是原来的抛异常,那么又回到了catch里,就会出现无限循环,所以这时候需要清理对应的缓存值。
代码演示:
/**
* 利用重试将可能发生计算错误的类在循环中重新计算
*/
public class ConcurrentCache<A, V> implements Computbale<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computbale<A, V> computbale;
public ConcurrentCache(Computbale<A, V> computbale) {
this.computbale = computbale;
}
@Override
public V compute(A arg) throws Exception{
while (true) {
Future<V> future = cache.get(arg);
if (future == null) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return computbale.compute(arg);
}
};
FutureTask<V> futureTask = new FutureTask<>(callable);
// 往里放, 如果这个key不存在的话,相反存在就会将存在的值进行返回
future = cache.putIfAbsent(arg, futureTask);
if (future == null) {
future = futureTask;
System.out.println("从FutureTask调用了计算逻辑");
futureTask.run();
}
}
try {
return future.get();
} catch (InterruptedException e) {
cache.remove(arg);
System.out.println("被中断了");
e.printStackTrace();
throw e;
} catch (ExecutionException e) {
cache.remove(arg);
System.out.println("计算错误,重新计算");
} catch (CancellationException e) {
cache.remove(arg);
System.out.println("被取消了");
e.printStackTrace();
throw e;
}
}
}
public static void main(String[] args) throws Exception {
ConcurrentCache<String, Integer> expensiveCompute = new ConcurrentCache<>(new MayFail());
ExecutorService service = Executors.newFixedThreadPool(10);
System.out.println("开始计算了");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.compute(String.valueOf(finalI));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(8000);
System.out.println("开始第二次的缓存取值");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.compute(String.valueOf(finalI));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
缓存过期
为每个结果指定过期时间,并定期扫描过期元素。
那么就很适合使用newScheduledThreadPool,指定定时执行过期操作,但是高并发情况下,如果同时过期的话,就同时拿不到缓存,导致都请求到Mysql,那么就会造成缓存雪崩,缓存击穿等高并发下的缓存问题,所以我们也要将过期时间设置成随机。
/**
* 出于安全性考虑,缓存需要设置有效期,到期自动失效,
* 否则缓存占用空间过大,且可能会出现缓存不一致的问题
*/
public class ConcurrentCache<A, V> implements Computbale<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computbale<A, V> computbale;
public ConcurrentCache(Computbale<A, V> computbale) {
this.computbale = computbale;
}
@Override
public V compute(A arg) throws Exception{
while (true) {
Future<V> future = cache.get(arg);
if (future == null) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return computbale.compute(arg);
}
};
FutureTask<V> futureTask = new FutureTask<>(callable);
// 往里放, 如果这个key不存在的话,相反存在就会将存在的值进行返回
future = cache.putIfAbsent(arg, futureTask);
if (future == null) {
future = futureTask;
System.out.println("从FutureTask调用了计算逻辑");
futureTask.run();
}
}
try {
return future.get();
} catch (InterruptedException e) {
cache.remove(arg);
System.out.println("被中断了");
throw e;
} catch (ExecutionException e) {
cache.remove(arg);
System.out.println("计算错误,重新计算");
} catch (CancellationException e) {
cache.remove(arg);
System.out.println("被取消了");
throw e;
}
}
}
public final static ScheduledExecutorService excutor = Executors.newScheduledThreadPool(5);
public V compute(A arg, long expire) throws Exception {
if (expire > 0) {
excutor.schedule(new Runnable() {
@Override
public void run() {
expire(arg);
}
}, expire, TimeUnit.MILLISECONDS);
}
return compute(arg);
}
public V computeRandom(A arg) throws Exception {
long randomExpire = (long) (5000 + (Math.random() * 10000));
return compute(arg, randomExpire);
}
public synchronized void expire(A key) {
Future<V> future = cache.get(key);
if (future != null) {
System.out.println("清除掉,过期时间已到");
if (!future.isDone()) {
future.cancel(true);
}
cache.remove(key);
}
}
public static void main(String[] args) throws Exception {
ConcurrentCache<String, Integer> expensiveCompute = new ConcurrentCache<>(new MayFail());
ExecutorService service = Executors.newFixedThreadPool(500);
System.out.println("开始计算了");
long start = System.currentTimeMillis();
for (int i = 0; i < 500; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.computeRandom(String.valueOf(666));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(3000);
System.out.println("开始第二次的缓存取值");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.computeRandom(String.valueOf(666));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(5000);
System.out.println("开始第三次的缓存取值");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.computeRandom(String.valueOf(666));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
service.shutdown();
while (!service.isTerminated()) {
}
System.out.println("总耗时: " + (System.currentTimeMillis() - start));
}
}
用CountDownLatch进行压测
/**
* 使用CountDownLatch实现压测
*/
public class ConcurrentCache<A, V> implements Computbale<A, V> {
private final CountDownLatch countDownLatch = new CountDownLatch(1);
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computbale<A, V> computbale;
public ConcurrentCache(Computbale<A, V> computbale) {
this.computbale = computbale;
}
@Override
public V compute(A arg) throws Exception{
while (true) {
Future<V> future = cache.get(arg);
if (future == null) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return computbale.compute(arg);
}
};
FutureTask<V> futureTask = new FutureTask<>(callable);
// 往里放, 如果这个key不存在的话,相反存在就会将存在的值进行返回
future = cache.putIfAbsent(arg, futureTask);
if (future == null) {
future = futureTask;
System.out.println("从FutureTask调用了计算逻辑");
futureTask.run();
}
}
try {
return future.get();
} catch (InterruptedException e) {
cache.remove(arg);
System.out.println("被中断了");
throw e;
} catch (ExecutionException e) {
cache.remove(arg);
System.out.println("计算错误,重新计算");
} catch (CancellationException e) {
cache.remove(arg);
System.out.println("被取消了");
throw e;
}
}
}
public final static ScheduledExecutorService excutor = Executors.newScheduledThreadPool(5);
public V compute(A arg, long expire) throws Exception {
if (expire > 0) {
excutor.schedule(new Runnable() {
@Override
public void run() {
expire(arg);
}
}, expire, TimeUnit.MILLISECONDS);
}
return compute(arg);
}
public V computeRandom(A arg) throws Exception {
long randomExpire = (long) (5000 + (Math.random() * 10000));
return compute(arg, randomExpire);
}
public synchronized void expire(A key) {
Future<V> future = cache.get(key);
if (future != null) {
System.out.println("清除掉,过期时间已到");
if (!future.isDone()) {
future.cancel(true);
}
cache.remove(key);
}
}
public static void main(String[] args) throws Exception {
ConcurrentCache<String, Integer> expensiveCompute = new ConcurrentCache<>(new MayFail());
ExecutorService service = Executors.newFixedThreadPool(500);
System.out.println("开始计算了");
long start = System.currentTimeMillis();
for (int i = 0; i < 500; i++) {
int finalI = i;
service.submit(() -> {
try {
System.out.println(Thread.currentThread().getName() + "开始等待");
expensiveCompute.countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "被放行");
Integer compute = expensiveCompute.computeRandom(String.valueOf(666));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(5000);
expensiveCompute.countDownLatch.countDown();
service.shutdown();
/*Thread.sleep(3000);
System.out.println("开始第二次的缓存取值");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.computeRandom(String.valueOf(666));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(5000);
System.out.println("开始第三次的缓存取值");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.computeRandom(String.valueOf(666));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}*/
// service.shutdown();
// while (!service.isTerminated()) {
//
// }
// System.out.println("总耗时: " + (System.currentTimeMillis() - start));
}
}
ThreadLocal来确认时间的统一性
且避免了每个请求都打印时间的情况,只让每个线程单独打印使用
/**
* 使用ThreadLocal确保时间统一性
*/
public class ConcurrentCache<A, V> implements Computbale<A, V> {
private final CountDownLatch countDownLatch = new CountDownLatch(1);
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computbale<A, V> computbale;
public ConcurrentCache(Computbale<A, V> computbale) {
this.computbale = computbale;
}
@Override
public V compute(A arg) throws Exception{
while (true) {
Future<V> future = cache.get(arg);
if (future == null) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return computbale.compute(arg);
}
};
FutureTask<V> futureTask = new FutureTask<>(callable);
// 往里放, 如果这个key不存在的话,相反存在就会将存在的值进行返回
future = cache.putIfAbsent(arg, futureTask);
if (future == null) {
future = futureTask;
System.out.println("从FutureTask调用了计算逻辑");
futureTask.run();
}
}
try {
return future.get();
} catch (InterruptedException e) {
cache.remove(arg);
System.out.println("被中断了");
throw e;
} catch (ExecutionException e) {
cache.remove(arg);
System.out.println("计算错误,重新计算");
} catch (CancellationException e) {
cache.remove(arg);
System.out.println("被取消了");
throw e;
}
}
}
public final static ScheduledExecutorService excutor = Executors.newScheduledThreadPool(5);
public V compute(A arg, long expire) throws Exception {
if (expire > 0) {
excutor.schedule(new Runnable() {
@Override
public void run() {
expire(arg);
}
}, expire, TimeUnit.MILLISECONDS);
}
return compute(arg);
}
public V computeRandom(A arg) throws Exception {
long randomExpire = (long) (5000 + (Math.random() * 10000));
return compute(arg, randomExpire);
}
public synchronized void expire(A key) {
Future<V> future = cache.get(key);
if (future != null) {
System.out.println("清除掉,过期时间已到");
if (!future.isDone()) {
future.cancel(true);
}
cache.remove(key);
}
}
public static void main(String[] args) throws Exception {
ConcurrentCache<String, Integer> expensiveCompute = new ConcurrentCache<>(new MayFail());
ExecutorService service = Executors.newFixedThreadPool(500);
System.out.println("开始计算了");
long start = System.currentTimeMillis();
for (int i = 0; i < 500; i++) {
int finalI = i;
service.submit(() -> {
try {
System.out.println(Thread.currentThread().getName() + "开始等待");
expensiveCompute.countDownLatch.await();
System.out.println(ThreadSafeFormatter.dateFormatter.get().format(new Date()));
System.out.println(Thread.currentThread().getName() + "被放行");
Integer compute = expensiveCompute.computeRandom(String.valueOf(666));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(5000);
expensiveCompute.countDownLatch.countDown();
service.shutdown();
/*Thread.sleep(3000);
System.out.println("开始第二次的缓存取值");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.computeRandom(String.valueOf(666));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(5000);
System.out.println("开始第三次的缓存取值");
for (int i = 0; i < 10; i++) {
int finalI = i;
service.submit(() -> {
try {
Integer compute = expensiveCompute.computeRandom(String.valueOf(666));
System.out.println("第"+ (finalI + 1) +"次计算结果:" + compute);
} catch (Exception e) {
e.printStackTrace();
}
});
}*/
// service.shutdown();
// while (!service.isTerminated()) {
//
// }
// System.out.println("总耗时: " + (System.currentTimeMillis() - start));
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatter = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("mm:ss");
}
@Override
public SimpleDateFormat get() {
return super.get();
}
};
}