JavaDriver JavaDriver
首页
  • 基础
  • 并发
  • JVM
  • 设计模式
  • 计算机网络
  • 操作系统
  • 数据结构
  • 算法
  • MYSQL
  • REDIS
  • Netty
  • Kafka
系统设计
非技术
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

YoungAnn

西二旗Java老司机一枚 致力于社会主义添砖Java
首页
  • 基础
  • 并发
  • JVM
  • 设计模式
  • 计算机网络
  • 操作系统
  • 数据结构
  • 算法
  • MYSQL
  • REDIS
  • Netty
  • Kafka
系统设计
非技术
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 基础

  • 并发

    • 线程池是如何实现的?
    • 简述 CAS 原理,什么是 ABA 问题,怎么解决?
    • 简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点
    • Synchronized 与 Lock 相比优缺点分别是什么?
    • 重入锁是如何实现的?
    • volatile 关键字解决了什么问题,它的实现原理是什么?
    • 简述 Java 锁升级的机制
    • 简述 Java AQS 的原理以及使用场景
    • 什么是公平锁?什么是非公平锁?
    • Java 的线程有哪些状态,转换关系是怎么样的?
    • Java 是如何实现线程安全的,哪些数据结构是线程安全的?
      • Java 是如何实现线程安全的,哪些数据结构是线程安全的?
      • synchronized 来实现售票场景
      • Lock 接口来实现售票场景
      • 哪些数据结构是线程安全的?
    • 手写死锁
    • 为什么我们不能直接调用 run() 方法?
    • Java 线程有哪些常用方法?
    • 手写生产者消费者模型
    • ThreadLocal 实现原理是什么?为什么要使用弱引用?
  • JVM

  • 设计模式

  • Java相关
  • 并发
YoungAnn
2022-04-04
目录

Java 是如何实现线程安全的,哪些数据结构是线程安全的?

# Java 是如何实现线程安全的,哪些数据结构是线程安全的?

如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

Java 中实现线程安全的方式有两个:

  • synchronized
  • Lock接口

举例一个售票场景:火车站4个窗口同时售票,共有3张票,不能超卖。

# synchronized 来实现售票场景

public class ThreadSynchronizedSecurity {
 
    static int tickets = 3;
 
    class SellTickets implements Runnable {
        @Override
        public void run() {
            // 同步代码块
            synchronized (this) {
                if (tickets <= 0) {
                    System.out.println(Thread.currentThread().getName() + "--->票已售罄!");
                    return;
                }
                System.out.println(Thread.currentThread().getName() + "--->售出第:  " + tickets + " 张票");
                tickets--;
            }
        }
    }
 
    public static void main(String[] args) {
        SellTickets sell = new ThreadSynchronizedSecurity().new SellTickets();
        Thread thread1 = new Thread(sell, "1号窗口");
        Thread thread2 = new Thread(sell, "2号窗口");
        Thread thread3 = new Thread(sell, "3号窗口");
        Thread thread4 = new Thread(sell, "4号窗口");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

输出:

1号窗口--->售出第: 3 张票
2号窗口--->售出第: 2 张票
3号窗口--->售出第: 3 张票
4号窗口--->票已售罄!
1
2
3
4

# Lock 接口来实现售票场景

package com.my.annotate.thread;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class ThreadLockSecurity {
    static int tickets = 3;
 
    class SellTickets implements Runnable {
        Lock lock = new ReentrantLock();
        @Override
        public void run() {
            // Lock锁机制
            if (tickets > 0) {
                try {
                    lock.lock();
                    if (tickets <= 0) {
                        return;
                    }
                    System.out.println(Thread.currentThread().getName() + "--->售出第:  " + tickets + " 票");
                    tickets--;
                } catch (Exception e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
            if (tickets <= 0) {
                System.out.println(Thread.currentThread().getName() + "--->票已售罄!");
            }
 
        }
    }
 
 
    public static void main(String[] args) {
        SellTickets sell = new ThreadLockSecurity().new SellTickets();
        Thread thread1 = new Thread(sell, "1号窗口");
        Thread thread2 = new Thread(sell, "2号窗口");
        Thread thread3 = new Thread(sell, "3号窗口");
        Thread thread4 = new Thread(sell, "4号窗口");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

输出:

1号窗口--->售出第: 3 张票
2号窗口--->售出第: 2 张票
3号窗口--->售出第: 3 张票
4号窗口--->票已售罄!
1
2
3
4

# 哪些数据结构是线程安全的?

JDK已经为大家准备好了一批好用的线程安全容器类,可以大大减少开发工作量,例如HashTable,ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentLinkedQueue,Vector,StringBuffer等。

  1. HashTable

HashTable实现了Map接口,为此其本身也是一个散列表,它存储的内容是基于key-value的键值对映射。

HashTable中的key、value都不可以为null;具有无序特性;由于其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。

HashTable使用synchronized来修饰方法函数来保证线程安全,但是在多线程运行环境下效率表现非常低下。

因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会粗线阻塞状态。

比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率。

  1. ConcurrentHashMap

我们知道HashMap是线程不安全的,ConcurrentHashMap是HashMap的线程安全版。

但是与HashTable相比,ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。

ConcurrentHashMap允许多个修改操作并发运行,其原因在于使用了锁分段技术:首先讲Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。这样就保证了每一把锁只是用于锁住一部分数据,那么当多线程访问Map里的不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率。

上述的处理机制明显区别于HashTable是给整体数据分配了一把锁的处理方法。

为此,在多线程环境下,常用ConcurrentHashMap在需要保证数据安全的场景中去替换HashMap,而不会去使用HashTable,同时在最新版的JDK中已经推荐废弃使用HashTable。

  1. CopyOnWriteArrayList

CopyOnWriteArrayList实现了List接口,提供的数据更新操作都使用了ReentrantLock的lock()方法来加锁,unlock()方法来解锁。

当增加元素的时候,首先使用Arrays.copyOf()来拷贝形成新的副本,在副本上增加元素,然后改变原引用指向副本。读操作不需要加锁,而写操作类实现中对其进行了加锁。因此,CopyOnWriteArrayList类是一个线程安全的List接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合(注意: 如上述更新操作会带来较大的空间与性能开销,如果更新操太过频繁,反而不太合适使用)。

  1. CopyOnWriteArraySet

CopyOnWriteArraySet是对CopyOnWriteArrayList使用了装饰模式后的具体实现。所以CopyOnWriteArrayList的实现机理适用于CopyOnWriteArraySet,此处不再赘述。

Java里的List和Set的之间的特性比较结论同样适用于CopyOnWriteArrayList与CopyOnWriteArraySet之间的比较;此外,CopyOnWriteArrayList与CopyOnWriteArraySet都是线程安全的。

  1. ConcurrentLinkedQueue

ConcurrentLinkedQueue可以被看作是一个线程安全的LinkedList,使用了非阻塞算法实现的一个高效、线程安全的并发队列;其本质是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素时会添加到队列的尾部;当获取一个元素时,会返回队列头部的元素。

ConcurrentLinkedQueue应该算是在高并发环境中性能最好的队列,没有之一。

  1. Vector

Vector通过数组保存数据,继承了Abstract,实现了List;所以,其本质上是一个队列。

但是和ArrayList不同,Vector中的操作是线程安全的,它是利用synchronized同步锁机制进行实现,其实现方式与HashTable类似。

  1. StringBuffer与StringBuilder

在Java里面,字符串操作应该是最频繁的操作了,为此有必要把StringBuffer与StringBuilder两个方法类比较一下。

首先,对于频繁的字符串拼接操作,是不推荐采用效率低下的“+”操作的。一般是采用StringBuffer与StringBuilder来实现上述功能。但是,这两者也是有区别的:前者线程安全,后者不是线程安全的。

StringBuffer是通过对方法函数进行synchronized修饰实现其线程安全特性,实现方式与HashTable、Vector类似。

编辑 (opens new window)
上次更新: 2022/05/19, 21:26:01
Java 的线程有哪些状态,转换关系是怎么样的?
手写死锁

← Java 的线程有哪些状态,转换关系是怎么样的? 手写死锁→

最近更新
01
电商-商品系统设计
12-17
02
关于如何写OKR
12-09
03
对事不对人 vs 对人不对事
12-09
更多文章>
Theme by Vdoing | Copyright © 2022-2023 YoungAnnn | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式