为了更好地支持并发程序,JDK内部提供了大量实用的API和框架。
说到同步控制,最先想到的便是synchronized关键字,这是一种最简单的控制方法, 它决定了一个线程是否可以访问临界区资源。配合wait()方法和notify()方法可以达到线程等待和通知的作用。而同步控制的另一种方式便是使用重入锁。重入锁可以完全替代关键字synchronized
ReentrantLock使用示例:
使用重入锁可以保护临界区资源 i,确保多线程对 i 操作的安全性。与synchronized相比,重入锁有显示的操作过程。开发人员必须手动指定何时加锁,何时释放锁。也正是因为这样,重入锁对逻辑控制的灵活性要远远优于关键字synchronized。但要注意的是,在退出临界区时,必须记得释放锁,否则其他线程就没有机会再访问临界区了。
对于synchronized关键字来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就继续保持等待。而使用重入锁,那么就多了一种可能,那就是等待的线程可以中断,让它停止等待。这种中断机制是很有必要的,它对于处理死锁是有一定帮助的。
上图是模拟一个死锁的状态,t1 线程开启后获得 lock1 锁,t2 线程开始后获得 lock2 锁,t1 线程等待 1 秒后想要获取 lock2 锁,t2 线程等待 1 秒后想要获取 lock1 锁,这样就会造成死锁。在第 58 行,我们通过 t2.interrupt() 发出中断标识,在第 35 行和 第 38 行通过 isHeldByCurrentThread()来对中断进行响应,就可以是 t1 线程获取到 lock2 锁,继续执行下去。最后真正完成工作的只有 t1,而 t2 线程则放弃其任务直接退出,释放资源。
除了等待外部通知之外,要避免死锁还有一种方式就是限时等待
上图中,tryLock()方法接收两个参数,一个表示等待时长,一个表示计时单位。在规定的等待时间中,如果还没得到锁,就会返回flase,如果成功获取锁就会返回true。这里设置了 3 秒等待时间,但是占用锁的线程持有锁的时间为 5 秒,因此第二个线程会请求锁失败。
tryLock()方法也可以不带参数直接运行,这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁成功,并返回true,如果锁被其他线程占用,则当前锁不会进行等待,直接返回flase。
在大多数情况下,锁的申请都是非公平的。
非公平锁:A 线程请求到了 锁1,B 线程也来请求锁1,又来了一个 C 线程也来请求锁 1,这里虽然 B 线程比 C 线程早来一步请求,但是最终哪个线程请求到了锁1也是不一定的,以为是从线程等待队列中随机挑选一个。
公平锁:它会按照时间的先后顺序,保证先到者先得,后到者后得。它不会造成饥饿现象,只要你排队,最终还是可以拿到资源的。
如果我们使用synchronized关键字进行锁控制,那么产生的锁就是非公平的,而重入锁允许我们对其公平性进行设置。
public ReentrantLock(boolean fair){}
当fair传入为true时,表示锁是公平的。但是公平锁需要维护一个有序队列,实现成本会比较高,性能会比较低下。默认情况下,锁是非公平的。
公平锁使用示例:
在公平锁的情况下,得到的输出顺序是依次执行的。
非公平锁使用示例:
可以看出,根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配的方式是高效的,但是无公平性而言。
ReentrantLock的几个重要方法:
重入锁的三个重要要素:
Condition的作用和 wait()和notify()是大致相同的,但是wait()和notify()是配合synchronized关键字使用的,而condition是和重入锁相关联的。
Condition 接口提供的基本方法:
void await() throws InterruptedException; //使当前线程等待,同时释放锁。可以等待的时候响应中断
boolean await(long time, TimeUnit unit) throws InterruptedException; //等待指定时间
void awaitUninterruptibly(); //与 await 相同,但是等待的时候不会响应中断
long awaitNanos(long nanosTimeout) throws InterruptedException;boolean awaitUntil(Date deadline) throws InterruptedException;void signal(); //唤醒一个等待中的线程
void signalAll(); //唤醒所有等待中的线程
Condition 使用示例:
注意:第30行我们需要释放锁,让 A 线程重新获取锁,不然虽然已经唤醒了 A 线程,但是因为它没有重新获取到锁,也就无法真正执行。
ArrayBlockingQueue 的部分实现代码:
信号量是为多线程提供了更为强大的控制方法。从广义上来讲,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。
Semaphore构造方法:
public Semaphore(int permits){}
public Semaphore(int permits, boolean fair){} //第二个参数用来控制是否是公平的
Semaphore主要方法:
//尝试获取一个准入的许可,无法获得则会等待,直到有线程释放一个许可或线程被中断,这个方法可以响应中断
public void acquire() throws InterruptedException {}
public void acquireUninterruptibly() {} //与 acquire() 方法相同,但是不响应中断
public boolean tryAcquire() {} //尝试获得一个许可,如果成功则返回true,失败则返回false,不会进行等待
public boolean tryAcquire(long timeout, TimeUnit unit) {} //等待指定的时间
public void release() {} //释放一个许可
Semaphore使用示例:
申请信号量使用acquire()方法操作,在离开时,务必使用replease()方法释放信号量。
读写分离锁可以有效地帮助减少锁竞争,提高系统性能。比如:A1、A2、A3三个线程进行写操作,B1、B2、B3三个线程进行读操作,如果使用重入锁或者内部锁,那么所有读之间,读与写之间,写之间都是串行操作。但是因为读操作并不会造成数据的完整性破坏,因此这种等待是不合理的。
读写锁的访问约束情况:
读 写 读 非阻塞 阻塞 写 阻塞 阻塞
读写锁使用示例:
读线程完全是并行的,写会阻塞读
CountDownLatch是一个非常实用的多线程控制工具类。这个工具通常用来控制线程等待,它可以让一个线程等待直到倒计时结束,再开始执行。
CountDownLatch构造器:
public CountDownLatch(int count) {} //count 表示这个计数器的计数个数
CountDownLatch使用示例:
这里计数数量为6,每当一个线程完成任务后,倒计数器就会减1,使用await()方法,要求主线程等待所有任务都执行完成后才能执行。
CyclicBarrier是另外一种多线程并发控制工具。Cyclic 意为循环,也就是说这个计数器可以反复使用,它比CountDownLatch更加强大一点,它可以接收一个参数作为barrierAction,barrierAction就是当计数器一次计数完成后,系统会执行的动作。
CyclicBarrier构造函数:
public CyclicBarrier(int parties, Runnable barrierAction) {} //parties 表示计数总数
CyclicBarrier使用示例:
与进程相比,线程是一种轻量级的工具。但是再怎么轻量,创建和销毁依然要花费时间,如果为每一个小任务都创建一个线程,就很可能出现创建和销毁线程的时间比线程真实工作的时间还要久,当线程数量过大时,反而会耗尽CPU和内存资源。而且,线程本身也是要占用内存空间,大量的线程会抢占宝贵的内存资源,如果处理不当,会导致Out Of Memory
为了避免系统频繁创建和销毁线程,我们可以让创建的线程复用,那么这个时候线程池就出现了,有点类似数据库的连接池,当系统需要使用数据库时,并不是创建一个新的连接,而是从连接池中获取一个连接即可,使用数据库连接池可以维护一些数据库连接,让它们长期保持在一个激活状态。线程池也是类似的概念。
JDK中提供了一套Executor框架,帮助开发人员有效进行线程控制,其本质就是一个线程池。其中ThreadPoolExecutor表示一个线程池,ThreadPoolExecutor类实现了Executor接口,因此通过这个接口,任何Runnable的对象都可以被ThreadPoolExecutor线程池调度。
Executor框架的各种类型的线程池:
public static ExecutorService newFixedThreadPool(int nThreads) {}
public static ExecutorService newCachedThreadPool() {}
public static ExecutorService newSingleThreadExecutor() {}
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {}
newFixedThreadPool 使用示例:
其他几种类型的线程池使用类似
无论是newFixedThreadPool,newCachedThreadPool还是newSingleThreadExecutor,内部均使用了ThreadPoolExecutor类。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
ThreadPoolExecutor类的构造器:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
喜欢请多多点赞评论转发,关注小编,后续小编会再带来更丰富的学习内容更新,希望大家能够喜欢哦~~~