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());//++a
System.out.println(a.getAndIncrement());//a++
System.out.println(a.decrementAndGet());//--a
System.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() {
@Override
public 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() {
@Override
public 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() {
@Override
public 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() {
@Override
public 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机制来实现