可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全,ReentrantLock 也是可重入锁. “Reentrant” 这个单词的原意就是 “可重入”
ReentrantLock 的用法:
lock() 方法:
void lock()
:如果锁可用,获取锁。如果锁不可用,当前线程会被阻塞,直到获取到锁为止。这是一个典型的阻塞式调用。tryLock() 方法:
boolean tryLock()
:尝试获取锁。如果锁可用,立即获取锁,并返回 true。如果锁不可用,立即返回 false,而不会等待。boolean tryLock(long timeout, TimeUnit unit)
:加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁,如果在指定的超时时间内获取到锁,则返回 true,否则返回 false。unlock() 方法:
void unlock()
:释放锁,允许其他线程尝试获取锁。在使用完锁之后,必须显式地调用此方法进行解锁,否则会导致死锁。下面是一个使用 ReentrantLock 的简单示例:
import java.util.concurrent.locks.ReentrantLock;
class MyThread implements Runnable {
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
lock.lock(); // 加锁
// 临界区代码
} finally {
lock.unlock(); // 解锁
}
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
thread1.start();
thread2.start();
}
}
ReentrantLock 和 synchronized 的区别:
// ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
如何选择使用哪个锁?
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个
以 AtomicInteger 举例,常见方法有
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;
虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.
线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 "池子"中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.
代码示例:
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
Executors 创建线程池的几种方式
我们之前提到过:Executors 本质上是 ThreadPoolExecutor 类的封装,那么什么是ThreadPoolExecutor呢?
ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定.
ThreadPoolExecutor是Java中用于创建和管理线程池的类。它提供了多个不同的构造方法来满足不同的需求。下面是ThreadPoolExecutor常用的四个构造方法的讲解:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue):
该构造方法指定了核心线程池大小(corePoolSize)、最大线程池大小(maximumPoolSize)、线程的空闲时间(keepAliveTime)以及这些参数的单位(unit),还有一个用于保存等待执行任务的阻塞队列(workQueue)。当一个任务提交到线程池时,如果线程池中的线程数量小于corePoolSize,则会创建一个新的线程来执行任务。如果线程池中的线程数量已经达到corePoolSize,但是阻塞队列(workQueue)还未满,则将任务添加到阻塞队列中等待执行。如果阻塞队列已满,且线程池中的线程数小于maximumPoolSize,则创建新的线程来执行任务。如果线程池中的线程数已达到maximumPoolSize且阻塞队列也已满,则根据ThreadPoolExecutor的拒绝策略来处理该任务。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory):
该构造方法与前一个构造方法相同,不过多了一个参数threadFactory,用于自定义创建线程的工厂类。可以通过自定义线程工厂来为线程池中的线程指定名称、设置线程的优先级等。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler):
该构造方法与第一个构造方法相同,不过多了一个参数handler,用于处理任务被拒绝后的策略。RejectedExecutionHandler是一个接口,有四种预定义的实现:AbortPolicy(默认策略,直接抛出RejectedExecutionException异常)、CallerRunsPolicy(使用调用线程来执行被拒绝的任务)、DiscardOldestPolicy(丢弃阻塞队列中最旧的任务)、DiscardPolicy(直接丢弃被拒绝的任务)。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler):
该构造方法将前面三个构造方法的参数都包含进来,并多了一个threadFactory和handler参数。
理解 ThreadPoolExecutor 构造方法的参数
把创建一个线程池想象成开个公司. 每个员工相当于一个线程.
代码示例:
ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
newThreadPoolExecutor.AbortPolicy());
for(int i=0;i<3;i++) {
pool.submit(new Runnable() {
@Override
void run() {
System.out.println("hello");
}
});
}
信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.
理解信号量:
可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.
当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)
当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)
如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.
注意:Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.
代码示例:
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("申请资源");
semaphore.acquire();
System.out.println("我获取到资源了");
Thread.sleep(1000);
System.out.println("我释放资源了");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 20; i++) {
Thread t = new Thread(runnable);
t.start();
}
就好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。
public class Demo {
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(10);
Runnable r = new Runable() {
@Override
public void run() {
try {
Thread.sleep(Math.random() * 10000);
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10; i++) {
new Thread(r).start();
}
// 必须等到 10 人全部回来
latch.await();
System.out.println("比赛结束");
}
}
原来的集合类, 大部分都不是线程安全的(Vector, Stack, HashTable, 是线程安全的其他的集合类不是线程安全的)
自己使用同步机制 (synchronized 或者 ReentrantLock)
前面做过很多相关的讨论了. 此处不再展开.
Collections.synchronizedList(new ArrayList);
synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
synchronizedList 的关键操作上都带有 synchronized
使用 CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
优点:
缺点:
ArrayBlockingQueue
基于数组实现的阻塞队列
LinkedBlockingQueue
基于链表实现的阻塞队列
PriorityBlockingQueue
基于堆实现的带优先级的阻塞队列
TransferQueue
最多只包含一个元素的阻塞队列
HashMap 本身不是线程安全的,在多线程环境下使用哈希表可以使用:
这相当于直接针对 Hashtable 对象本身加锁.
2) ConcurrentHashMap
相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例
扩容方式为:
死锁是多个进程或线程由于竞争共享资源而陷入的一种无限等待的状态,导致它们都无法继续执行。在死锁中,每个进程或线程都在等待其他进程或线程释放资源,但这些资源都被其他进程或线程占用,从而导致所有参与者都无法继续执行,形成了相互等待的循环。
死锁通常发生在以下四个必要条件同时满足时:
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。