ReentrantLock
ReentrantLock 和 synchronized 一样,也是一个可重入锁。
- ReentrantLock 使用方法
import java.util.concurrent.locks.ReentrantLock;
/**
两个线程操作一个数
*/
public class MyReentrantLock {
static int count;
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread() {
@Override
public void run() {
reentrantLock.lock();
for (int i = 0; i < 100000; i++) {
count++;
}
reentrantLock.unlock();
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
reentrantLock.lock();
for (int i = 0; i < 100000; i++) {
count++;
}
reentrantLock.unlock();
}
};
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
- ReenTrantLock 是 JDK(java 源码包) 实现的,而 synchronized 是关键字,通过 JVM 底层源码(c++) 实现;
- ReenTrantLock 自己实现加锁(lock)和解锁(unlock),而 synchronized 是全套服务,加锁解锁自己实现;
- ReenTrantLock 中有 tryLock ,当线程阻塞后,可以手动实现等待时间,或者直接返回(tryLock() 方法中可以设置等待时间,或者不设置,因此可以避免进入内核态的阻塞状态,线程进入内核态就是不可控的,往往进入内核态意味着效率更低。所以设计锁就是避免线程进入内核态)(为什么有 synchronized 还要有 ReenTrantLock 的原因);
semaphore
semaphore(信号量),简单点来理解,信号量就是一个计数器,表示可用资源的个数。
怎么来理解这个 计数器 呢?
类似于停车场入口显示的车位剩余量,当一辆车离开停车场,那么车位剩余量+1,如果一辆车进入停车场,那么车位剩余量-1,如果车位剩余量为 0 ,那么车不能挺进去。
semaphore 也是这样,涉及到两个操作
P :申请一个可用资源(可用资源-1,也就是剩余车位数量-1)
V :释放一个可用资源(可用资源+1,也就是剩余车位数量+1)
如果信号量为 0 ,也就是可用资源个数是 0,那么就会阻塞,直到其他线程释放资源。
注意:而 信号量 + 1 或者 - 1 操作都是 原子性 的。
基于 信号量 的理解,因此可以设计一个锁
- 信号量只有 0 和 1;
- 如果一个线程进行 P 操作,那么信号量 -1,此时如果其他线程想使用此资源,需要阻塞等待;
- 如果线程释放了资源,那么信号量 +1,此时阻塞线程可以申请资源。
也可以实现一个 阻塞队列
需要三个 semaphore,一个代表锁,一个代表资源的个数,一个代表空位的个数;
CountDownLatch
类似于一个计数器,但是有不同,举个例子。
比如下载一个游戏,需要多个线程下载。那么线程 1 下载好了当前的文件,而其他线程还没下载好,因此并不算完全下载成功,等待所有线程都下载成功后,才算真的下载完成。
原子类

原子类能够保证操作数据时 CPU 的指令是原子的。
- 示例
import java.util.concurrent.atomic.AtomicInteger;
public class MyAtomic {
static AtomicInteger atomicInteger = new AtomicInteger();
static int num = 0;
/**
* 执行 5 次自增对比结果
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 5; i++) {
doIncrement();
System.out.println("atomicInteger"+i + "= " + atomicInteger);
System.out.println("num"+i + "= " + num);
System.out.println("--------分割线---------");
atomicInteger = new AtomicInteger();
num = 0;
}
}
/**
* 创建两个线程对一个数进行修改
* @throws InterruptedException
*/
public static void doIncrement() throws InterruptedException {
Thread thread1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
atomicInteger.incrementAndGet();
num++;
}
}
};
thread1.start();
Thread thread2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
atomicInteger.incrementAndGet();
num++;
}
}
};
thread2.start();
thread1.join();
thread2.join();
}
}
- 结果
atomicInteger1= 100001
num1= 63105
--------分割线---------
atomicInteger2= 100001
num2= 91873
--------分割线---------
atomicInteger3= 100001
num3= 91147
--------分割线---------
atomicInteger4= 100001
num4= 92904
--------分割线---------
atomicInteger5= 100001
num5= 88765
由结果得知,原子类 AtomicInteger 是线程安全的
线程安全的集合
平时刷题用的一些集合都是线程不安全的,例如
- ArrayList
- LinkedList
- HashSet
- HashMap
当然也有安全的集合,例如
5. Stack
6. Vector
7. HashTable(目前没用到过)
针对线程不安全的集合,在 Java.util.concurrent 这个包中包含了许多线程安全的集合(标记了部分)

当然,也可以自己实现线程安全的集合,那就是通过加 synchronized 关键字。而 HashTable 就是使用了这个方法,保证线程安全的。
而为什么有了 HashTable 还要有 concurrentHashMap ?(HashTable 和 concurrentHashMap 的区别)
直接加 synchronized 关键字必然会导致效率很低,而 concurrentHashMap 是通过什么方式保证线程安全呢?
- 每个链表/红黑树加锁的方式来保证线程安全(这种分法是 jdk1.8之后的分法,而之前是几个链表/红黑树加一个锁),也就是说,如果线程之间修改的不是同一个数据,那么就不会导致线程不安全。
- 针对读操作,不加锁,虽然可能会导致一个线程正在修改数据的时候,另外一个线程刚好读这个数据,导致线程不安全。当然可以用读写锁来进行优化。
- 内部广泛使用 CAS 操作来提高效率,例如获取元素个数的时候,没加锁,直接 CAS;
- 针对扩容进行了优化,假如某一个线程触发了扩容,那么当其他线程进行操作的时候,就会一起进行扩容。而 HashTable 则是触发扩容的线程单独进行扩容。
浙公网安备 33010602011771号