1、CountDownLatch(计数器)
CountDownLatch 类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在await()锁上等待的线程就可以恢复执行任务。
package com.zhang; import java.util.concurrent.CountDownLatch; public class TestCountDownLatch { public static void main(String[] args) throws InterruptedException { final CountDownLatch countDownLatch = new CountDownLatch(2); new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName() + ",子线程开始执行..."); countDownLatch.countDown(); System.out.println(Thread.currentThread().getName() + ",子线程结束执行..."); } }).start(); new Thread(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName() + ",子线程开始执行..."); countDownLatch.countDown();//计数器值每次减去1 System.out.println(Thread.currentThread().getName() + ",子线程结束执行..."); } }).start(); countDownLatch.await();// 減去为0,恢复任务继续执行 System.out.println("两个子线程执行完毕...."); System.out.println("主线程继续执行....."); for (int i = 0; i < 10; i++) { System.out.println("main,i:" + i); } } }
2、CyclicBarrier(屏障)
CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续CyclicBarrier就象它名字的意思一样,可看成是个障碍,所有的线程必须到齐后才能一起通过这个障碍。
CyclicBarrier初始时还可带一个Runnable的参数,此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。
import java.util.concurrent.CyclicBarrier; class Writer extends Thread { private CyclicBarrier cyclicBarrier; public Writer(CyclicBarrier cyclicBarrier) { this.cyclicBarrier = cyclicBarrier; } @Override public void run() { try { System.out.println("线程" + Thread.currentThread().getName() + ",正在写入数据"); Thread.sleep(3000); System.out.println("线程" + Thread.currentThread().getName() + ",写入数据成功....."); cyclicBarrier.await(); System.out.println("所有线程执行完毕.........."); } catch (Exception e) { } } } public class Test001 { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() { public void run() { System.out.println("全部线程唤醒前被执行"); } }); for (int i = 0; i < 5; i++) { Writer writer = new Writer(cyclicBarrier); writer.start(); } } }
3、Semaphore(计数信号量)
Semaphore是一种基于计数的信号量。它可以设定一个阈值,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后线程申请许可信号将会被阻塞。
需求: 一个厕所只有3个坑位,但是有10个人来上厕所,那怎么办?假设10的人的编号分别为1-10,并且1号先到厕所,10号最后到厕所。那么1-3号来的时候必然有可用坑位,顺利如厕,4号来的时候需要看看前面3人是否有人出来了,如果有人出来就进去,否则等待。同样的道理,4-10号也需要等待正在上厕所的人出来后才能进去,并且谁先进去这得看等待的人是否有素质,是否能遵守先来先上的规则。(公平锁与非公平锁:排队和竞争)
package com.zhang.test; import java.util.concurrent.Semaphore; class ThradDemo001 extends Thread { private String name; private Semaphore wc; public ThradDemo001(String name, Semaphore wc) { this.name = name; this.wc = wc; } @Override public void run() { try { // 剩下的资源 int availablePermits = wc.availablePermits(); if (availablePermits > 0) { System.out.println(name + "天助我也,终于有茅坑了....."); } else { System.out.println(name + "怎么没有茅坑了..."); } // 申请资源 wc.acquire(); System.out.println(name + "终于上厕所啦.爽啊" + ",剩下厕所:" + wc.availablePermits()); Thread.sleep(1000); System.out.println(name + "厕所上完啦!"); // 释放资源 wc.release(); } catch (Exception e) { } } } public class TestSemaphore { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3); for (int i = 1; i <= 10; i++) { ThradDemo001 thradDemo001 = new ThradDemo001("第" + i + "个人", semaphore); thradDemo001.start(); } } }
在很多情况下,可能有多个线程需要访问数目很少的资源。假想在服务器上运行着若干个回答客户端请求的线程。这些线程需要连接到同一数据库,但任一时刻只能获得一定数目的数据库连接。你要怎样才能够有效地将这些固定数目的数据库连接分配给大量的线程?
答:1.给方法加同步锁,保证同一时刻只能有一个人去调用此方法,其他所有线程排队等待,但是此种情况下即使你的数据库链接有10个,也始终只有一个处于使用状态。这样将会大大的浪费系统资源,而且系统的运行效率非常的低下。
2.另外一种方法当然是使用信号量,通过信号量许可与数据库可用连接数相同的数目,将大大的提高效率和性能。
3、并发队列
在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能非阻塞队列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种都继承自Queue。
4、阻塞队列与非阻塞队
阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得不满
1.ArrayDeque, (数组双端队列)
2.PriorityQueue, (优先级队列)
3.ConcurrentLinkedQueue, (基于链表的并发队列)
4.DelayQueue, (延期阻塞队列)(阻塞队列实现了BlockingQueue接口)
5.ArrayBlockingQueue, (基于数组的并发阻塞队列)
6.LinkedBlockingQueue, (基于链表的FIFO阻塞队列)
7.LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)
8.PriorityBlockingQueue, (带优先级的无界阻塞队列)
9.SynchronousQueue (并发同步阻塞队列)
5、ConcurrentLinkedQueue(非阻塞)
ConcurrentLinkedQueue : 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue.它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不允许null元素。
ConcurrentLinkedQueue重要方法:
add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中这俩个方法没有任何区别)
poll() 和peek() 都是取头元素节点,区别在于前者会删除元素,后者不会。
package com.zhang.test; import java.util.concurrent.ConcurrentLinkedQueue; public class TestConcurrentLinkedQueue { public static void main(String[] args) { ConcurrentLinkedQueue q = new ConcurrentLinkedQueue(); q.offer("张三"); q.add("add"); q.offer("李四"); q.offer("哈哈哈"); System.out.println(q.poll());//从头获取元素,删除该元素 System.out.println(q.peek()); //从头获取元素,不刪除该元素 System.out.println(q.size()); //获取总长度 } }
6、BlockingQueue
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。因此当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。阻塞队列是线程安全的。
7、ArrayBlockingQueue
ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。
package com.zhang.test; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; public class TestArrayBlockingQueue { public static void main(String[] args) throws Exception{ ArrayBlockingQueue<String> arrays = new ArrayBlockingQueue<String>(3); arrays.add("李四"); arrays.add("张军"); arrays.add("张军"); // 添加阻塞队列 arrays.offer("张三", 1, TimeUnit.SECONDS); System.out.println(arrays.poll()); System.out.println(arrays.poll()); System.out.println(arrays.poll()); System.out.println(arrays.poll());//null } }
8、LinkedBlockingQueue
LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。和ArrayBlockingQueue一样,LinkedBlockingQueue 也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。
package com.zhang.test; import java.util.concurrent.LinkedBlockingQueue; public class TestLinkedBlockingQueue { public static void main(String[] args) { LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3); linkedBlockingQueue.add("张三"); linkedBlockingQueue.add("李四"); linkedBlockingQueue.add("李四"); // linkedBlockingQueue.add("王五"); //抛异常 System.out.println(linkedBlockingQueue.size()); } }
9、使用BlockingQueue模拟生产者与消费者
package com.zhang.test; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; class ProducerThread implements Runnable { private BlockingQueue<String> blockingQueue; private AtomicInteger count = new AtomicInteger(); private volatile boolean FLAG = true; public ProducerThread(BlockingQueue<String> blockingQueue) { this.blockingQueue = blockingQueue; } public void run() { System.out.println(Thread.currentThread().getName() + "生产者开始启动...."); while (FLAG) { String data = count.incrementAndGet() + ""; try { boolean offer = blockingQueue.offer(data, 2, TimeUnit.SECONDS); if (offer) { System.out.println(Thread.currentThread().getName() + ",生产队列" + data + "成功.."); } else { System.out.println(Thread.currentThread().getName() + ",生产队列" + data + "失败.."); } Thread.sleep(1000); } catch (Exception e) { } } System.out.println(Thread.currentThread().getName() + ",生产者线程停止..."); } public void stop() { this.FLAG = false; } } class ConsumerThread implements Runnable { private volatile boolean FLAG = true; private BlockingQueue<String> blockingQueue; public ConsumerThread(BlockingQueue<String> blockingQueue) { this.blockingQueue = blockingQueue; } public void run() { System.out.println(Thread.currentThread().getName() + "消费者开始启动...."); while (FLAG) { try { String data = blockingQueue.poll(2, TimeUnit.SECONDS); if (data == null) { FLAG = false; System.out.println("消费者超过2秒时间未获取到消息."); return; } System.out.println("消费者获取到队列信息成功,data:" + data); } catch (Exception e) { // TODO: handle exception } } } } public class Test0008 { public static void main(String[] args) { LinkedBlockingQueue<String> blockingQueue = new LinkedBlockingQueue<String>(3); ProducerThread producerThread = new ProducerThread(blockingQueue); ConsumerThread consumerThread = new ConsumerThread(blockingQueue); Thread t1 = new Thread(producerThread); Thread t2 = new Thread(consumerThread); t1.start(); t2.start(); //10秒后 停止线程.. try { Thread.sleep(10*1000); producerThread.stop(); } catch (Exception e) { // TODO: handle exception } } }
10、线程池的作用
合理地使用线程池能够带来3个好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
11、ThreadPoolExecutor机制
corePoolSize |
核心线程池大小 |
maximumPoolSize |
最大线程池大小 |
keepAliveTime |
线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程有效时间 |
TimeUnit |
keepAliveTime时间单位 |
workQueue |
阻塞任务队列 |
threadFactory |
新建线程工厂 |
RejectedExecutionHandler |
当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理 |
- 当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
- 当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
- 当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
- 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
- 当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
- 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
12、线程池原理剖析
提交一个任务到线程池中,线程池的处理流程如下:
- 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
- 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
- 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
13、线程池四种创建方式
Java通过Executors(jdk1.5并发包)提供四种线程池:
newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
// 无限大小线程池 jvm自动回收 ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int temp = i; newCachedThreadPool.execute(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName() + ",i:" + temp); } }); }
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
ExecutorService newFixedThreadPool= Executors.newFixedThreadPool(10); for (int i=0;i<100;i++) { final int temp = i; newFixedThreadPool.execute(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+",i"+temp); } }); }
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5); for (int i = 0; i < 10; i++) { final int temp = i; newScheduledThreadPool.schedule(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+",i:" + temp); } }, 3, TimeUnit.SECONDS);//表示延迟3秒执行 }
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
//结果依次输出,相当于顺序执行各个任务。 ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { final int index = i; newSingleThreadExecutor.execute(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+",index:" + index); try { Thread.sleep(200); } catch (Exception e) { } } }); }
14、自定义线程线程池
package com.zhang.test; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * //提交一个任务到线程池中,线程池的处理流程如下: * //1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。 * //2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。 * //3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。 */ public class Test0007 { public static void main(String[] args) { /** * int corePoolSize 核心线程数 * int maximumPoolSize 最大能创建多少个线程 * long keepAliveTime 存活时间 * TimeUnit unit, * BlockingQueue<Runnable> workQueue 线程队列 */ ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3)); for (int i = 1; i <= 5; i++) { final int temp=i; threadPoolExecutor.execute(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName()+"任务"+temp); } }); } threadPoolExecutor.shutdown(); } }
15、合理配置线程池
CPU密集:是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务
IO密集:即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数
16、Callable与Future
在Java中创建线程一般有两种方式,一种是继承Thread类,一种是实现Runnable接口。这两种方式的缺点是在线程任务执行结束后,无法获取执行结果。我们一般只能采用共享变量或共享存储区以及线程通信的方式实现获得任务结果的目的。Java中也提供了使用Callable和Future来实现获取任务结果的操作。Callable用来执行任务产生结果,而Future用来获得结果。Future常用方法:
- V get() :获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。
- V get(Long timeout , TimeUnit unit) :获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
- boolean isDone() :如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。
- boolean isCanceller() :如果任务完成前被取消,则返回true。
- boolean cancel(boolean mayInterruptRunning) :如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(...)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。
通过方法分析我们也知道实际上Future提供了3种功能:
- 能够中断执行中的任务
- 判断任务是否执行完成
- 获取任务执行完成后的结果
package com.zhang.test; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class TestMain { public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newCachedThreadPool(); Future<Integer> future = executor.submit(new Callable<Integer>() { public Integer call() throws Exception { System.out.println("子线程执行call()"); Thread.sleep(5000); return 200; } }); System.out.println("主线程执行其他任务"); Integer integer = future.get();//等待返回结果,这里会阻塞 System.out.println(integer); // 关闭线程池 if (executor != null) executor.shutdown(); } }
17、Futrure模式
在多线程中经常举的一个例子就是:网络图片的下载,刚开始是通过模糊的图片来代替最后的图片,等下载图片的线程下载完图片后在替换。而在这个过程中可以做一些其他的事情。首先客户端向服务器请求RealSubject,但是这个资源的创建是非常耗时的。这种情况下,首先返回Client一个FutureSubject,以满足客户端的需求,于此同时,Future会通过另外一个Thread 去构造一个真正的资源,资源准备完毕之后,在给future一个通知。如果客户端急于获取这个真正的资源,那么就会阻塞客户端的其他所有线程,等待资源准备完毕
package com.zhang.future; /** * 公共数据接口,FutureData和RealData都要实现 */ public interface Data { String getRequest(); } package com.zhang.future; public class RealData implements Data { private String result; public RealData(String data) { System.out.println("正在使用data:" + data + "网络请求数据,耗时操作需要等待."); try { Thread.sleep(3000); } catch (Exception e) { } System.out.println("操作完毕,获取结果..."); result = "哈哈哈"; } public String getRequest() { return result; } } package com.zhang.future; /** * FutureData,当有线程想要获取RealData的时候,程序会被阻塞。等到RealData被注入才会使用getReal()方法 */ public class FurureData implements Data { private volatile static boolean ISFLAG = false; private RealData realData; public synchronized void setRealData(RealData realData) { // 如果已经获取到结果,直接返回 if (ISFLAG) { return; } // 如果没有获取到数据,传递真是对象 this.realData = realData; ISFLAG = true; // 进行通知 notify(); } public synchronized String getRequest() { while (!ISFLAG) { try { wait(); } catch (Exception e) { } } // 获取到数据,直接返回 return realData.getRequest(); } } package com.zhang.future; public class FutureClient { public Data request( final String queryStr) { final FurureData furureData = new FurureData(); new Thread(new Runnable() { public void run() { RealData realData = new RealData(queryStr); furureData.setRealData(realData); } }).start(); return furureData; } } package com.zhang.future; public class Main { public static void main(String[] args) { FutureClient futureClient = new FutureClient(); Data request = futureClient.request("请求参数."); System.out.println("请求发送成功!"); System.out.println("执行其他任务..."); String result = request.getRequest(); System.out.println("获取到结果..." + result); } }
18、悲观锁与乐观锁
悲观锁:悲观锁悲观的认为每一次操作都会造成更新丢失问题,在每次查询时加上排他锁。每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。synchronized的思想也是悲观锁。
Select * from xxx for update;
乐观锁:乐观锁会乐观的认为每次查询都不会造成更新丢失,利用版本字段控制。一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
19、重入锁
重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。ReentrantLock和synchronized 都是可重入锁
package com.zhang.cache; public class Test implements Runnable { private synchronized void get() { System.out.println("name:" + Thread.currentThread().getName() + " get();"); set(); } private synchronized void set() { System.out.println("name:" + Thread.currentThread().getName() + " set();"); } public void run() { get(); } public static void main(String[] args) { Test ss = new Test(); new Thread(ss).start(); new Thread(ss).start(); new Thread(ss).start(); new Thread(ss).start(); } }
package com.zhang.cache; import java.util.concurrent.locks.ReentrantLock; public class Test02 extends Thread { ReentrantLock lock = new ReentrantLock(); private void get() { lock.lock(); System.out.println(Thread.currentThread().getId()+",get()"); set(); lock.unlock(); } private void set() { lock.lock(); System.out.println(Thread.currentThread().getId()+",set()"); lock.unlock(); } public void run() { get(); } public static void main(String[] args) { Test02 ss = new Test02(); new Thread(ss).start(); new Thread(ss).start(); new Thread(ss).start(); } }
20、读写锁
假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。
package com.zhang.cache; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class Cache { static Map<String, Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); static Lock w = rwl.writeLock(); // 获取一个key对应的value private static Object get(String key) { r.lock(); try { System.out.println("正在做读的操作,key:" + key + " 开始"); Thread.sleep(100); Object object = map.get(key); System.out.println("正在做读的操作,key:" + key + ",value:" + object + "结束."); return object; } catch (InterruptedException e) { } finally { r.unlock(); } return key; } // 设置key对应的value private static Object put(String key, Object value) { w.lock(); try { System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始."); Thread.sleep(100); Object object = map.put(key, value); System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束."); return object; } catch (InterruptedException e) { } finally { w.unlock(); } return value; } public static void main(String[] args) { new Thread(new Runnable() { public void run() { for (int i = 0; i < 10; i++) { Cache.put(i + "", i + ""); } } }).start(); new Thread(new Runnable() { public void run() { for (int i = 0; i < 10; i++) { Cache.get(i + ""); } } }).start(); } }
21、CAS无锁机制
无锁的好处:
- 由于其非阻塞性,它对死锁问题天生免疫,并且线程间的相互影响也远远比基于锁的方式要小。
- 使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此它要比基于锁的方式拥有更优越的性能。
CAS算法理解:
- 它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
- CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
- 简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。
CAS缺点:
- CAS存在一个很明显的问题,即ABA问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性
- CPU开销较大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力
- 不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
22、自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋:死循环),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,线程不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
23、常用原子类
Java中的原子操作类大致可以分为4类:原子更新基本类型、原子更新数组类型、原子更新引用类型、原子更新属性类型。这些原子类中都是用了无锁的概念,有的地方直接使用CAS操作的线程安全的类型。
AtomicBoolean
AtomicInteger
AtomicLong
AtomicReference
package com.zhang.future; import java.util.concurrent.atomic.AtomicInteger; public class Test0001 implements Runnable { private static AtomicInteger atomic = new AtomicInteger(); public void run() { while (true) { int count = getCountAtomic(); System.out.println(count); if (count >= 150) { break; } } } private Integer getCountAtomic() { try { Thread.sleep(50); } catch (Exception e) { } return atomic.incrementAndGet(); } public static void main(String[] args) { Test0001 test0001 = new Test0001(); Thread t1 = new Thread(test0001); Thread t2 = new Thread(test0001); t1.start(); t2.start(); } }
24、并发框架Disruptor