如何保证集合是线程安全的?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何保证集合是线程安全的?相关的知识,希望对你有一定的参考价值。

并发(concurrency)一个并不陌生的词,简单来说,就是cpu在同一时刻执行多个任务。
而Java并发则由多线程实现的。
在jvm的世界里,线程就像不相干的平行空间,串行在虚拟机中。(当然这是比较笼统的说法,线程之间是可以交互的,他们也不一定是串行。)
多线程的存在就是压榨cpu,提高程序性能,还能减少一定的设计复杂度(用现实的时间思维设计程序)。
这么说来似乎线程就是传说中的银弹了,可事实告诉我们真正的银弹并不存在。
多线程会引出很多难以避免的问题, 如死锁,脏数据,线程管理的额外开销,等等。更大大增加了程序设计的复杂度。
但他的优点依旧不可替代。

死锁和脏数据就是典型的线程安全问题。
简单来说,线程安全就是: 在多线程环境中,能永远保证程序的正确性。
只有存在共享数据时才需要考虑线程安全问题。
java内存区域:

其中, 方法区和堆就是主要的线程共享区域。那么就是说共享对象只可能是类的属性域或静态域。

了解了线程安全问题的一些基本概念后, 我们就来说说如何解决线程安全问题。我们来从一个简单的servlet示例来分析:public class ReqCounterServlet extends HttpServlet private int count = 0;
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException
count++;
System.out.print("当前已达到的请求数为" + count);

public void doPost(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException // ignore
1. 了解业务场景的线程模型
这里的线程模型指的是: 在该业务场景下, 可能出现的线程调用实况。
众所周知,Servlet是被设计为单实例,在请求进入tomcat后,由Connector建立连接,再讲请求分发给内部线程池中的Processor,
此时Servlet就处于一个多线程环境。即如果存在几个请求同时访问某个servlet,就可能会有几个线程同时访问该servlet对象。如图:

线程模型,如果简单的话,就在脑海模拟一下就好了,复杂的话就可以用纸笔或其他工具画出来。

2. 找出共享对象
这里的共享对象就很明显就是ReqCounterServlet。

3. 分析共享对象的不变性条件
不变性条件,这个名词是在契约式编程的概念中的。不变性条件保证类的状态在任何功能被执行后都保持在一个可接受的状态。
这里可以引申出, 不可变对象是线程安全的。(因为不可变对象就没有不变性条件)
不变性条件则主要由对可变状态的修改与访问构成。
这里的servlet很简单, 不变性条件大致可以归纳为: 每次请求进入时count计数必须加一,且计数必须正确。
在复杂的业务中, 类的不变性条件往往很难考虑周全。设计的世界是险恶的,只能小心谨慎,用测量去证明,最大程度地减少错误出现的几率。

4. 用特定的策略解决线程安全问题。
如何解决的确是该流程的重点。目前分三种方式解决:
第一种,修改线程模型。即不在线程之间共享该状态变量。一般这个改动比较大,需要量力而行。
第二种,将对象变为不可变对象。有时候实现不了。
第三种,就比较通用了,在访问状态变量时使用同步。 synchronized和Lock都可以实现同步。简单点说,就是在你修改或访问可变状态时加锁,独占对象,让其他线程进不来。
这也算是一种线程隔离的办法。(这种方式也有不少缺点,比如说死锁,性能问题等等)

其实有一种更好的办法,就是设计线程安全类。《代码大全》就有提过,问题解决得越早,花费的代价就越小。
是的,在设计时,就考虑线程安全问题会容易的多。
首先考虑该类是否会存在于多线程环境。如果不是,则不考虑线程安全。
然后考虑该类是否能设计为不可变对象,或者事实不可变对象。如果是,则不考虑线程安全
最后,根据流程来设计线程安全类。
设计线程安全类流程:
1、找出构成对象状态的所有变量。
2、找出约束状态变量的不变性条件。
3、建立对象状态的并发访问管理策略。

有两种常用的并发访问管理策略:
1、java监视器模式。 一直使用某一对象的锁来保护某状态。
2、线程安全委托。  将类的线程安全性委托给某个或多个线程安全的状态变量。(注意多个时,这些变量必须是彼此独立,且不存在相关联的不变性条件。)
参考技术A

    1、不可变 在java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。如final关键字修饰的数据不可修改,可靠性最高。 2、绝对线程安全 绝对的线程安全完全满足Brian GoetZ给...

    2021-11-14 回答者: 爱笑的来往春秋 20个回答

    Java集合中哪些类是线程安全的?

    答:线程安全类 在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的。在jdk1.2之后,就出现许许多多非线程安全的类。 下面是这些线程安全的同步的类: vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建...

    2020-01-03 回答者: 粥小姐啊 2个回答 4

    java线程安全的集合不能保证线程安全?

    问:List<String> product = Collections.synchronizedList(new ArrayList<S...

    答:理论上来说你的product list应该是线程安全的,你把你的代码贴出来看看呗,看看你是怎么操作product,又是如何出现数据不一致的

    2012-07-09 回答者: cdreamer1 4个回答

    Java中如何保证线程安全性

    答:并发(concurrency)一个并不陌生的词,简单来说,就是cpu在同一时刻执行多个任务。 而Java并发则由多线程实现的。 在jvm的世界里,线程就像不相干的平行空间,串行在虚拟机中。(当然这是比较笼统的说法,线程之间是可以交互的,他们也不一定是...

    2020-05-13 回答者: 爱喝可乐小兔砸 3个回答

    Java中的类如何保证线程安全

    答:java中,线程安全的解决方法或过程: 1.如果对象是immutable,则是线程安全的,例如:String,可以放心使用。 2. 如果对象是线程安全的,则放心使用。 3.有条件线程安全,对于Vector和Hashtable一般情况下是线程安全的,但是对于某些特殊情况

参考技术B 在传统的集合框架中,如何解决线程安全问题。 当然,除了Hashtable等同步容器,我们可以使用同步包装器创建一个线程安全的容器。但是这种方式用的是非常粗的同步方式,在高并发情况下,性能比较低下。 具体的位置...
2.重头戏首选的肯定还是我们的Java并发包啊 具体位置如下: 下面楼主也写了一些示范如何使用的简单代码: package com...
3.关于Java8以后的ConcurrentHashMap的一点思考。

在多线程中如何保证集合的安全

线程和进程

进程(Process)的概念。狭义的进程是正在运行的程序的实例;广义的进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是操作系统动态执行的基本单元。

线程(Thread),有时被称为轻量级进程(LWP),是程序执行流的最小单位;一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。

通常情况下,一个进程由一个到多个线程组成,各个线程之间共享程序的内存空间及一些进程级的资源。

在大多数软件应用中,线程的数量都不止一个,多线程程序处在一个多变的环境中,可访问的全局变量和堆数据随时都可能被其他的线程改变,这就将“线程安全”的问题提上了议程。那么,如何确保线程的安全?

 

确保线程安全的方法

一般说来,基于Linux操作系统,确保线程安全的方法有这几个:竞争与原子操作、同步与锁、可重入、过度优化

 

竞争与原子操作
多个线程同时访问和修改一个数据,可能造成很严重的后果。出现严重后果的原因是很多操作被操作系统编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断了而去执行别的代码了。一般将单指令的操作称为原子的(Atomic),因为不管怎样,单条指令的执行是不会被打断的。

 

因此,为了避免出现多线程操作数据的出现异常,Linux系统提供了一些常用操作的原子指令,确保了线程的安全。但是,它们只适用于比较简单的场合,在复杂的情况下就要选用其他的方法了。

 

同步与锁
为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。

 

同步的最常用的方法是使用(Lock),它是一种非强制机制,每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。

 

二元信号量是最简单的一种锁,它只有两种状态:占用与非占用,它适合只能被唯一一个线程独占访问的资源。对于允许多个线程并发访问的资源,要使用多元信号量(简称信号量)。

 

可重入
一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。

 

过度优化
在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可能对代码进行过度的优化以确保线程安全。

 

我们可以使用volatile关键字试图阻止过度优化,它可以做两件事:第一,阻止编译器为了提高速度将一个变量缓存到寄存器而不写回;第二,阻止编译器调整操作volatile变量的指令顺序。

 

在另一种情况下,CPU的乱序执行让多线程安全保障的努力变得很困难,通常的解决办法是调用CPU提供的一条常被称作barrier的指令,它会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。

 

在循环中修改集合数据的问题

看代码:

List asList = Arrays.asList(1,2);
  for(int i:asList){
  asList.add(3);
  System.err.println(i);
}

我们执行这段代码会抛出这个异常 java.lang.UnsupportedOperationException,这个异常告诉我们,不支持在迭代中修改当前的集合(增加或删除元素)

为什么会这样?那我们应该如果避免这个问题呢?

1、foreach循环中是不支持对集合中的元素删除或添加的。

2、使用Arrays.asList产生的集合不支持在循环中更改;使用new ArrayList是可以的;但是我们不应该使用这种写法;因为元素的添加会移除会导致集合的长度发生变化;这极容易导致BUG;并且不易排查。

3、如果确实需要在循环中移除元素; 可以考虑使用迭代器进行操作Iterator

但是如果我们在一个多线程的程序中使用了一个共享的集合;我们怎么能保证其它线和不更改集合中的元素?
  可以使用Collections.synchronizedList获取一个线程安全的集合,要想在循环集合的时候保证线程的安全必须在循环的外层为所需要循环或迭代的集合添加锁控制;即获取当前集合持有的锁进行访问控制;
代码:
final static List asList = Collections.synchronizedList(new ArrayList(Arrays.asList("a","b")));
public static void main(String[] args) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
          Thread.sleep(150);
          }
       catch (
          InterruptedException e) { } reload(); } }).start();
synchronized (asList) { for(String e:asList){ try {Thread.sleep(100);}     
       catch (InterruptedException e1) {}    System.out.println(e); } } } public static void reload(){ asList.clear(); asList.addAll(Arrays.asList("a","b","c")); }

注意:如果不使用Collections.synchronizedList获取一个线程安全的集合;就必须在修改集合之前首先获取集合的锁进行同步控制;或者可以使用java.util.concurrent包下的相关线程安全的集合类。

 

以上是关于如何保证集合是线程安全的?的主要内容,如果未能解决你的问题,请参考以下文章

Java -- 每日一问:如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?

如何创建线程?如何保证线程安全?

Java -- 每日一问:如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?

如何创建线程?如何保证线程安全?

在多线程中如何保证集合的安全

如何创建线程?如何保证线程安全?