JAVA多线程——(二)多线程编程
文章目录
- JAVA多线程——(二)多线程编程
- 【一】ReentrantLock
- 【二】ReadWriteLock
- 【三】Condition
- 【四】并发容器
- 【五】Atomic
- 【六】ExecutorService
- 【七】CountDownLatch
- 【八】CyclicBarrier
- 【九】Volatile
- 【十】ThreadLocal
【一】ReentrantLock
虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
- lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
- tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待
- tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
- lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
- 我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:
ReentrantLock lock = new ReentrantLock(true);
public class Main {
Lock lock = new ReentrantLock();
AtomicInteger integer = new AtomicInteger(1);
public void print(){
//使用ReentrantLock加锁的时候,必须在finally中释放锁,不然可能造成死锁
lock.lock();
try {
System.out.println(integer.get());
integer.getAndIncrement();
}finally {
lock.unlock();
}
}
public static void main(String args[]){
Main main = new Main();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.print();
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.print();
}
});
thread1.start();
thread2.start();
}
}
【二】ReadWriteLock
读写锁是一种通用技术,在其他编程语言或者数据库中都有对应实现。读写锁一般遵守下面三条规则:
- 允许多个线程获取读锁
- 只允许一个线程获取写锁
- 如果某个线程获取了写锁,其他线程不能再获取读锁
由于规则1多个线程在只读的情况下可以同时读取数据获取共享变量,所以读写锁优于互斥锁。
读写锁适用于读多写少:
ReadWriteLock接口的两个方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
例子:
public class Main {
ReadWriteLock lock = new ReentrantReadWriteLock();
AtomicInteger count = new AtomicInteger(1);
public void get(){
//获取数据时,获取读锁
Lock readLock = this.lock.readLock();
readLock.lock();
try {
System.out.println(count);
}finally {
readLock.unlock();
}
}
public void add(){
//写的时候,获取写锁
Lock writeLock = this.lock.writeLock();
writeLock.lock();
try {
count.getAndIncrement();
}finally {
writeLock.unlock();
}
}
public static void main(String args[]){
Main main = new Main();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.get();
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.add();
}
});
thread1.start();
thread2.start();
}
}
【三】Condition
在java Lock体系下依然会有同样的方法实现等待/通知机制,而Condition与Lock配合完成等待通知机制。
- Condition能够支持不响应中断,而通过使用Object方式不支持;
- Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
- Condition能够支持超时时间的设置,而Object不支持
await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
signal()All :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。
例子:
public class Main {
public Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public Stack<Integer> stack = new Stack();
public int i = 1;
public void producer(){
lock.lock();
try {
while (stack.isEmpty()){
stack.push(new Integer(i++));
condition.signalAll();
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consumer(){
lock.lock();
try {
while (!stack.isEmpty()){
System.out.println(stack.pop());
condition.signalAll();
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String args[]){
Main main = new Main();
Thread thread1 = new Thread(()->{
main.producer();
});
Thread thread2 = new Thread(()->{
main.consumer();
});
thread1.start();
thread2.start();
}
}
【四】并发容器
- ConcurrentHashMap
主要为了解决HashMap线程不安全和Hashtable效率不高的问题。
1、JDK7版本
分段锁机制,简而言之,ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable
ConcurrentHashMap的数据结构:
ConcurrentHashMap类结构如上图所示。由图可知,在ConcurrentHashMap中,定义了一个Segment<K, V>[]数组来将Hash表实现分段存储,从而实现分段加锁;而么一个Segment元素则与HashMap结构类似,其包含了一个HashEntry数组,用来存储Key/Value对。Segment继承了ReetrantLock,表示Segment是一个可重入锁,因此ConcurrentHashMap通过可重入锁对每个分段进行加锁
2、JDK8版本
在JDK1.8中,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现
CAS(Compare And Swap,比较交换):CAS有三个操作数,内存值V、预期值A、要修改的新值B,当且仅当A和V相等时才会将V修改为B,否则什么都不做。
JDK1.8的数据结构:
public class Main {
private ConcurrentHashMap<String,Character> map = new ConcurrentHashMap<>();
public void producer(String key,char value){
map.put(key,value);
}
public void consumer(String key){
Character value = map.get(key);
System.out.println(value);
}
public static void main(String args[]){
Main main = new Main();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.producer(String.valueOf(i), (char) ('a'+i));
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.consumer(String.valueOf(i));
}
});
thread1.start();
thread2.start();
}
}
- ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个基于链表的无界非阻塞队列,并且是线程安全的,它采用的是先进先出的规则,当我们增加一个元素时,它会添加到队列的末尾,当我们取一个元素时,它会返回一个队列头部的元素。
offer():将指定的元素插入队列的尾部
poll() :获取并移除队列的头,如果队列为空则返回null
peek():获取表头元素但不移除队列的头,如果队列为空则返回null。
remove(Object obj):移除队列已存在的元素,返回true,
如果元素不存在,返回false。
add(E e):将指定元素插入队列末尾,成功返回true,
失败返回false(此方法非线程安全的方法,不推荐使用)。
注意:
虽然ConcurrentLinkedQueue的性能很好,
但是在调用size()方法的时候,会遍历一遍集合
对性能损害较大,执行很慢,因此应该尽量的减少使用这个方法,
如果判断是否为空,最好用isEmpty()方法。
ConcurrentLinkedQueue不允许插入null元素,会抛出空指针异常。
ConcurrentLinkedQueue是无界的,所以使用时,
一定要注意内存溢出的问题。即对并发不是很大中等的情况下使用,
不然占用内存过多或者溢出,对程序的性能影响很大,甚至是致命的。
- CopyOnWriteArrayList
1、实现了List接口
2、内部持有一个ReentrantLock lock = new ReentrantLock();
3、底层是用volatile transient声明的数组 array
4、读写分离,写时复制出一个新的数组,完成插入、
修改或者移除操作后将新数组赋值给array
性能上:
Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降。
而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况
- CopyOnWriteArraySet
- BlockingQueue
【五】Atomic
- 线程安全的几个问题:
1、原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作
2、可见性:一个线程对主内存的修改可以及时的被其他线程观察到
3、有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序 - 原子更新基本类型
AtomicBoolean: 原子更新布尔类型。
AtomicInteger: 原子更新整型。
AtomicLong: 原子更新长整型。
- 原子更新数组
AtomicIntegerArray: 原子更新整型数组里的元素。
AtomicLongArray: 原子更新长整型数组里的元素。
AtomicReferenceArray: 原子更新引用类型数组里的元素。
- 原子更新引用类型
AtomicReference: 原子更新引用类型。
AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
AtomicMarkableReferce: 原子更新带有标记位的引用类型
- 原子更新字段类
AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。
【六】ExecutorService
- 一、线程池: 提供一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁的额外开销,提高了响应的速度。
- 二、线程池的体系结构:
java.util.concurrent.Executor 负责线程的使用和调度的根接口
|–ExecutorService 子接口: 线程池的主要接口
|–ThreadPoolExecutor 线程池的实现类
|–ScheduledExceutorService 子接口: 负责线程的调度
|–ScheduledThreadPoolExecutor : 继承ThreadPoolExecutor,实现了ScheduledExecutorService - 三、工具类 : Executors
1、ExecutorService newFixedThreadPool() : 创建固定大小的线程池
2、ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
3、ExecutorService newSingleThreadExecutor() : 创建单个线程池。 线程池中只有一个线程
4、ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务
【七】CountDownLatch
CountDownLatch,英文翻译为倒计时锁存器,是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
1、确保某个计算在其需要的所有资源都被初始化之后才继续执行;
2、确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
3、等待直到某个操作所有参与者都准备就绪再继续执行。
用法:
- 第一种:
某一线程在开始运行前等待n个线程执行完毕。
将 CountDownLatch 的计数器初始化为n :new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 - 第二种:
实现多个线程开始执行任务的最大并行性。
注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。
例子:计算多线程耗时
public class TestCountDownLatch {
public static void main(String[] args){
//CountDownLatch 为唯一的、共享的资源
final CountDownLatch latch = new CountDownLatch(5);
LatchDemo latchDemo = new LatchDemo(latch);
long begin = System.currentTimeMillis();
for (int i = 0; i <5 ; i++) {
new Thread(latchDemo).start();
}
try {
//多线程运行结束前一直等待
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("耗费时间:"+(end-begin));
}
}
class LatchDemo implements Runnable{
private CountDownLatch latch;
public LatchDemo(CountDownLatch latch){
this.latch=latch;
}
public LatchDemo(){
super();
}
@Override
public void run() {
//当前对象唯一,使用当前对象加锁,避免多线程问题
synchronized (this){
try {
for (int i = 0; i < 50000; i++) {
if (i%2==0){
System.out.println(i);
}
}
}finally {
//保证肯定执行
latch.countDown();
}
}
}
}
【八】CyclicBarrier
CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。
CyclicBarrier内部使用了ReentrantLock和Condition两个类。
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程使用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier的另一个构造函数CyclicBarrier(int parties, Runnable barrierAction),用于线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。
public class CyclicBarrierTest {
// 自定义工作线程
private static class Worker extends Thread {
private CyclicBarrier cyclicBarrier;
public Worker(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
super.run();
try {
System.out.println(Thread.currentThread().getName() + "开始等待其他线程");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
// 工作线程开始处理,这里用Thread.sleep()来模拟业务处理
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "执行完毕");
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
for (int i = 0; i < threadCount; i++) {
System.out.println("创建工作线程" + i);
Worker worker = new Worker(cyclicBarrier);
worker.start();
}
}
}
【九】Volatile
【十】ThreadLocal