Hi!欢迎光临陕西省的权威思科、华为、Oracle、红帽、深信服、微软认证培训中心!
| 029-88235527
您现在所在位置:首页 > 新闻资讯 > 行业新闻 >

Java编程并发下集合类

发布日期:2020-08-16 17:19:55点击次数:

分享到:
Java编程并发集合类主要有:
ConcurrentHashMap:支持多线程的分段哈希表,它通过将整个哈希表分成多段的方式减小锁粒度。
ConcurrentSkipListMap:ConcurrentSkipListMap的底层是通过跳表来实现的。跳表是一个链表,但是通过使用“跳跃式”查找的方式使得插入、读取数据时复杂度变成了O(logn)。
ConCurrentSkipListSet:参考 ConcurrentSkipListMap。
CopyOnWriteArrayList:是 ArrayList 的一个线程安全的变形,其中所有可变操作(添加、设置,等等)都是通过对基础数组进行一次新的复制来实现的。
CopyOnWriteArraySet:参考 CopyOnWriteArrayList。
ConcurrentLinkedQueue:cas 实现的非阻塞并发队列。
线程池
介绍
多线程的设计优点是能很大限度的发挥多核处理器的计算能力,但是,若不控制好线程资源反而会拖累cpu,降低系统性能,这就涉及到了线程的回收复用等一系列问题;而且本身线程的创建和销毁也很耗费资源,因此找到一个合适的方法来提高线程的复用就很必要了。
线程池就是解决这类问题的一个很好的方法:线程池中本身有很多个线程,当需要使用线程的时候拿一个线程出来,当用完则还回去,而不是每次都创建和销毁。在 JDK 中提供了一套 Executor 线程池框架,帮助开发人员有效的进行线程控制。
Executor 使用
获得线程池的方法:
newFixedThreadPool(int nThreads) :创建固定数目线程的线程池。
newCachedThreadPool:创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线 程并添加到池中。
newSingleThreadExecutor:创建一个单线程化的 Executor。
newScheduledThreadPool:创建一个支持定时及周期性的任务执行的线程池。
以上方法都是返回一个 ExecutorService 对象,executorService.execute() 传入一个 Runnable 对象,可执行一个线程任务。
下面看示例代码
public class Test implements Runnable{
 int i=0;
 public Test(int i){
  this.i=i;
 }
 public void run() {
  System.out.println(Thread.currentThread().getName()+"====="+i);
 }
    public static void main(String[] args) throws InterruptedException {
  ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
  for(int i=0;i<10;i++){
   cachedThreadPool.execute(new Test(i));
   Thread.sleep(1000);
  }
 }
}
线程池是一个庞大而复杂的体系,本文定位是基础,不对其做更深入的研究,感兴趣的小伙伴可以自行查资料进行学习。
ScheduledExecutorService
newScheduledThreadPool(int corePoolSize) 会返回一个ScheduledExecutorService 对象,可以根据时间对线程进行调度;其下有三个执行线程任务的方法:schedule(),scheduleAtFixedRate() 以及 scheduleWithFixedDelay() 该线程池可解决定时任务的问题。
示例:
class Test implements Runnable {
    private String testStr;
    Test(String testStr) {
        this.testStr = testStr;
    }
    @Override
    public void run() {
        System.out.println(testStr + " >>>> print");
    }
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
        long wait = 1;
        long period = 1;
        service.scheduleAtFixedRate(new MyScheduledExecutor("job1"), wait, period, TimeUnit.SECONDS);
        service.scheduleWithFixedDelay(new MyScheduledExecutor("job2"), wait, period, TimeUnit.SECONDS);
        scheduledExecutorService.schedule(new MyScheduledExecutor("job3"), wait, TimeUnit.SECONDS);//延时waits 执行
    }
}
job1的执行方式是任务发起后间隔 wait 秒开始执行,每隔 period 秒(注意:不包括上一个线程的执行时间)执行一次;
job2的执行方式是任务发起后间隔 wait 秒开始执行,等线程结束后隔 period 秒开始执行下一个线程;
job3只执行一次,延迟 wait 秒执行;
ScheduledExecutorService 还可以配合 Callable 使用来回调获得线程执行结果,还可以取消队列中的执行任务等操作,这属于比较复杂的用法,我们这里掌握基本的即可,到实际遇到相应的问题时我们在现学现用,节省学习成本。
锁优化
减小锁持有时间
减小锁的持有时间可有效的减少锁的竞争。如果线程持有锁的时间越长,那么锁的竞争程度就会越激烈。因此,应尽可能减少线程对某个锁的占有时间,进而减少线程间互斥的可能。
减少锁持有时间的方法有:
进行条件判断,只对必要的情况进行加锁,而不是整个方法加锁。
减少加锁代码的行数,只对必要的步骤加锁。
减小锁粒度
减小锁的范围,减少锁住的代码行数可减少锁范围,减小共享资源的范围也可减小锁的范围。减小锁共享资源的范围的方式比较常见的有分段锁,比如 ConcurrentHashMap ,它将数据分为了多段,当需要 put 元素的时候,并不是对整个 hashmap 进行加锁,而是先通过 hashcode 来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程 put 的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
锁分离
锁分离最常见的操作就是读写分离了,读写分离的操作参考 ReadWriteLock 章节,而对读写分离进一步的延伸就是锁分离了。为了提高线程的并行量,我们可以针对不同的功能采用不同的锁,而不是统统用同一把锁。比如说有一个同步方法未进行锁分离之前,它只有一把锁,任何线程来了,只有拿到锁才有资格运行,进行锁分离之后就不是这种情形了——来一个线程,先判断一下它要干嘛,然后发一个对应的锁给它,这样就能一定程度上提高线程的并行数。
锁粗化
一般为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,也就是说锁住的代码尽量少。但是如果如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。比如有三个步骤:a、b、c,a同步,b不同步,c同步;那么一个线程来时候会上锁释放锁然后又上锁释放锁。这样反而可能会降低线程的执行效率,这个时候我们将锁粗化可能会更好——执行 a 的时候上锁,执行完 c 再释放锁。
锁扩展
分布式锁
JDK 提供的锁在单体项目中不会有什么问题,但是在集群项目中就会有问题了。在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。JDK 锁显然无法满足我们的需求,于是就有了分布式锁。
分布式锁的实现有三种方式:
基于数据库实现分布式锁
基于缓存(redis,memcached,tair)实现分布式锁
基于 Zookeeper 实现分布式锁
基于redis的分布式锁比较使用普遍,在这里介绍其原理和使用:
redis 实现锁的机制是 setnx 指令,setnx 是原子操作命令,锁存在不能设置值,返回 0 ;锁不存在,则设置锁,返回 1 ,根据返回值来判断上锁是否成功。看到这里你可能想为啥不先 get 有没有值,再 set 上锁;首先我们要知道,redis 是单线程的,同一时刻只可能有一个线程操作内存,然后 setnx 是一个操作步骤(具有原子性),而 get 再 set 是两个步骤(不具有原子性)。如果使用第二种可能会发生这种情况:客户端 a get发现没有锁,这个时候被切换到客户端b,b get也发现没锁,然后b set,这个时候又切换到a客户端 a set;这种情况下,锁完全没起作用。所以,redis分布式锁,原子性是关键。
对于 web 应用中 redis 客户端用的比较多的是 lettuce,jedis,redisson。springboot 的 redis 的 start 包底层是 lettuce ,但对 redis 分布式锁支持得最好的是 redisson(如果用 redisson 你就享受不到 redis 自动化配置的好处了);不过 springboot 的 redisTemplete 支持手写 lua 脚本,我们可以通过手写 lua 脚本来实现 redis 锁。
代码示例:
public boolean lockByLua(String key, String value, Long expiredTime){
    String strExprie = String.valueOf(expiredTime);
    StringBuilder sb = new StringBuilder();
    sb.append("if redis.call(\"setnx\",KEYS[1],ARGV[1])==1 ");
    sb.append("then ");
    sb.append("    redis.call(\"pexpire\",KEYS[1],KEYS[2]) ");
    sb.append("    return 1 ");
    sb.append("else ");
    sb.append("    return 0 ");
    sb.append("end ");
    String script = sb.toString();
    RedisCallback<Boolean> callback = (connection) -> {
        return connection.eval(script.getBytes(), ReturnType.BOOLEAN, 2, key.getBytes(Charset.forName("UTF-8")),strExprie.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")));
    };
    Boolean execute = stringRedisTemplate.execute(callback);
    return execute;
}
关于lua脚本的语法我就不做介绍了。
在 github 上也有开源的 redis 锁项目,比如 spring-boot-klock-starter 感兴趣的小伙伴可以去试用一下。
数据库锁
对于存在多线程问题的项目,比如商品货物的进销存,订单系统单据流转这种,我们可以通过代码上锁来控制并发,也可以使用数据库锁来控制并发,数据库锁从机制上来说分乐观锁和悲观锁。
悲观锁:
悲观锁分为共享锁(S锁)和排他锁(X锁),MySQL 数据库读操作分为三种——快照读,当前读;快照读就是普通的读操作,如:
select *from table
当前读就是对数据库上悲观锁了;其中 select ... lock in share mode 属于共享锁,多个事务对于同一数据可以共享,但只能读不能修改。而下面三种 SQL :
select ...for update
update ... set...
insert into ...
属于排他锁,排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改,排他锁是阻塞锁。
乐观锁:
就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,如果有则更新失败。一种实现方式为在数据库表中加一个版本号字段 version ,任何 update 语句 where 后面都要跟上 version=?,并且每次 update 版本号都加 1。如果 a 线程要修改某条数据,它需要先 select 快照读获得版本号,然后 update ,同时版本号加一。这样就保证了在 a 线程修改某条数据的时候,确保其他线程没有修改过这条数据,一旦其他线程修改过,就会导致 a 线程版本号对不上而更新失败(这其实是一个简化版的mvcc)。
乐观锁适用于允许更新失败的业务场景,悲观锁适用于确保更新操作被执行的场景。

西安鸥鹏是西安当地为数不多获取民办办学许可证的合法机构;西安鸥鹏IT教育多年专注C++、Java、Oracle、 HUAWEI华为、思科Cisco、 Linux、python、信息安全、大数据、云计算网络等IT各个领域。学员可以选择最适合自己的课程,而不会因为培训中心只经营单一课程,而被误导学习了并非适合自己的课程,浪费经济和时间成本,影响自己的职业生涯发展。鸥鹏IT几乎为所有IT巨头权威相关知名IT企业CISCO、MICROSOFT、REDHAT、华为、ORACLE授权培训机构,同时是PROMETRIC和VUE授权的相关考试中心,可以组织学员参加认证考试并获得IT资格认证。