目录
思维导图![在这里插入图片描述](https://img-blog.csdnimg.cn/9d4229afe52b4c358fc35d45098ac6a6.png)
1 同步容器
如java的Vector和Hashtable
这些容器通过封装它们的状态,并对每一个公共方法进行同步实现了线程安全。
1.1 同步容器的问题
- 并发程度小,一次只能有一个线程访问。
- 对于复合操作,需要外部添加额外的代码同步。
示例代码:
public class SynchronousExampleOne {
public static Object getLast(Vector<Object> vector) {
int last = vector.size() - 1;
return vector.get(last);
}
public static void deleteLast(Vector<Object> vector) {
int last = vector.size() - 1;
vector.remove(last);
}
}
如果两个线程分别同时访问getLast和deleteLast可能出现如下情况:
可以看出会出现非预期的结果。
此时我们需要在方法上加synchronized进行同步。
1.2 迭代器和并发修改异常(ConcurrentModificationException)
在设计同步容器的迭代器时,没有考虑并发修改的问题,它们是及时失败——fail-fast的,当在迭代过程发现被修改,将会抛出ConcurrentModificationException。
示例代码:
public class ConcurrentModificationExample {
private List<Integer> list = Collections.synchronizedList(new ArrayList<>());
public ConcurrentModificationExample() {
list.add(1);
list.add(12);
list.add(13);
}
public static void main(String[] args) {
ConcurrentModificationExample testObject = new ConcurrentModificationExample();
Thread thread = new Thread(() -> {
for (int i = 10; i < 100; i++) {
testObject.list.add(i);
}
});
thread.start();
for (Integer o: testObject.list) {
System.out.println(o);
}
}
}
运行结果:
解决方法:迭代期间对容器加锁,或者使用迭代器的add和remove方法进行元素添加和移除。
2 并发容器
Java5.0 提供了几种并发的容器改进了同步容器,如ConcurrentHashMap和CopyOnWriteArrayList。
用并发容器替代同步容器,可以显著的提高扩展性和并发性。
2. 1 ConcurrentHashMap
ConcurrentHashMap同样是哈希表,但是拥有更加细化的锁机制:分段锁,读写线程可以并发的访问map,同时还支持并发的修改map不同分段。
参考如下put源码:
可见并发的修改是基于分段的,只能并发加锁修改不同的哈希表索引分段。
同时ConcurrentHashMap修改了同步容器迭代器的并发修改失败,迭代过程允许并发修改。
2.2 并发容器附加的原子操作
类似同步容器,并没有提供缺少就加入这种原子操作。而并发容器已经实现这些原子操作,不需要外部额外的代码进行同步。
如下并发容器顶级接口:
已经默认定义了复杂操作的原子操作定义,由实现类实现。
2.3 CopyOnWriteArrayList
同步list的并发替代,通过写时复制,避免对容器进行加锁。
它的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();
}
}
可见添加是:通过内部lock锁对数据先复制一份,然后进行修改,之后再赋值给原数组。只能一个线程进行add操作。
而读取操作一直是对原数组进行操作,可以多个线程进行,不受修改线程影响。
3 阻塞队列和生产者-消费者模型
阻塞队列提供可阻塞的put和take方法,同时支持中断响应。
如下是BlockQueue接口定义的方法:
可以看出put和take都是阻塞方法(抛出中断异常),也就是说如果put一个满的队列和take一个空的队列会导致线程进入阻塞状态。而支持中断则可以中断这种阻塞状态。
常见的生产者-消费者模式是线程池和工作队列相结合。
如下实例演示生产者-消费者模式:
@Data
@NoArgsConstructor
public class ProducerAndConsumerExample {
private BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(10);
public static void main(String[] args) {
ProducerAndConsumerExample exampleObject = new ProducerAndConsumerExample();
Thread produce = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
System.out.println("生产整数:" + i);
exampleObject.blockingQueue.put(i);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "被中断了");
e.printStackTrace();
}
}
});
Thread consumer = new Thread(() -> {
while (true) {
try {
System.out.println("消费整数:" + exampleObject.blockingQueue.take());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "被中断了");
e.printStackTrace();
}
}
});
produce.start();
consumer.start();
}
}
运行结果:
由BlockQueue作为工作队列,使得生产者和消费者进行解耦,具体操作都绑定到对工作队列的操作上。
4 阻塞和可中断方法
阻塞可能会因为IO操作未结束、等待获取锁或等待线程唤醒Thread.sleep。
阻塞操作和普通操作区别:被阻塞线程必须等待一个事件的发生才能继续进行,并且这个事件是不处于它控制的。
阻塞队列中的put和take就是阻塞方法,因为抛出了中断异常InterruptedException,意味着如果线程被中断,则该方法抛出中断异常,线程提前结束阻塞状态。
中断是一种协作机制。
当我们调用一个阻塞方法时需要注意如何处理中断,通常有以下两种操作:
- 传递InterruptedException,将该异常交给调用方进行处理。
- 恢复中断,比如如果捕获该异常,需要进行恢复中断,而不能不做任何响应。
如下demo:
public class InterruptedExample extends Thread {
private BlockingQueue<Integer> blockingQueue;
@Override
public void run() {
try {
blockingQueue.take();
} catch (InterruptedException e) {
//正确——恢复中断,设置中断状态位true
Thread.currentThread().interrupt();
//错误——什么都不做
}
}
}
我们应该捕获,并设置中断位,不能不做处理。
5 Synchronizer同步器
Synchronizer同步器是一个对象,根据本身的状态控制调节线程的控制流。
处理上述的阻塞队列,其他类型的同步器有:信号量(semaphore)、关卡(barrier)和闭锁(latch)
等。
5.1 闭锁
闭锁是一种同步器,它可以延迟线程的进度直到线程到达终止。
比如CountDownLatch就是一个闭锁实现,运行一个或多个线程等待一个事件集的发生。
countDown方法对计数器做减一操作,表示一个事件已经完成。
await方法等待计数器达到0,此时所有等待事件都发生。如果计数器非零,await会一直阻塞到计数器为0,或者等待线程中断或超时。
CountDownLatch常用于启动和停止线程,如下示例demo:
public class CountDownLatchExample {
public long timeTasks(int nThreads, Runnable task) {
Assert.isTrue(nThreads>0, "线程个数必须大于0");
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for (int i = 0; i < nThreads; i++) {
Thread thread = new Thread() {
@Override
public void run() {
try {
startGate.await();
try {
task.run();
} finally {
endGate.countDown();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
}
long start = System.currentTimeMillis();
startGate.countDown();
try {
endGate.await();
long end = System.currentTimeMillis();
return end - start;
} catch (InterruptedException e) {
e.printStackTrace();
return 0L;
}
}
}
通过两个countdownlatch来计算执行时间。
5.2 FutureTask
FutureTask同样可以作为闭锁,它的计算通过Callable实现可以返回一个计算结果。
FutureTask有三种状态:等待、运行和完成。
完成包括:正常结束、取消和异常(InterruptedException | ExecutionException)
以下demo是一个演示FutureTask异步处理获取结果的示例:
public class FutureTaskExample {
private int count = 1;
private FutureTask<Integer> futureTask = new FutureTask<>(() -> {
Thread.sleep(5000);
int res = 100 / count;
System.out.println("计算完成:");
return 100;
});
private Thread thread;
public void startThread(FutureTask<Integer> futureTask) {
thread = new Thread(futureTask);
thread.start();
}
public static void main(String[] args) {
FutureTaskExample futureTaskExample = new FutureTaskExample();
futureTaskExample.normalExecution();
futureTaskExample.InterruptedExceptionExecution();
futureTaskExample.ExeExceptionExecution();
}
public void normalExecution() {
FutureTaskExample futureTaskExample = new FutureTaskExample();
futureTaskExample.startThread(futureTaskExample.futureTask);
futureTaskExample.count = 0;
try {
System.out.println("---------正常完成计算----------");
System.out.println(futureTaskExample.futureTask.get());
} catch (ExecutionException e) {
System.out.println("执行异常处理");
e.printStackTrace();
} catch (InterruptedException e) {
System.out.println("中断异常处理");
e.printStackTrace();
}
}
public void ExeExceptionExecution() {
FutureTaskExample futureTaskExample = new FutureTaskExample();
futureTaskExample.startThread(futureTaskExample.futureTask);
try {
System.out.println(futureTaskExample.futureTask.get());
} catch (ExecutionException e) {
System.out.println("执行异常处理");
e.printStackTrace();
} catch (InterruptedException e) {
System.out.println("中断异常处理");
e.printStackTrace();
}
}
public void InterruptedExceptionExecution() {
FutureTaskExample futureTaskExample = new FutureTaskExample();
futureTaskExample.startThread(futureTaskExample.futureTask);
Thread.currentThread().interrupt();
try {
System.out.println(futureTaskExample.futureTask.get());
} catch (ExecutionException e) {
System.out.println("执行异常处理");
e.printStackTrace();
} catch (InterruptedException e) {
System.out.println("中断异常处理");
e.printStackTrace();
}
}
}
运行结果如下:
结果显示了计算完成的三种状态:正常结束、任务执行异常比如运行时异常和调用线程被中断,不再进行阻塞等待结果。
5.3 信号量Semaphore
计数信号量:用于控制共享访问特定资源活动的数量。
Semaphore管理permit许可集来进行访问控制,只要还有permit就允许访问。如果没有acquire会被阻塞,直到有permit被释放。
信号量Semaphore示例如下,测试一个同时只允许三个线程访问的资源,访问过程:
public class SemaphoreExample {
private Semaphore semaphore;
private Set<String> resource = new HashSet<>();
public SemaphoreExample(int permit) {
resource.add("Beijing");
resource.add("Shanghai");
resource.add("Xian");
semaphore = new Semaphore(permit);
}
public void readResource() {
try {
this.semaphore.acquire();
System.out.println(Thread.currentThread().getName()+ "获取成功——打印获取的资源");
for (String s: this.resource
) {
System.out.println(s);
}
//休眠长时间观察,permit获取释放情况
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.semaphore.release();
}
}
public static void main(String[] args) {
SemaphoreExample semaphoreExample = new SemaphoreExample(3);
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
semaphoreExample.readResource();
});
thread.start();
}
}
}
前期运行结果:
可见一开始只有三个线程获取了permit执行了资源访问,之后过了50s当休眠结束,线程释放permit才开始唤醒阻塞的其余线程。
5.4 关卡CyclicBarrier
CyclicBarrier阻塞一组线程,使他们相互等待,直到最后一个线程到来,在一起执行一个屏障点内容。
CyclicBarrier:构造函数:
第一种:设置参与线程数,和所有线程都到达屏障点后,由最后一个到达者执行传入的barrierAction。
第二种:只设置线程数,达到屏障点后不做action,各自继续执行。
示例demo:
public class CyclicBarrierExample {
private CyclicBarrier cyclicBarrier;
public CyclicBarrierExample(int nthread) {
cyclicBarrier = new CyclicBarrier(nthread, () -> {
System.out.println("全部到达屏障点——执行屏障runnable");
System.out.println("合并子任务");
});
}
public static void main(String[] args) {
CyclicBarrierExample cyclicBarrierExample = new CyclicBarrierExample(2);
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
System.out.println("线程" + Thread.currentThread().getName() + "达到屏障点");
System.out.println("执行子任务");
try {
cyclicBarrierExample.cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
executorService.submit(() -> {
System.out.println("线程" + Thread.currentThread().getName() + "达到屏障点");
System.out.println("执行子任务");
try {
cyclicBarrierExample.cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
executorService.shutdown();
}
}
运行结果:
可见只有全部线程都到达,才会执行barrierAction。
参考文献
[1], 《JAVA并发编程实战》.