JAVA并发编程实战——构建块

思维导图在这里插入图片描述

1 同步容器

如java的Vector和Hashtable

这些容器通过封装它们的状态,并对每一个公共方法进行同步实现了线程安全。

1.1 同步容器的问题

  1. 并发程度小,一次只能有一个线程访问。
  2. 对于复合操作,需要外部添加额外的代码同步。

示例代码:

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,意味着如果线程被中断,则该方法抛出中断异常,线程提前结束阻塞状态。

中断是一种协作机制。

当我们调用一个阻塞方法时需要注意如何处理中断,通常有以下两种操作:

  1. 传递InterruptedException,将该异常交给调用方进行处理。
  2. 恢复中断,比如如果捕获该异常,需要进行恢复中断,而不能不做任何响应。

如下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并发编程实战》.

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LamaxiyaFc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值