多线程进阶2

目录

JUC (java.util.concurrent)常见类 

Callable 接口

 ReentrantLock

信号量 Semaphore

CountDownLatch

线程安全的集合类

Collections.synchronizedList (new ArrayList);

CopyOnWriteArrayList   写时拷贝

多线程环境使用队列

多线程环境使用哈希表(重)

小结


JUC (java.util.concurrent)常见类 

这个包里面的内容主要是一些多线程相关的组件

Callable 接口

创建线程的一种方式, 想让某个线程执行一个逻辑并返回一个结果的时候使用

相比之下, Runnable 不关注结果

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo4 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            // Callable 中的和新方法, 返回值就是Intrger, 期望返回一个整数
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                 for(int i = 0; i < 1000; i++) {
                     sum += i;
                 }
                 return sum;
            }
        };
        // 把任务放到线程中执行
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        // 此处 get 就能获得到 callable 里面的返回结果
        // 由于线程是并发执行的, 执行到主线程的 get 的时候, t 线程可能还没执行完
        // 没执行玩的话 get 就会阻塞
        System.out.println(futureTask.get());
    }
}

 futureTask 是干嘛的?

例子:

我们去吃饭点菜之后拿到小票, 点餐完成之后, 后厨相当于一个线程, 开始执行, 这个过程等着, 等才做好之后凭小票取餐, futureTask 就相当于 小票, 拿小票换执行结果.

总结:

线程创建方式

1> 继承Thread 重写run (创建单独的类, 也可以匿名内部类)

2> 实现Runnable 重写run (创建单独的类, 也可以匿名内部类)

3> 实现 Callale 重写 call (创建单独的类, 也可以匿名内部类)

4> 使用 lambda 表达式

5> ThreadFactory 线程工厂

6> 线程池

.......

 ReentrantLock

可重入锁, 使用效果和 synchronized 类似

优势

1> ReentrantLock 在加锁的时候有两种方式, lock tryLock

使用了 lock 之后遇到锁冲突之后就会阻塞等待

使用 tryLock 遇到锁冲突就会放弃

tryLock 给了更多可操作空间.

2> ReentrantLock 提供了公平锁的实现 (默认情况下是非公平锁)

3> ReentrantLock 提供了更强大的等待通知机制, 搭配了 Condition 类实现等待通知

虽然有上述优势, 但在加锁时首选 synchronized, 因为其使用更复杂, 尤其是忘记解锁

信号量 Semaphore

操作系统中重要概念

例子:

找地方停车, 停车场的门口挂个牌子, 剩余 xxx 个车位, 车子进来数字 - 1, 车子出去 + 1

信号量就是一个计数器, 描述了 " 可用资源 " 的个数, 申请一个可用资源, 计数器 -1 (P 操作), 释放一个可用资源 计数器 +1 (V 操作) 

信号量初始情况时 10, 进行10次 P 操作, 数值就是 0, 如果继续 P 操作就会 阻塞等待(类似锁)

 锁就是一种特殊的信号量, 锁是可用资源 为 1的信号量, 加锁操作, P操作, 1 -> 0, 解锁操作 V操作 0 -> 1. 锁就叫做二元信号量

操作系统提供了 信号量 实现, 提供了 api, JVM 封装了这样的 api, 可以在 Java 代码中使用了.

代码实现

import java.util.concurrent.Semaphore;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(4);
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
    }
}

开发中遇到了需要申请资源的场景就可以使用 semaphore来实现了 

CountDownLatch

适用于  多个线程 来完成一系列任务的时候, 用来用来衡量任务的进度是否完成.

比如 : 需要把一个大的任务拆分成多个小任务, 并让这些任务并发执行

使用 countDownLatch 来判定这些任务是否全部完成了

下载一个文件就可以使用多线程下载

CountDownLatch 主要有两个方法:

1> await 调用的时候就会阻塞, 等待其他线程任务完成之后 await 才会返回继续往下走

2> countDown 告诉 countDownLatch 当前这一个字任务已经完成了

代码实现:

import java.util.concurrent.CountDownLatch;

public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        // 10 个选手参赛, await 就会在 10 次调用完, countDown 之后才能继续执行
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            int id = i;
            Thread t = new Thread(() -> {
                System.out.println("thread " + id);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 通知当前任务执行完毕了
                countDownLatch.countDown();
            });
            t.start();
        }
        // a => all
        countDownLatch.await();
        System.out.println("所有的任务都完成了.");
    }
}

线程安全的集合类

数据结构中大部分的集合类都是线程不安全. Vector, Stack, Hashtable 线程安全 => synchronized(不建议使用, 上古时期的东西)
针对线程不安全的集合类, 想要在多线程环境下使用需要考虑 线程安全问题

标准库提供了一些搭配的组件, 保证线程安全

Collections.synchronizedList (new ArrayList);

这个东西会返回一个新的对象, 这个对象相当于给 ArrayList 套了一层壳, 这层可就是在方法上直接使用 synchronized 的

CopyOnWriteArrayList   写时拷贝

举个例子:
如果两个线程同时使用一个 ArrayList, 可能会读也可能会修改, 如果两个线程读, 直接读就好了, 如果某个线程需要修改, 就把 ArrayList 复制出一份副本, 修改线程就修改这个副本, 与此同时另一个线程仍然可以读取数据 (从原来的数据进行读取), 修改完毕后就会使用修改好的数据代替原来的数据, 这样就不用加锁了.

1> 当前操作 ArrayList 不能太大(不然的话比加锁成本还高了)

2> 更适合一个线程去修改, 不能是多个线程同时修改(多个线程都, 一个线程修改)

适合于 服务器的配置更新, 通过配置文件来描述配置的详细内容(本身不大)

配置的内容被读到内存中, 再由其他的线程读取这里的内容, 但是修改这个配置内容往往只有一个线程来修改, 使用某个命令让服务器重新加载配置就可以使用写时拷贝了.

多线程环境使用队列

多线程环境使用哈希表(重)

HashMap 本身不是线程安全的

在多线程环境下使用哈希表可以使用: 

1> Hashtable

2> ConcurrentHashMap

Hashtable 保证线程安全主要是给关键方法加上 synchronized , 直接加到 方法上的, 所以只要有两个线程操作同一个 Hashtable 就会出现 锁冲突 

但是, 实际上对于哈希表来说, 所不一定非得这么加, 有些情况其实不涉及到线程安全问题的.

如果两个线程操作同一个链表比较容易出现问题, 如果两个线程操作的是不同的链表根本不用加锁, 只有操作的是同一个链表才需要加锁.

ConcurrentHashMap 最核心的改进

1> 把一个全局的大锁改进成了 每个链表独立地一把小锁, 这样大幅度降低了锁冲突的概率. (一个 hash 表中有很多这样的链表, 两个线程恰好同时访问一个链表情况, 本身情况少)

怎么完成的: 把每个链表的头节点作为锁对象, synchronized 可以使用任意对象作为锁对象

2> 充分利用了 CAS 特性, 把一些不必要加锁环节给省略加锁了. 比如: 使用变量记录 hash 表中的元素个数(使用原子操作(CAS) 修改元素个数)

3> ConcurrentHashMap, 还有一个激进的操作, 针对读操作没有加锁. 读和读之间, 读和写之间不会有锁竞争(ConcurrentHashMap编码过程中 避免使用 ++之类的非原子的操作)写和写之间加锁

4> ConcurrentHashMap 针对扩容操作做出了单独的优化.

本身 Hashtable 或者 HashMap 在扩容的时候需要把所有的元素都拷贝了一遍. 如果元素很多, 拷贝就比较耗时

使用了 化整为零的方法. 需要扩容时, 需要搬运, 但是分多次完成.

小结

理解即可.

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值