Java 多线程与并发——JUC 包

多线程与并发 专栏收录该内容
8 篇文章 0 订阅

JUC 包(java.util.concurrent)提供了并发编程的解决方案,CAS 是 java.util.concurrent.atomic 包的基础,AQS 是 java.util.concurrent.locks 包以及一些常用类比如 Semophore,ReentrantLock 等类的基础。

JUC 包的分类:

  • executor:线程执行器
  • locks:锁
  • atomic:原子变量类
  • tools:并发工具类
  • collections:并发集合

1.并发工具类

包括倒计时闭锁 CountDownLatch、栅栏 CyclicBarrier、信号量 Semaphore、交换器 Exchanger。

1.CountDownLatch

倒计时闭锁 CountDownLatch 也称为倒计时计数器,可以让主线程等待一组事件发生后继续执行,事件是指 CountDownLatch 里的 countDown() 方法。

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        new CountDownLatchDemo().execute();
    }
    private void execute() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(3); // 初始化count为3
        new Thread(new Task(countDownLatch), "thread1").start();
        new Thread(new Task(countDownLatch), "thread2").start();
        new Thread(new Task(countDownLatch), "thread3").start();
        // 调用await()方法的线程会被挂起, 它会等待直到count值为0才继续执行
        // 带超时参数的await(long timeout, TimeUnit unit)方法超时后不管count是否为0,都会继续执行
        countDownLatch.await();
        System.out.println("所有线程已到达, 主线程开始执行");
    }
    class Task implements Runnable {
        private CountDownLatch countDownLatch;
        private Task(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }
        @Override
        public void run() {
            System.out.println("线程" + Thread.currentThread().getName() + "已经到达");
            countDownLatch.countDown(); // 将count值减1
        }
    }
}

执行结果:

线程thread3已经到达
线程thread1已经到达
线程thread2已经到达
所有线程已到达, 主线程开始执行

2.CyclicBarrier

栅栏 CyclicBarrier 可以阻塞当前线程,等待其他线程,所有线程必须同时到达栅栏位置后,才能继续执行;所有线程到达栅栏处,可以触发执行另一个预先设置的线程。

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        new CyclicBarrierDemo().execute();
    }
    private void execute() {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3); // 初始化栅栏的参与者为3
        new Thread(new Task(cyclicBarrier), "thread1").start();
        new Thread(new Task(cyclicBarrier), "thread2").start();
        new Thread(new Task(cyclicBarrier), "thread3").start();
    }
    class Task implements Runnable {
        private CyclicBarrier cyclicBarrier;
        private Task(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
        @Override
        public void run() {
            System.out.println("线程" + Thread.currentThread().getName() + "已经到达");
            try {
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println("线程" + Thread.currentThread().getName() + "开始处理");
        }
    }
}

执行结果:

线程thread3已经到达
线程thread1已经到达
线程thread2已经到达
线程thread2开始处理
线程thread3开始处理
线程thread1开始处理

3.Semaphore

信号量 Semaphore 可以控制某个资源可被同时访问的线程个数。

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        // 只能5个线程同时访问
        final Semaphore semp = new Semaphore(5);
        for (int index = 0; index < 20; index++) {
            final int no = index;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        // 获取许可
                        semp.acquire();
                        System.out.println("Accessing: " + no);
                        Thread.sleep((long) (Math.random() * 10000));
                        // 访问完后, 释放
                        semp.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            exec.execute(runnable);
        }
        // 退出线程池
        exec.shutdown();
    }
}

只有 5 个客户端可以访问,其他客户端只能等待已经获取许可的 5 个客户端调用 release() 方法释放许可,才能进行访问。

4.Exchanger

交换器 Exchanger 主要用于线程之间数据交换,只能用户两个线程,可以实现两个线程到达同步点后,相互交换数据。先到达同步点的线程会被阻塞,当两个线程都到达同步点后,开始交换数据。

public class ExchangerDemo {
    private static Exchanger<String> exchanger = new Exchanger<String>();
    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(2);
        // 线程一
        exec.execute(() -> {
            try {
                String text = exchanger.exchange("hello1");
                System.out.println("thread1: " + text);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 线程二
        exec.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
                String text = exchanger.exchange("hello2");
                System.out.println("thread2: " + text);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

执行结果:

thread1: hello2
thread2: hello1

#2.并发集合

包括 ConcurrentHashMap、BlockingQueue。

##1.BlockingQueue

BlockingQueue 接口提供了可阻塞的入队和出队操作,如果队列满了,入队操作将阻塞,直到有空间可用;如果队列空了,出队操作将阻塞,直到有元素可用。

BlockingQueue 接口主要用于生产者 - 消费者模式,在多线程场景时生产者线程在队列尾部添加元素,而消费者则在队列头部消费元素,通过这种方式能够达到将任务的生产和消费进行隔离的目的。

以下是 BlockingQueue 常用的实现类,都是线程安全的:

说明
ArrayBlockingQueue一个由数组组成的有界阻塞队列
LinkedBlockingQueue一个由链表组成的有界/无界阻塞队列
PriorityBlockingQueue一个支持优先级排序的无界阻塞队列
DelayQueue一个使用优先级队列实现的无界阻塞队列
SynchronousQueue一个不存储元素的阻塞队列
LinkedTransferQueue一个由链表组成的无界阻塞队列
LinkedBlockingDeque一个由链表组成的双向阻塞队列

3.锁

Lock(Java5新特性)提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。

主要针对一个 JVM 中的多个线程对共享资源的操作。

Java 中常见的锁有独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁

锁 Lock 分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,先进先出,而非公平锁就是一种获取锁的抢占机制。

1.ReentrantLock

ReentrantLock 是一个可重入锁。

  • 位于 java.util.concurrent.locks 包;
  • 和 CountDownLatch、FutureTask、Semaphore 一样基于 AQS 实现,AQS 是 Java 并发用来构建锁或其他同步组件的基础框架,JUC( java.util.concurrent)package 的核心;
  • ReentrantLock 能够实现比 synchronized 更细粒度的控制,如设置公平性 fairness;
  • ReentrantLock 调用 lock() 之后,必须调用 unlock() 释放锁;
  • 性能未必比 synchronized 高,并且也是可重入的。

ReentrantLock 公平性的设置:

// 参数为true时, 倾向于将锁赋予等待时间最久的线程
// 获取锁的顺序按先后调用lock方法的顺序(慎用)
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁

// 获取锁的顺序按抢占的顺序, 看运气
ReentrantLock fairLock = new ReentrantLock(); // 默认非公平锁
ReentrantLock fairLock = new ReentrantLock(false); // 非公平锁

公平性是减少线程饥饿的情况发生的一个办法,线程饥饿指个别线程长期等待锁,但却始终无法获取锁的情况。

synchronized 和 ReentrantLock 的区别:

synchronized 是非公平锁。

其实大多数场景中,公平性未必有想象中的那么重要,Java 默认的调度策略很少会导致饥饿的发生,若要保证公平性,则要引入额外的开销,导致一定的吞吐量下降,所以建议只有当程序确实有公平性需要的时候,才有必要去指定公平锁。

ReentrantLock 相比 synchronized,将锁对象化,提供各种便利的方法,进行精细的同步操作,甚至可以表达 synchronized 难以表达的用例:

  • 判断是否有线程,或者某个特定线程,在排队等待获取锁;
  • 带超时的获取锁的尝试;
  • 感知有没有成功获取到锁。

Condition

如果说 ReentrantLock 将 synchronized 转变成了可控的对象,那么是否也能将 wait/notify/notifyAll 对象化?答案是可以的,java.util.concurrent.locks.Condition 类做到了这一点,

synchronized 和 ReentrantLock 的区别总结:

  • synchronized 是关键字,ReentrantLock 是类;

ReentrantLock 比 synchronized 的扩展性体现在:

  • ReentrantLock 可以对获取锁的等待时间进行设置,避免死锁;
  • ReentrantLock 可以获取各种锁的信息;
  • ReentrantLock 可以灵活地实现多路通知;

最关键的是两者的锁机制也是不一样的,synchronized 底层操作的是 Java 对象头中的 Mark Word,ReentrantLock 底层调用 Unsafe 类的 park() 方法加锁。Unsafe 类可以用来在任意内存地址位置处读写数据,另外,Unsafe 还支持一些 CAS 的操作。

1.ReentrantLock

ReentrantLock 是一种排他锁,同一时间只有一个线程在执行 ReentrantLock.lock() 方法后面的任务。

private ReentrantLock lock = new ReentrantLock(); //默认非公平锁
// private ReentrantLock lock = new ReentrantLock(true); //公平锁, true:公平锁, false:非公平锁
public void testMethod() {
	try {
		lock.lock(); //获取锁
		...
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		lock.unlock(); //释放锁
	}
}

ReentrantLock 实现等待/通知,可以借助于 Condition 对象实现:

public class MyService {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void await() {
        try {
            System.out.println("await start");
            lock.lock(); //获取锁
            condition.await(); //等同于Object类中的wait()方法
            System.out.println("await end");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); //释放锁
        }
    }

    public void signal() {
        try {
            System.out.println("signal start");
            lock.lock(); //获取锁
            condition.signal(); //等同于Object类中的notify()方法, notifyAll同理
            System.out.println("signal end");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); //释放锁
        }
    }
}
public class MyThread1 extends Thread {
    private MyService service;
    public MyThread1(MyService service){
        super();
        this.service = service;
    }
    @Override
    public void run() {
        System.out.println("MyThread1 start");
        service.await();
        System.out.println("MyThread1 end");
    }
}
public static void main(String[] args) {
	MyService service = new MyService();
	MyThread1 t1 = new MyThread1(service);
	t1.start();
	Thread.sleep(3000);
	service.signal();
}

运行结果:

MyThread1 start
await start 
//这里停顿了3秒
signal start
signal end
await end
MyThread1 end

如果需要唤醒的是部分线程,可以通过定义多个 Condition 实现。

ReentrantLock 一些常用的方法如下:

方法说明
int getHoldCount()查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。
int getQueueLength()返回正等待获取此锁定的线程估计数。
int getWaitQueueLength(Condition condition)返回等待与此锁定相关的给定条件Condition的线程估计数。
boolean hasQueuedThread(Thread thread)查询指定的线程是否正在等待获取此锁定。
boolean hasQueuedThreads()查询是否有线程正在等待获取此锁定。
boolean hasWaiters(Condition condition)查询是否有线程正在等待与此锁定有关的condition条件。
boolean isFair()判断是否是公平锁。
boolean isHeldByCurrentThread()查询当前线程是否保持此锁定。
boolean isLocked()查询此锁定是否由任意线程保持。
void lockInterruptibly()如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常。
boolean tryLock()仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定。
boolean tryLock(long timeout, TimeUnit unit)如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。

3.读写锁

1.ReentrantReadWriteLock

排他锁 ReentrantLock 虽然保证了实例变量的线程安全性,但效率却是十分低下的。所以在 JDK 中提供了一种读写锁 ReentrantReadWriteLock 类,使用它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁 ReentrantReadWriteLock 来提升该方法的代码运行速度。

读写锁包含两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也就做排他锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。多个 Thread 可以同时进行读取操作,但是同一时刻只允许一个 Thread 进行写入操作。

1.读读共享

public class MyService {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
        try {
            lock.readLock().lock(); //获取锁
            System.out.println("获取读锁" + Thread.currentThread().getName()
                    + " " + System.currentTimeMillis());
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock(); //释放锁
        }
    }
}
public class MyThread1 extends Thread {
    private MyService service;
    public MyThread1(MyService service){
        super();
        this.service = service;
    }
    @Override
    public void run() {
        service.read();
    }
}
public class MyThread2 extends Thread {
    private MyService service;
    public MyThread2(MyService service){
        super();
        this.service = service;
    }
    @Override
    public void run() {
        service.read();
    }
}
public static void main(String[] args) {
	MyService service = new MyService();
	MyThread1 t1 = new MyThread1(service);
	t1.setName("t1");
	MyThread2 t2 = new MyThread2(service);
	t2.setName("t2");
	t1.start();
	t2.start();
}

程序运行后的结果如下:

获取读锁t1 1550141547658
获取读锁t2 1550141547659

两个线程几乎同时进入 lock() 后面的代码,说明在此使用了 lock.readLock() 读锁可以提高运行效率,允许多个线程同时执行 lock 方法后面的代码。

2.写写互斥

更改类 MyService.java 代码如下:

public class MyService {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void write() {
        try {
            lock.writeLock().lock(); //获取锁
            System.out.println("获取写锁" + Thread.currentThread().getName()
                    + " " + System.currentTimeMillis());
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock(); //释放锁
        }
    }
}

MyThread1、MyThread2 的 run() 方法改为调用 write() 方法。

程序运行后的结果如下:

获取写锁t1 1550144386681
获取写锁t2 1550144396682

说明写写操作是互斥的。

3.读写互斥

更改类 MyService.java 代码如下:

public class MyService {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read() {
        try {
            lock.readLock().lock(); //获取锁
            System.out.println("获取读锁" + Thread.currentThread().getName()
                    + " " + System.currentTimeMillis());
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock(); //释放锁
        }
    }

    public void write() {
        try {
            lock.writeLock().lock(); //获取锁
            System.out.println("获取写锁" + Thread.currentThread().getName()
                    + " " + System.currentTimeMillis());
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock(); //释放锁
        }
    }
}

MyThread2 的 run() 方法调用 read() 方法,而 MyThread2 的 run() 方法改为调用 write() 方法。

程序运行后的结果如下:

获取读锁t1 1550144931598
获取写锁t2 1550144941598

说明读写操作是互斥的。同理写读操作也是互斥的。

  • 16
    点赞
  • 1
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值