1、概念介绍

  • 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
  • 线程不安全就是不提供数据访问保护,多线程先后更改数据会产生数据不一致或者数据污染的情况。
  • 一般使用synchronized关键字加锁同步控制,来解决线程不安全问题。

2、线程安全的集合对象

  • ArrayList线程不安全,Vector线程安全;
  • HashMap线程不安全,HashTable线程安全;
  • StringBuilder线程不安全,StringBuffer线程安全。

3、代码测试

  • ArrayList线程不安全:
    在主线程中新建100个子线程,分别向ArrayList中添加100个元素,最后打印ArrayList的size。

    public class Test {
    
    public static void main(String [] args){
    // 用来测试的List
    List<String> data = new ArrayList<>();
    // 用来让主线程等待100个子线程执行完毕
    CountDownLatch countDownLatch = new CountDownLatch(100);
    // 启动100个子线程
    for(int i=0;i<100;i++){
    SampleTask task = new SampleTask(data,countDownLatch);
    Thread thread = new Thread(task);
    thread.start();
    }
    try{
    // 主线程等待所有子线程执行完成,再向下执行
    countDownLatch.await();
    }catch (InterruptedException e){
    e.printStackTrace();
    }
    // List的size
    System.out.println(data.size());
    }
    }
    class SampleTask implements Runnable {
    CountDownLatch countDownLatch;
    List<String> data;
    public SampleTask(List<String> data,CountDownLatch countDownLatch){
    this.data = data;
    this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
    // 每个线程向List中添加100个元素
    for(int i = 0; i < 100; i++)
    {
    data.add("1");
    }
    // 完成一个子线程
    countDownLatch.countDown();
    }
    }

    7次测试输出():

    9998
    10000
    10000
    ArrayIndexOutOfBoundsException
    10000
    9967
    9936
  • Vector线程安全:
    在主线程中新建100个子线程,分别向Vector中添加100个元素,最后打印Vector的size。

    public class Test {
    
    public static void main(String [] args){
    // 用来测试的List
    List<String> data = new Vector<>();
    // 用来让主线程等待100个子线程执行完毕
    CountDownLatch countDownLatch = new CountDownLatch(100);
    // 启动100个子线程
    for(int i=0;i<100;i++){
    SampleTask task = new SampleTask(data,countDownLatch);
    Thread thread = new Thread(task);
    thread.start();
    }
    try{
    // 主线程等待所有子线程执行完成,再向下执行
    countDownLatch.await();
    }catch (InterruptedException e){
    e.printStackTrace();
    }
    // List的size
    System.out.println(data.size());
    }
    }
    class SampleTask implements Runnable {
    CountDownLatch countDownLatch;
    List<String> data;
    public SampleTask(List<String> data,CountDownLatch countDownLatch){
    this.data = data;
    this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
    // 每个线程向List中添加100个元素
    for(int i = 0; i < 100; i++)
    {
    data.add("1");
    }
    // 完成一个子线程
    countDownLatch.countDown();
    }
    }

    7次测试输出():

    10000
    10000
    10000
    10000
    10000
    10000
    10000
  • 使用synchronized关键字来同步ArrayList:

    public class Test {
    
    public static void main(String [] args){
    // 用来测试的List
    List<String> data = new ArrayList<>();
    // 用来让主线程等待100个子线程执行完毕
    CountDownLatch countDownLatch = new CountDownLatch(100);
    // 启动100个子线程
    for(int i=0;i<100;i++){
    SampleTask task = new SampleTask(data,countDownLatch);
    Thread thread = new Thread(task);
    thread.start();
    }
    try{
    // 主线程等待所有子线程执行完成,再向下执行
    countDownLatch.await();
    }catch (InterruptedException e){
    e.printStackTrace();
    }
    // List的size
    System.out.println(data.size());
    }
    }
    class SampleTask implements Runnable {
    CountDownLatch countDownLatch;
    List<String> data;
    public SampleTask(List<String> data,CountDownLatch countDownLatch){
    this.data = data;
    this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
    // 每个线程向List中添加100个元素
    for(int i = 0; i < 100; i++)
    {
    synchronized(data){
    data.add("1");
    }
    }
    // 完成一个子线程
    countDownLatch.countDown();
    }
    }

    7次测试输出():

    10000
    10000
    10000
    10000
    10000
    10000
    10000

3、原因分析

  • ArrayList在添加一个元素的时候,它会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
    在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
    而如果是在多线程情况下,比如有两个线程,线程
    A 先将元素1存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B向此 ArrayList 添加元素2,因为此时
    Size 仍然等于 0
    (注意,我们假设的是添加一个元素是要两个步骤,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加
    Size 的值,结果Size都等于1。
    最后,ArrayList中期望的元素应该有2个,而实际元素是在0位置,造成丢失元素,故Size 等于1。导致“线程不安全”。
    ArrayList源码:

    @Override public boolean add(E object) {
    Object[] a = array;
    int s = size;
    if (s == a.length) {
    Object[] newArray = new Object[s +
    (s < (MIN_CAPACITY_INCREMENT / 2) ?
    MIN_CAPACITY_INCREMENT : s >> 1)];
    System.arraycopy(a, 0, newArray, 0, s);
    array = a = newArray;
    }
    a[s] = object;
    size = s + 1;
    modCount++;
    return true;
    }
  • Vector的所有操作方法都被同步了,既然被同步了,多个线程就不可能同时访问vector中的数据,只能一个一个地访问,所以不会出现数据混乱的情况,所以是线程安全的。
    Vector源码:
    @Override
    public synchronized boolean add(E object) {
    if (elementCount == elementData.length) {
    growByOne();
    }
    elementData[elementCount++] = object;
    modCount++;
    return true;
    }

4、线程安全的集合并不安全

分析以下场景:

synchronized(map){
Object value = map.get(key);
if(value == null)
{
value = new Object();
map.put(key,value);
}
return value;}

由于线程安全的集合对象是基于单个方法的同步,所以即使map是线程安全的,也会产生不同步现象。
在非单个方法的场景下,我们仍然需要使用synchronized加锁才能保证对象的同步。

代码测试:

public class Test {

  public static void main(String [] args){
// 用来测试的List
List<String> data = new Vector<>();
// 用来让主线程等待100个子线程执行完毕
CountDownLatch countDownLatch = new CountDownLatch(100);
// 启动100个子线程
for(int i=0;i<1000;i++){
SampleTask task = new SampleTask(data,countDownLatch);
Thread thread = new Thread(task);
thread.start();
}
try{
// 主线程等待所有子线程执行完成,再向下执行
countDownLatch.await();
}catch (InterruptedException e){
e.printStackTrace();
}
// List的size
System.out.println(data.size());
}
}
class SampleTask implements Runnable {
CountDownLatch countDownLatch;
List<String> data;
public SampleTask(List<String> data,CountDownLatch countDownLatch){
this.data = data;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// 每个线程向List中添加100个元素
int size = data.size();
data.add(size,"1");
// 完成一个子线程
countDownLatch.countDown();
}
}
997
993
995
996
997
998
997

5、总结

    • 如何取舍
      线程安全必须要使用synchronized关键字来同步控制,所以会导致性能的降低
      当不需要线程安全时,可以选择ArrayList,避免方法同步产生的开销;
      多个线程操作同一个对象时,可以选择线程安全的Vector;
    • 线程不安全!=不安全
      有人在使用过程中有一个不正确的观点:我的程序是多线程的,不能使用ArrayList要使用Vector,这样才安全。
      线程不安全并不是多线程环境下就不能使用
      注意线程不安全条件:多线程操作同一个对象。比如上述代码就是在多个线程操作同一个ArrayList对象。
      如果每个线程中new一个ArrayList,而这个ArrayList只在这一个线程中使用,那么是没问题的。
    • 线程‘安全’的集合对象
      较复杂的操作下,线程安全的集合对象也无法保证数据的同步,仍然需要我们来处理。

Java多线程理解:线程安全的集合对象的更多相关文章

  1. java 多线程 线程安全及非线程安全的集合对象

    一.概念: 线程安全:就是当多线程访问时,采用了加锁的机制:即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读取完之后,其他线程才可以使用.防止出现数据不一致或 ...

  2. Java多线程| 01 | 线程概述

    Java多线程| 01 | 线程概述 线程相关概念 进程与线程 进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位.可以把进程简单的理解 ...

  3. 关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇高质量的博文)

    Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文) 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享 ...

  4. JAVA多线程之线程间的通信方式

    (转发) 收藏 记 周日,北京的天阳光明媚,9月,北京的秋格外肃穆透彻,望望窗外的湛蓝的天,心似透过栏杆,沐浴在这透亮清澈的蓝天里,那朵朵白云如同一朵棉絮,心意畅想....思绪外扬, 鱼和熊掌不可兼得 ...

  5. Java多线程与线程池技术

    一.序言 Java多线程编程线程池被广泛使用,甚至成为了标配. 线程池本质是池化技术的应用,和连接池类似,创建连接与关闭连接属于耗时操作,创建线程与销毁线程也属于重操作,为了提高效率,先提前创建好一批 ...

  6. Java多线程之线程其他类

    Java多线程之线程其他类 实际编码中除了前面讲到的常用的类之外,还有几个其他类也有可能用得到,这里来统一整理一下: 1,Callable接口和Future接口 JDK1.5以后提供了上面这2个接口, ...

  7. Java多线程之线程的通信

    Java多线程之线程的通信 在总结多线程通信前先介绍一个概念:锁池.线程因为未拿到锁标记而发生的阻塞不同于前面五个基本状态中的阻塞,称为锁池.每个对象都有自己的锁池的空间,用于放置等待运行的线程.这些 ...

  8. Java多线程之线程的同步

    Java多线程之线程的同步 实际开发中我们也经常提到说线程安全问题,那么什么是线程安全问题呢? 线程不安全就是说在多线程编程中出现了错误情况,由于系统的线程调度具有一定的随机性,当使用多个线程来访问同 ...

  9. Java多线程之线程的控制

    Java多线程之线程的控制 线程中的7 种非常重要的状态:  初始New.可运行Runnable.运行Running.阻塞Blocked.锁池lock_pool.等待队列wait_pool.结束Dea ...

随机推荐

  1. Spring中 @Autowired标签与 @Resource标签 的区别(转)

    spring不但支持自己定义的@Autowired注解,还支持由JSR-250规范定义的几个注解,如:@Resource. @PostConstruct及@PreDestroy. 1. @Autowi ...

  2. 从头认识java-15.3 使用HashSet须要注意的地方

    这一章节我们来讨论一下使用Set的各种实现须要注意的地方. Set接口的经常使用实现类有:HashSet.TreeSet,LinkedHashSet 1.HashSet 大家对于HashSet的印象都 ...

  3. WPF silverlight获取子控件(获取DataTemplate里的子控件)

    public static class VisualTreeExtensions { /// <summary> /// 获取父节点控件 /// </summary> /// ...

  4. Ext4,Ext3的特点和区别

    Linux kernel 自 2.6.28 开始正式支持新的文件系统 Ext4. Ext4 是 Ext3 的改进版,修改了 Ext3 中部分重要的数据结构,而不仅仅像 Ext3 对 Ext2 那样,只 ...

  5. CxImage学习

    官方下载地址是:http://www.xdp.it/cximage/ 打开工程后可以看到下例这些工程: - CxImage - CxImageCrtDll - CxImageMfcDll - dome ...

  6. 时间处理工具类TimeUtil

    转自:https://cnblogs.com/ityouknow/p/5662753.html 功能 Date与String之间的互相转换,以及一些特殊格式的时间字符串处理 代码 /** * 类名:T ...

  7. jqGrid的editrules参数

    原文链接:http://blog.csdn.net/mengtianyalll/article/details/13502841 editrules    editrules是用来设置一些可用于可编辑 ...

  8. Shiro授权认证原理和流程

    先来张图: 这是一张shiro的功能图: Authentication: 身份认证/登录,验证用户是否拥有相应的身份 Authorization: 授权/权限验证,验证某个已认证的用户是否拥有某个权限 ...

  9. 【算法】Bellman-Ford算法(单源最短路径问题)(判断负圈)

    单源最短路问题是固定一个起点,求它到其他所有点的最短路的问题. 算法: 设 d[i]  表示 起点 s 离点 i 的最短距离. [1.初始化]  固定起点s,对所有的点 , 如果 i =  s ,  ...

  10. HTML基础——网站后台显示页面

    1.框架集标签:(作用:将页面进行区域的划分) <frameset rows="" cols=""> <frame src="&qu ...