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();
}
}
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号窗口--->票已售罄!
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();
}
}
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号窗口--->票已售罄!
2
3
4
# 哪些数据结构是线程安全的?
JDK已经为大家准备好了一批好用的线程安全容器类,可以大大减少开发工作量,例如HashTable,ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentLinkedQueue,Vector,StringBuffer等。
- HashTable
HashTable实现了Map接口,为此其本身也是一个散列表,它存储的内容是基于key-value的键值对映射。
HashTable中的key、value都不可以为null;具有无序特性;由于其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。
HashTable使用synchronized来修饰方法函数来保证线程安全,但是在多线程运行环境下效率表现非常低下。
因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会粗线阻塞状态。
比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率。
- ConcurrentHashMap
我们知道HashMap是线程不安全的,ConcurrentHashMap是HashMap的线程安全版。
但是与HashTable相比,ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。
ConcurrentHashMap允许多个修改操作并发运行,其原因在于使用了锁分段技术:首先讲Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。这样就保证了每一把锁只是用于锁住一部分数据,那么当多线程访问Map里的不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率。
上述的处理机制明显区别于HashTable是给整体数据分配了一把锁的处理方法。
为此,在多线程环境下,常用ConcurrentHashMap在需要保证数据安全的场景中去替换HashMap,而不会去使用HashTable,同时在最新版的JDK中已经推荐废弃使用HashTable。
- CopyOnWriteArrayList
CopyOnWriteArrayList实现了List接口,提供的数据更新操作都使用了ReentrantLock的lock()方法来加锁,unlock()方法来解锁。
当增加元素的时候,首先使用Arrays.copyOf()来拷贝形成新的副本,在副本上增加元素,然后改变原引用指向副本。读操作不需要加锁,而写操作类实现中对其进行了加锁。因此,CopyOnWriteArrayList类是一个线程安全的List接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合(注意: 如上述更新操作会带来较大的空间与性能开销,如果更新操太过频繁,反而不太合适使用)。
- CopyOnWriteArraySet
CopyOnWriteArraySet是对CopyOnWriteArrayList使用了装饰模式后的具体实现。所以CopyOnWriteArrayList的实现机理适用于CopyOnWriteArraySet,此处不再赘述。
Java里的List和Set的之间的特性比较结论同样适用于CopyOnWriteArrayList与CopyOnWriteArraySet之间的比较;此外,CopyOnWriteArrayList与CopyOnWriteArraySet都是线程安全的。
- ConcurrentLinkedQueue
ConcurrentLinkedQueue可以被看作是一个线程安全的LinkedList,使用了非阻塞算法实现的一个高效、线程安全的并发队列;其本质是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素时会添加到队列的尾部;当获取一个元素时,会返回队列头部的元素。
ConcurrentLinkedQueue应该算是在高并发环境中性能最好的队列,没有之一。
- Vector
Vector通过数组保存数据,继承了Abstract,实现了List;所以,其本质上是一个队列。
但是和ArrayList不同,Vector中的操作是线程安全的,它是利用synchronized同步锁机制进行实现,其实现方式与HashTable类似。
- StringBuffer与StringBuilder
在Java里面,字符串操作应该是最频繁的操作了,为此有必要把StringBuffer与StringBuilder两个方法类比较一下。
首先,对于频繁的字符串拼接操作,是不推荐采用效率低下的“+”操作的。一般是采用StringBuffer与StringBuilder来实现上述功能。但是,这两者也是有区别的:前者线程安全,后者不是线程安全的。
StringBuffer是通过对方法函数进行synchronized修饰实现其线程安全特性,实现方式与HashTable、Vector类似。