并发编程从入门到放弃系列开始和结束(一)
对于 Java 部分的面试来说,突然想到并发这一块的内容是不太完整的,这篇文章会通篇把多线程和并发都大致阐述一遍,至少能够达到了解原理和使用的目的,内容会比较多,从最基本的线程到我们常用的类会统一说一遍,慢慢看。
并发编程
进程&线程
对于基本的概念,大家应该都很熟悉了,进程是资源分配的单位,线程是CPU调度的单位,线程是进程中的一个实体。
对于我们的Java程序来说,天生就是多线程的,我们通过main方法启动,就是启动了一个JVM的进程,同时创建一个名为main的线程,main就是JVM进程中的一个实体线程。
进程&线程
线程生命周期
线程几种基本状态:
- New,初始状态,就是New了一个线程,但是还没有调用start方法
- Runnable,可运行Ready或者运行Running状态,线程的就绪和运行中状态我们统称为Runnable运行状态
- Blocked/Wating/Timed_Wating,这些状态统一就叫做休眠状态
- Terminated,终止状态
几个状态之间的转换我们分别来说。
New:我们创建一个线程,但是线程没有调用start方法,就是初始化状态。
Runnable:调用start()启动线程进入Ready可运行状态,等待CPU调度之后进入到Running状态。
Blocked:阻塞状态,当线程在等待进入synchronized锁的时候,进入阻塞状态。
Waiting:等待状态需要被显示的唤醒,进入该状态分为三种情况,在synchonized中调用Object.wait(),调用Thread.join(),调用LockSupport.park()。
Timed_Waiting:和Waiting的区别就是多了超时时间,不需要显示唤醒,达到超时时间之后自动唤醒,调用图中的一些带有超时参数的方法则会进入该状态。
Terminated:终止状态,线程执行完毕。
守护线程&用户线程
Java中的线程分为守护线程和用户线程,上面我们提到的main线程其实就是一个用户线程。
他们最主要的区别就在于,只要有非守护线程没有结束,JVM就不会正常退出,而守护线程则不会影响JVM的退出。
可以通过简单的方法设置一个线程为守护线程。
Thread t = new Thread();
t.setDaemon(true);
锁
锁是控制多线程并发访问共享资源的方式,为了更简单快速的了解Java中的锁,我们可以按照显示锁和隐式锁来做一个大致的区分。
锁
隐式锁
在没有Lock接口之前,加锁通过synchronzied实现,在之前的Java基础系列中我已经说过了,就不在这里过多的阐述,此处引用之前写过的,更多详细可以看《我想进大厂》之Java基础夺命连环16问。
synchronized是java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现,主要作用就是实现原子性操作和解决共享变量的内存可见性问题。
执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。
执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。
如果再深入到源码来说,synchronized实际上有两个队列waitSet和entryList。
- 当多个线程进入同步代码块时,首先进入entryList
- 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
- 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
- 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null
显示锁
虽然synchronized使用简单,但是也使得加锁的流程固化了,显示锁在Java1.5版本之后加入了Lock接口,可以通过声明式显示的加锁和解锁。
Lock lock = new ReentrantLock();
lock.lock(); //加锁
lock.unlock(); //解锁
独占锁
在上述的伪代码中,我们使用到了ReentrantLock,它其实就是独占锁,独占锁保证任何时候都只有一个线程能获得锁,当然了,synchronized也是独占锁。
这里我们看ReentrantLock的几个加锁接口。
void lock(); //阻塞加锁
void lockInterruptibly() throws InterruptedException; //可中断
boolean tryLock(); //非阻塞
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //超时加锁
这几个加锁接口,向我们明白地展示了他和synchronized的区别。
- 可中断加锁lockInterruptibly,synchronized可能会有死锁的问题,那么解决方案就是能响应中断。当前线程加锁时,如果其他线程调用当前线程的中断方法,则会抛出异常。
- 非阻塞加锁tryLock,调用后立刻返回,获取锁则返回true,否则返回false
- 支持超时加锁tryLock(long time, TimeUnit unit),超时时间内获取锁返回true,否则返回false
- 支持公平和非公平锁,公平指的是获取锁按照请求锁的时间顺序决定,先到先得,非公平则是直接竞争锁,先到不一定先得
- 支持Condition
如果你看过阻塞队列的源码,那么你对 Condition 应该挺了解了,我们举个栗子来看看,我们需要实现:
- 如果队列满了,那么写入阻塞
- 如果队列空了,那么删除(取元素)阻塞
我们给阻塞队列提供一个 put 写入元素和 take 删除元素的方法。
put 时候加锁且响应中断,如果队列满了,notFull.await 释放锁,进入阻塞状态,反之,则把元素添加到队列中,notEmpty.signal 唤醒阻塞在删除元素的线程。
take 的时候一样加锁且响应中断,如果队列空了,notEmpty.await 进入释放锁,进入阻塞状态,反之,则删除元素,notFull.signal 唤醒阻塞在添加元素的线程。
public class ConditionTest {
public static void main(String[] args) throws Exception {
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(10);
}
static class ArrayBlockingQueue<E> {
private Object[] items;
int takeIndex;
int putIndex;
int count;
private ReentrantLock lock;
private Condition notEmpty;
private Condition notFull;
public ArrayBlockingQueue(int capacity) {
this.items = new Object[capacity];
lock = new ReentrantLock();
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(E e) throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
notFull.await();
}
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length){
putIndex = 0;
}
count++;
notEmpty.signal();
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
notEmpty.await();
}
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length){
takeIndex = 0;
}
count--;
notFull.signal();
return x;
}
}
}
文章转自公众号:艾小仙