JUC(Java.util.concurrent)的常见类
1.ReentrantLock
可重入互斥锁。和synchronized定位类似,都是使用实现互斥效果,保证线程安全。
ReentrantLock的用法:
- lock():加锁,如果获取不到锁就会死等。
- trylock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁。
- unlock:解锁。
import java.util.concurrent.locks.ReentrantLock;/* Describe:ReentrantLock中lock的使用* User:lenovo* Date:2023-03-30* Time:15:58*/
public class TestDemo1 {public static ReentrantLock lock = new ReentrantLock();public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {lock.lock();try {for (int i = 0; i < 1000; i++) {count++;}}finally {lock.unlock();}});Thread t2 = new Thread(() -> {lock.lock();try {for (int i = 0; i < 1000; i++) {count++;}}finally {lock.unlock();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
ReentrantLock和synchronized的区别:
- synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现的)。ReentrantLock是标准库中的一个类,在JVM外实现的(基于Java实现的)
- synchronized使用时不用手动释放锁。ReentrantLock使用时需要手动释放锁。使用起来更加灵活,但是也容易遗漏unlock.(如果程序抛出异常,或中途跳出,容易导致忘记释放锁)
- synchronized在申请锁失败的时候,会出现死等的情况。ReentrantLock可以通过trylock的方式等待一段时间就放弃。
- synchronized是非公平锁,ReentrantLock默认是非公平锁。可以通过构造方法传入一个true开启公平锁模式。
- 更强大的唤醒机制。synchronized是通过Object的wait/notify实现等待-唤醒。每次唤醒的是一个随机等待的线程。ReentrantLock搭配Condition类实现等待-唤醒,可以更精准控制唤醒某个指定的线程。
如何选择那个锁呢?
- 锁竞争不激烈的时候,使用synchronized,效率更高,自动释放更方便。
- 锁竞争激烈的时候,使用ReentrantLock,搭配trylock更加灵活控制加锁的行为,而不是死等。
- 如果需要使用公平锁,使用ReentrantLock.
2.原子类
原子类内部使用的是CAS实现,所以性能要比加锁实现i++高了好多。原子类有:
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicIntegerLong
- AtomicReference
- AtomicStampedReference
以AtomicInteger举例,常见的方法有:
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;
public class TestDemo3 {public static void main(String[] args) {AtomicInteger a = new AtomicInteger(0);System.out.println(a.incrementAndGet());//++aSystem.out.println(a.getAndIncrement());//a++System.out.println(a.decrementAndGet());//--aSystem.out.println(a.getAndDecrement());//a--}
}
3.线程池
虽然创建销毁线程比创建销毁进程更轻量,但是在频繁的创建和销毁线程的时候还是会比较低效的
线程池就是为了解决这个问题。如果某个线程不在使用了,并不是真正的把线程释放了,而是放到一个池子里。如果需要用到线程就直接从池子中取,不必通过系统来创建。
3.1ExecutorService 和 Executors
【代码示例】
- ExecutorService表示一个线程池示例。
- Executors是一个工厂类,能够创建及几种不同风格的线程池
- ExecutorService的submit方法能够向线程池中提交若干个任务。
public class TestDemo4 {public static void main(String[] args) {ExecutorService pool = Executors.newFixedThreadPool(10);pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});}
}
Executors创建线程池的几种方式
- newFixedThreadPool:创建固定线程数量的线程池;
- newCachedThreadPool:创建线程数量动态增长的线程池;
- newSingleThreadExecutor:创建只包含单个线程的线程池;
- newScheduledThreadPool:设定延迟时间后执行命令,或者定期执行命令。是进阶版的Timer
Executors本质上是ThreadPoolExecutor类的封装。
3.2ThreadPoolExecutor
ThreadPoolExecutor提供了更多的可选参数,可以进一步细化线程池行为的设定。
【构造方法】
理解ThreadPoolExecutor构造方法的参数
把创建一个线程可以想象为开个公司,每个员工相当于一个线程。
- corePoolSize:正式员工的数量(线程一旦被创建就不会销毁)
- maximumPoolSize:正式员工 + 临时工(线程一段时间不使用,就会销毁)
- keepAliveTime:临时工允许的空闲时间;
- unit:keepAliveTime的时间单位,是秒,分钟,还是其他的值;
- workQueue:传递任务的阻塞队列
- threadFacktory:创建线程的工厂,参与具体的创建线程工作;
- RejectedExecutionHandle:拒绝策略,如果任务量超过公司的接下来怎么处理
【拒绝策略】
- AbortPolicy():超过负荷,直接抛出异常;
- CallerRunsPolicy:调用者负责处理;
- DiscardOldestPolicy():丢弃队列中最老的任务;
- DiscardPolicy():丢弃最新的任务。
public class TestDemo5 {public static void main(String[] args) {ExecutorService pool = new ThreadPoolExecutor(4, 8, 1000, TimeUnit.MICROSECONDS,new SynchronousQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());for (int i = 0; i < 9; i++) {pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}});}}
}
4.信号量
信号量:用来表示可用资源的个数。本质上是一个计数器。
【理解信号量】可以把信号量理解为停车场的指示牌:当前由100个车位,表示有100个车位是空闲的。当有一辆车进入的时候,相当于申请一个资源,数量就会-1;当有一辆车出来的时候,相当于释放资源,数量就会+1。同样的栗子,有火车票剩余数量,酒店空闲房间等。
Semaphore的PV操作中加减计数器操作都是原子的,可以在多线程的环境下使用。
【代码示例】
- 创建Semaphore示例,初始化为1,表示有1个可用的资源;
- acquire方法表示申请资源(P操作),release方法表示释放资源(V操作)
- 创建20个线程,每个线程都尝试申请资源,sleep1秒之后释放资源。
public class TestDemo6 {public static void main(String[] args) {Semaphore semaphore = new Semaphore(1);Runnable runnable = new Runnable() {@Overridepublic void run() {try {System.out.println("申请资源");semaphore.acquire();System.out.println("获取到资源了");Thread.sleep(1000);semaphore.release();System.out.println("释放资源了");} catch (InterruptedException e) {e.printStackTrace();}}};for (int i = 0; i < 20; i++) {Thread t = new Thread(runnable);t.start();}}}
5.CountDownLatch
同时等待N个任务执行结束
【举个栗子】好像跑步比赛,10个选手依次就位,哨声响起同时出发;所有选手到达终点,比赛此结束。
- 构造CountDownLatch,初始化10,表示有10个任务需要完成。
- 每个任务执行完毕,都调用latch.countDown().在CountDownLatch内部的计数器同时自减;
- 主线程中使用latch.await();阻塞等待所有任务执行完成。相当于计数器为0了。
public class TestDemo7 {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(10);Runnable r = new Runnable() {@Overridepublic void run() {System.out.println("hello");try {Thread.sleep(1000);latch.countDown();} catch (InterruptedException e) {e.printStackTrace();}}};for (int i = 0; i < 10; i++) {new Thread(r).start();}latch.await();System.out.println("比赛结束");}
}
6.相关面试题
1)线程同步的方式有哪些?
synchronized,ReentrantLock,Semaphore等都可以用于线程同步。
2)为什么有了synchronized,还需要juc下的lock?
以juc的ReentrantLock为例,
- synchronized使用时不需要手动释放锁。ReentrantLock需要手动释放锁,使用起来更灵活。
- synchronized在申请锁失败的时候,会死等。ReentrantLock可以通过trylock的方式等待一段时间就放弃了。
- synchronized是非公平锁。ReentrantLock默认是非公平锁,可以通过构造方法传入一个true开启公平锁模式。
- synchronized是通过Object的wait/notify实现等待唤醒。每次唤醒的是一个随机等待的线程。ReentrantLock搭配Condition类实现等待-唤醒,更加精确控制某个指定的线程
3)AtomicInteger实现的原理是什么?
基于CAS机制来实现