Java并发编程:Callable与ReentrantLock实战解析
JUCCallable 接口1. 继承 Thread包含匿名内部类2.实现 Runnable包含匿名内部类3.基于 lambda4.基于 CallableRunnable 关注的是这个过程不关注执行结果Callable 要关注执行结果Callable 提供的 call 方法返回值就是线程执行任务得到的结果当我们要想获得结果就得专门创建一个成员变量保存计算结果。如果使用Callable 就可以避免引入额外的成员变量了。因为 Thread 没有提供构造函数来传入 callable 所以 引入一个 FutureTask 类作为 Thread 和 callable 之间的联系桥梁futureTaskget带有阻塞功能如果任务没执行完是不会获取到结果Callable 能干的事情使用 Runnable 也能干。对于这种带有返回值的任务使用 Callable 会更好一些代码更直观这里的 Future Task 起到的作用 十分重要ReentrantLock 可重入锁在之前sychronized 没有现在这样的强大ReentrantLock 用来实现可重入锁的选择传统锁的风格这个对象提供了两个方法lockunlock这个方法可能引起加了锁之后就忘了去解锁了在 unlock 之前触发了 return 或者 异常就可能引起unlock 执行不到了正确使用 ReetrantLock 就需要把 unlock 操作放到 finally中既然有了 synchronized 为啥还要有 ReentrantLock1.ReentrantLock 提供了 tryLock 操作lock 直接进行加锁加锁不成就要阻塞tryLock尝试进行加锁如果加锁不成不阻塞直接返回false2.ReentrantLock 提供了公平锁的实现通过队列记录加锁线程的先后顺序synchronized 是非公平锁ReentrantLock 构造方法中填写参数就可以设置成公平锁3.搭配的等待通知机制不同的对于 sychronized 搭配 wait/notify对于 ReentrantLock 搭配 Condition 类功能比wait notify 略强一点停车场停车场一般会显示当前剩余的车位开进去一个车 车位就减一开出来一个车车位就加一如果为0就不能开进来了剩余的车位就是信号量表示“可用资源的个数”申请一个可用资源就会使数字减一这个操作称为 P 操作释放一个可用资源就会使数字加一这个操作称为 V 操作如果数值为 0 了继续 P 操作P操作就会阻塞public class ThreadDemo37 { public static void main(String[] args) throws InterruptedException { Semaphore semaphore new Semaphore(1); semaphore.acquire(); System.out.println(P操作); semaphore.acquire(); System.out.println(P操作); semaphore.acquire(); System.out.println(P操作); semaphore.release(); } }public class ThreadDemo38 { private static int count 0; public static void main(String[] args) throws InterruptedException { Semaphore semaphore new Semaphore(1); Thread t1 new Thread(() - { for (int i 0; i 50000; i) { try { semaphore.acquire(); } catch (InterruptedException e) { throw new RuntimeException(e); } count; semaphore.release(); } }); Thread t2 new Thread(() - { for (int i 0; i 50000; i) { try { semaphore.acquire(); } catch (InterruptedException e) { throw new RuntimeException(e); } count; semaphore.release(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }如何确保线程安全1.sychronized2.ReentrantLock3.CAS原子类4.Semaphoresemaphore 也可以用来实现生产者消费者模型定义两个信号量一个用来表示队列中有多少个可以被消费的元素 sem1一个用来表示队列中有多少个可以放置新元素的空间 sem2生产一个元素sem1.Vsem2.P()消费一个元素sem1.Psem2.V下载一个文件肯呢个很大但是可以拆成多个部分每个线程负责下载一部分下载完成之后最终把下载的结果凭借到一起像多线程下载这样的场景最终执行完成之后要把所有内容拼到一起这个拼必然要等到所有线程执行完成使用 CountDownLatch 就可以很方便感知到这个事情如果使用 join 方式就只能使用每个线程执行一个任务借助 countDownLatch 就可以让一个线程执行多个任务public static void main(String[] args) throws InterruptedException { //1.此处构造方法中写 10意思是有 10 个线程/任务 CountDownLatch latch new CountDownLatch(10); //创建 10 个线程负责下载 for (int i 0; i 10; i) { int id i; Thread t new Thread(() - { Random random new Random(); int time (random.nextInt(5)1) * 1000; System.out.println(线程 id 开始下载); try { Thread.sleep(time); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(线程 id 结束下载); //告知 countDownLatch 执行结束了 latch.countDown(); }); t.start(); } latch.await(); System.out.println(所有任务已经完成了); }多环境下使用ArrayListVector Stack Hashtable 把关键方法都加上了 sychronized因此这几个无论如何都得加锁是不科学的Collections.synchronizedList(new ArrayList)给 ArrayList 套了个壳ArrayList 各种操作本身不带锁的通过上述套壳之后得到了新的对象新的对象里面的关键方法都是带锁的使用 CopyOnWriteArrayListCopyOnWrite容器即写时复制的容器。写时拷贝线程安全问题多个修改同一个线程如果多线程读这个顺序表没有任何线程安全问题一旦有线程要修改这里的值把顺序表复制一份修改新的顺序表内容并且修改引用的指向这个操作是原子的必须要加锁这种操作也有很大的局限性1.修改不能太频繁2.顺序表也不应该太大多线程环境使用队列1.自己加锁2.BlockingQueue 线程安全的多线程环境使用 哈希表HashMap 肯定不行 属于线程不安全的更靠谱的Hashtable们就在关键方法上添加了 sychronized后来标准库又引入了一个更好的解决方案ConcurrHashMapConcurrHashMap 的改进1.缩小了锁的粒度直接在方法上使用 sychronized就相当于是对this 加锁此时尝试修改两个不同链表上的元素都会出发锁冲突ConcurrHashMap 就是把锁变小了给每个链表都发了一个锁上述设定不会产生更多的空间代价因为 java 中任何一个对象都可以直接作为锁对象本身哈希表中就得有数组数组的元素都是已经存在的每个链表的头结点此时只要使用数组元素链表头结点作为加锁的对象即可锁桶Hash桶构成一个类似于“桶”每个链表就是构成桶的一个木板所谓“锁桶”就是针对每个木板每个链表分别加锁的在 java1.7及其之前 ConcurrentHashMap 是通过“分段锁” 来实现的给若干个链表分配一把锁这种设定不太合适实现更复杂效率也不够引入额外的空间开销Java 8 开始这里的设定就成了每个链表一把锁了2.充分使用了CAS原子操作减少一些加锁比如 针对哈希表元素个数的维护sychronized 里头刚开始是偏向锁或者轻量级锁速度很快但 sychronized 也有可能成为重量级锁是够升级了但咱们不可控3.针对扩容操作优化扩容是一个重量操作负载因子--描述了每个桶上平均有多少个元素此时桶上的链表元素个数不应该太长如果太长1.变成树2.扩容创建一个更大的数组把旧的Hash表的元素搬运到删除/插入新的数组上如果hash表本身元素非常多这里的扩容操作就会消耗很长时间hash表表现不稳定平时很快突然某个操作就满了过一会又快了无法控制合适出发扩容一旦扩容触发了就会导致这次操作非常耗时HashMap 的扩容操作是一把锁在某一次插入元素操作中整体完成扩容了ConcurrentHashMap 则是每次操作都只搬运一部分元素