文章目录
Java并发
1. 线程的状态
在任意时间点,一个线程只能有其中的一种状态
- 新建(new)
- 创建后尚未启动的线程就是处于这种状态。
- 新建一个线程的三种方法:
(1)通过继承 Thread 类,重写 run 方法
(2)通过实现 Runnable 接口创建线程
(3)通过实现 Callable 接口
- 就绪(Runnable)
也被称为“可执行状态”。一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当调用了线程对象的start()方法即启动了线程,此时线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他就绪线程竞争CPU,只有获得CPU使用权才可以运行线程。比如在单核心CPU的计算机系统中,不可能同时运行多个线程,一个时刻只能有一个线程处于运行状态。对与多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度执行。 - 运行状态(Running)
线程获取到CPU使用权进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。真正开始执行run()方法的内容。 - 阻塞状态(Blocked)
线程在获取锁失败时(因为锁被其它线程抢占),它会被加入锁的同步阻塞队列,然后线程进入阻塞状态(Blocked)。处于阻塞状态(Blocked)的线程放弃CPU使用权,暂时停止运行。待其它线程释放锁之后,阻塞状态(Blocked)的线程将在次参与锁的竞争,如果竞争锁成功,线程将进入就绪状态(Runnable) - 等待状态(WAITING)
或者叫条件等待状态,当线程的运行条件不满足时,通过锁的条件等待机制(调用锁对象的wait()或显示锁条件对象的await()方法)让线程进入等待状态(WAITING)。处于等待状态的线程将不会被cpu执行,除非线程的运行条件得到满足后,其可被其他线程唤醒,进入阻塞状态(Blocked)。调用不带超时的Thread.join()方法也会进入等待状态。 - 限时等待状态(TIMED_WAITING)
限时等待是等待状态的一种特例,线程在等待时我们将设定等待超时时间,如超过了我们设定的等待时间,等待线程将自动唤醒进入阻塞状态(Blocked)或就绪状态(Runnable) 。在调用Thread.sleep()方法,带有超时设定的Object.wait()方法,带有超时设定的Thread.join()方法等,线程会进入限时等待状态(TIMED_WAITING)。 - 死亡状态(TERMINATED)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
2. 线程间的通信
Thread 类中常见的方法
-
interrupted
-
join
Thread 的非静态方法 join() 让一个线程等待另一个线程完成才继续执行。如果线程 A 执行体中调用 B 线程的 join() 方法,则 A 线程将会阻塞,直到B线程执行完为止,A才能得以继续执行。 -
sleep【不释放锁】
Sleep——让当前正在执行的线程先暂停一定的时间,并进入阻塞状态。在其睡眠的时间段内,该线程由于不是处于就绪状态,因此不会得到执行的机会。即使此时系统中没有任何其他可执行的线程,处于sleep()中的线程也不会执行。因此sleep()方法常用来暂停线程的执行。当sleep()结束后,然后转入到 Runnable(就绪状态),这样才能够得到执行的机会。 -
yield【线程让步:不会释放锁】
让一个线程执行了yield()方法后,就会进入Runnable(就绪状态),【ble(就绪状态),【不同于sleep()和join()方法,因为这两个方法是使线程进入阻塞状态】。
除此之外,yield()方法还与线程优先级有关,当某个线程调用yield()方法时,就会从运行状态转换到就绪状态后,CPU从就绪状态线程队列中只会选择与该线程优先级相同或者更高优先级的线程去执行。
3. 原子性、可见性和有序性
原子性、可见性和有序性
Java 内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。
- 原子性
- 可见性
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对synchronzed内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized具有可见性。同样的在volatile分析中,会通过在指令中添加lock指令,以实现内存可见性。因此, volatile具有可见性 - 有序性
4. synchronized
4.1 synchronized 使用场景
synchronized 可以用在方法上,也可以使用在代码块中。其中方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。这里的需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。
(1)如果synchronized锁住的是实例对象,就可以防止多个线程同时访问这个对象的 synchronized 方法。
(2)如果synchronized锁住的是类对象,那么就可以防止多个线程同时访问这个类中的 synchronized 方法,此种修饰可以对此类的所有对象实例起作用。
4.2 synchronized 底层实现方式
synchronized 底层实现方式
我们知道java是用字节码指令来控制程序,在字节指令中,存在有synchronized所包含的代码块,那么会形成2段流程的执行。 其实 synchronized 映射成字节码指令就是增加来两个指令:monitorenter 和 monitorexit 。当一条线程进行执行的遇到monitorenter指令的时候,它会去尝试获得锁,如果获得锁那么锁计数+1(为什么会加一呢,因为它是一个可重入锁,所以需要用这个锁计数判断锁的情况),如果没有获得锁,那么阻塞。当它遇到monitorexit的时候,锁计数器-1,当计数器为0,那么就释放锁。
synchronized锁释放有两种机制,一种就是执行完释放;另外一种就是发送异常,虚拟机释放。
4.3 对象锁(monitor)机制 && Java 对象头
我们知道,对象被创建在堆中,并且对象在内存中的存储布局方式可以分为3块区域:对象头、实例数据、对齐填充。
一般而言,synchronized 使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头,其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构:
同步的时候是获取对象的 monitor,即获取对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。
锁状态:锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
-
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
(1)偏向锁的获取
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。 -
轻量级锁
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。 -
重量级锁
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
可重入锁与不可重入锁
- 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
- 可重入锁的一个优点是可一定程度避免死锁
- AQS通过控制status状态来判断锁的状态,对于非可重入锁状态不是0则去阻塞;对于可重入锁如果是0则执行,非0则判断当前线程是否是获取到这个锁的线程,是的话把status状态+1,释放的时候,只有status为0,才将锁释放。
5. Lock
- 首先synchronized是 Java 内置关键字,在 jvm 层面,Lock 是个 Java 类
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁
6. AQS 框架
概述
类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架。许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。
框架
它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:
- getState()
- setState()
- compareAndSetState()
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源
- tryAcquire():独占方式。尝试获取资源,成功则返回true,失败则返回false
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false
6. Semaphore
semaphore 主要用于控制当前可活动的线程数目。就如同停车场系统一般,而Semaphore则相当于看守的人,用于控制总共允许停车的停车位的个数,而对于每辆车来说就如同一个线程,线程需要通过acquire()方法获取许可,而release()释放许可。如果许可数达到最大活动数,那么调用acquire()之后,便进入等待队列,等待已获得许可的线程释放许可,从而使得多线程能够合理的运行。
CountDownLatch
CountDownLatch
CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。CountDownLatch的作用也是如此,在构造CountDownLatch的时候需要传入一个整数n,在这个整数“倒数”到0之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。总结来说,CountDownLatch的作用就是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,然后主线程才继续往下执行。
CountDownLatch主要有两个方法:countDown()和await()。countDown()方法用于使计数器减一,其一般是执行任务的线程调用,await()方法则使调用该方法的线程处于等待状态,其一般是主线程调用。这里需要注意的是,countDown()方法并没有规定一个线程只能调用一次,当同一个线程调用多次countDown()方法时,每次都会使计数器减一;另外,await()方法也并没有规定只能有一个线程执行该方法,如果多个线程同时执行await()方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁。
5. Synchronized 和 Lock 的区别
- Lock主要是通过编码的方式实现锁,也就是 AQS。在硬件层面依赖特殊的CPU指令(CAS)
- synchronized 主要通过底层 JVM 进行实现,而且 JVM 为了优化,产生偏向锁、轻量级锁、重量级锁
6. volatile
Java 内存模型告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。**线程在工作内存进行操作后何时会写到主内存中?**这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
Java 中的锁
1. 公平锁 / 非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁 。 new ReentrntLock(isFare);
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
2. 可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
synchronized void setA() {
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
对于ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
3. 独享锁 / 共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
对于Synchronized而言,当然是独享锁。
4. 乐观锁 / 悲观锁
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
5. 分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
6. 偏向锁 / 轻量级锁 / 重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
7. 自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
7. Java 的线程池?具体介绍一下每一个参数?线程具体怎么分配?哪些线程会被销毁?
- 与数据库连接池类似的是,线程池在系统启动的时候即创建大量的空闲线程。程序将一个 Runnable 对象传给线程池,线程池就会启动一条线程来执行该对象的 run 方法,当 run 方法结束后,该线程并不会死亡,而是返回线程池成为空闲状态,等待下一个 Runnable 对象的 run 方法。
- 使用线程池执行线程任务的步骤如下:
(1)调用 Executors 类的静态工厂方法创建一个 ExecutorService 对象,该对象代表一个线程池。
(2)调用 Runnable 或 Callable 实现类的实例,作为线程执行任务。
(3)调用 ExecutorService 对象的 submit 方法来提交 Runnable 实例或 Callable 实例。
(4)当不想提交任何任务时调用 ExecutorService 对象的 shutdown 方法来关闭线程池。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class TestThread implements Runnable{
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"的i值为"+i);
}
}
}
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(6);
pool.submit(new TestThread());
pool.submit(new TestThread());
pool.shutdown();
}
}
- ThreadPoolExecutor
ThreadPoolExecutor提供了四个构造方法,我们看下最重要的一个构造函数:
public class ThreadPoolExecutor extends AbstractExecutorService {
public ThreadExecutor(int corePoolSize, int maximunPoolSize, long keepAliveTime, TimeUnit unit
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory
RejectedExecutionHandler handler){
}
}
函数的参数含义如下:
- corePoolSize: 线程池维护线程的最少数量
- maximumPoolSize:线程池维护线程的最大数量
- keepAliveTime: 线程池维护线程所允许的空闲时间
- unit: 线程池维护线程所允许的空闲时间的单位
- workQueue: 线程池所使用的缓冲队列
- handler: 线程池对拒绝任务的处理策略
线程池执行任务的过程:
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。 - 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
继承关系:
任务拒绝策略:
当线程池的任务缓存队列已满并且线程池中的线程数目达到 maximumPoolSize 时,如果还有任务到来就采取拒绝策略,通常有一下四种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
预设的定制线程池
ThreadPoolExecutor预设了一些已经定制好的线程池,由Executors里的工厂方法创建。
Executors 提供的四种线程池:
Executors 提供了一系列工厂方法用于创先线程池,返回的线程池都实现了 ExecutorService 接口。
这四种方法都是用的 Executors 中的 ThreadFactory 建立的线程。
- newCacheThreadPool()
- 缓存型池子,先查看池中有没有以前建立的线程,如果有,就 reuse 如果没有,就建一个新的线程加入池中
- 缓存型池子通常用于执行一些生存期很短的异步型任务 因此在一些面向连接的 daemon 型 SERVER 中用得不多。但对于生存期短的异步任务,它是 Executor 的首选。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newCachedThreadPool生成一个会缓存的线程池,线程数量可以从0到Integer.MAX_VALUE,超时时间为1分钟。线程池用起来的效果是:如果有空闲线程,会复用线程;如果没有空闲线程,会新建线程;如果线程空闲超过1分钟,将会被回收。
- newFixedThreadPool(int):
- newFixedThreadPool 与 cacheThreadPool 差不多,也是能 reuse 就用,但不能随时建新的线程。
- 其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子。
- 和 cacheThreadPool 不同,FixedThreadPool 没有 IDLE 机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的 TCP 或 UDP IDLE 机制之类的),所以 FixedThreadPool 多数针对一些很稳定很固定的正规并发线程,多用于服务器。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newFixedThreadPool的corePoolSize和maximumPoolSize都设置为传入的固定数量,keepAliveTim设置为0。线程池创建后,线程数量将会固定不变,适合需要线程很稳定的场合。
- newScheduledThreadPool(int)
- 调度型线程池
- 这个池子里的线程可以按 schedule 依次 delay 执行,或周期执行
- newSingleThreadExecutor()
- 单例线程,任意时间池中只能有一个线程
- 用的是和 cache 池和 fixed 池相同的底层池,但线程数目是 1-1,0 秒 IDLE(无 IDLE)
等待队列
newCachedThreadPool的线程上限几乎等同于无限,但系统资源是有限的,任务的处理速度总有可能比不上任务的提交速度。因此,可以为ThreadPoolExecutor提供一个阻塞队列来保存因线程不足而等待的Runnable任务,这就是BlockingQueue。
JDK为BlockingQueue提供了几种实现方式,常用的有:
- ArrayBlockingQueue:数组结构的阻塞队列
- LinkedBlockingQueue:链表结构的阻塞队列
- PriorityBlockingQueue:有优先级的阻塞队列
- SynchronousQueue:不会存储元素的阻塞队列
newFixedThreadPool和newSingleThreadExecutor在默认情况下使用一个无界的LinkedBlockingQueue。要注意的是,如果任务一直提交,但线程池又不能及时处理,等待队列将会无限制地加长,系统资源总会有消耗殆尽的一刻。所以,推荐使用有界的等待队列,避免资源耗尽。但解决一个问题,又会带来新问题:队列填满之后,再来新任务,这个时候怎么办?后文会介绍如何处理队列饱和。
newCachedThreadPool使用的SynchronousQueue十分有趣,看名称是个队列,但它却不能存储元素。要将一个任务放进队列,必须有另一个线程去接收这个任务,一个进就有一个出,队列不会存储任何东西。因此,SynchronousQueue是一种移交机制,不能算是队列。newCachedThreadPool生成的是一个没有上限的线程池,理论上提交多少任务都可以,使用SynchronousQueue作为等待队列正合适。
ThreadFactory
每当线程池需要创建一个新线程,都是通过线程工厂获取。如果不为ThreadPoolExecutor设定一个线程工厂,就会使用默认的defaultThreadFactory:
public static ThreadFactory defaultThreadFactory() {
return new DefaultThreadFactory();
}
22. Java 中的队列有哪些?
首先,我们要明白使用队列的目的是什么?如果是一些及时的消息的处理,并且处理时间很短的情况下是不需要使用队列的,直接阻塞式的方法调用就可以了。但是,如果在消息处理的时候特别费时间,这个时候如果有新的消息来了,就只能处于阻塞状态,造成用户等待,这个时候在项目中引入队列是十分有必要的。当我们接受到消息后,先把消息放到队列中,然后再用新的线程进行处理,这个时候就不会有消息的阻塞了。
-
Java 中的队列
- 非阻塞队列
- LinkedList
- PriorityQueue
- ConcurrentLinkedQueue
- 阻塞队列
- ArrayBlockingQueue
- LinkedBlockingQueue
- PriorityBlockingQueue
- 非阻塞队列
-
什么是阻塞队列?
阻塞队列
阻塞队列是一个在队列基础上又支持了两个附加操作的队列。
2个附加操作:
支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。
支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。 -
阻塞队列的应用场景
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。 -
BlockingQueue
- 放入数据
(1)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程);
(2)offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
(3)put(anObject):把 anObject 加到 BlockingQueue 里,如果 BlockQueue没有空间,则调用此方法的线程被阻断直到 BlockingQueue 里面有空间再继续。 - 获取数据
(1)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
(2)poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
(3)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
(4)drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
- 放入数据
-
常见的 BlockingQueue
-
ArrayBlockingQueue
ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同LinkedBlockingQueue。 -
LinkedBlockingQueue
而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。 -
PriorityBlockingQueue
基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
-
执行器和线程池
线程池具备这样的优先级处理策略:
- 请求到来首先交给 coreSize 内的常驻线程执行
- 如果 coreSize 的线程全忙,任务被放入队列里
- 如果队列放满了,会新增线程,直到达到 maxSize
- 如果还是处理不过来,会把一个异常扔到RejectedExecutionHandler中去,用户可以自己设定这种情况下的最终处理策略。
ExecutorService:
- Future,异步计算的结果对象
- Callable
- Executor,执行提交任务的对象,只有一个 execute 方法
- Executors,辅助类和工厂类,帮助生成下面这些ExecutorService
- ExecutorService,Executor的子接口,管理执行异步任务的执行器
- AbstractExecutorService
- ThreadPoolExecutor,线程池,AbstractExecutorService的子类
- RejectedExecutionHandler,当任务无法被执行的时候,定义处理逻辑的地方,前面已经提到过了
- ThreadFactory,线程工厂,用于创建线程
8. 介绍一下 CAS
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A) 和 新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)
ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
9. ThreadLocal
ThreadLocal 是什么?
ThreadLocal 很多地方叫做线程本地变量,ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程都可以访问自己内部的副本变量。也就是对于同一个ThreadLocal,每一个 get、set、remove 方法只会影响自身线程的数据,不会干扰其它线程中的数据。
ThreadLocal 的实现
从 ThreadLocal 的 set 方法说起,set 是用来设置想要在线程本地的数据,可以看到先拿到当前线程,然后获取当前线程的 ThreadLocalMap,如果 map 不存在先创建一个 map,然后设置本地变量值。
public void set(){
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal 是什么?和线程有什么关系?
ThreadLocal
可以看到 ThreadLocal 其实是线程自身的一个成员属性 threadLocals 的类型。也就是说,线程本地数据都是存在这个 threadLocals 应用的 ThreadLocalMap 中的。
我们接下来看看 ThreadLocalMap,跟想象中的Map有点不一样,它其实内部是有个Entry数组,将数据包装成静态内部类 Entry 对象,存储在这个 table 数组中,数组的下标是threadLocal的threadLocalHashCode&(INITIAL_CAPACITY-1),因为数组的大小是2的n次方,那其实这个值就是threadLocalHashCode%table.length,用&而不用%,其实是提升效率。只要数组的大小不变,这个索引下标是不变的,这也方便去set和get数据。
我们来看看什么是 Entry,Entry 继承自 WeakReference,构造方法有两个参数,一个是 threadLocal 对象,一个是线程本地变量。
static class Entry extends WeakReference<ThreadLocal>{
Object value;
Entry(ThreadLocal k, Object v){
super(k);
value = v;
}
}
看到这里大家应该就明白了,每个线程自身都维护着一个ThreadLocalMap,用来存储线程本地的数据,可以简单理解成ThreadLocalMap的key是ThreadLocal变量,value是线程本地的数据。就这样很简单的实现了线程本地数据存储和交互访问。
- 当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
- 首先,它是一个数据结构,有点像HashMap,可以保存"key : value"键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。
ThreadLocal<String> localName = new ThreadLocal();
localName.set("占小狼");
String name = localName.get();
在线程1中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值 占小狼,同时在线程1中通过 localName.get()可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。
3. 这是为什么,如何实现各个线程数据互不干扰?
不过之前也说了,ThreadLocal 保证了各个线程的数据互不干扰。
可以发现,每个线程中都有一个 ThreadLocalMap 数据结构,当执行 set 方法时,其值是保存在当前线程的 threadLocals 变量中,当执行 set 方法中,是从当前线程的 threadLocals 变量获取。
所以在线程1中set的值,对线程2来说是摸不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。
- ThreadLocal 原理总结
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
这时ThreadLocal 的 get() 方法,用来获取当前线程操控的那个变量副本
代码解析:
5. 获取当前所在线程
6. 通过当前线程当做key 得到一个叫 ThreadLocalMap 的家伙
7. 如果存在就从这个ThreadLocalMap 里面去取得它的enter对象
8. 返回 enter对象保存的value。
- 线程的每个变量副本存储在哪儿?
ThreadlocalMap 就是用来保存每个线程的变量副本的,构造方法的 第一个参数(K)是 当前的所在线程,第二个参数(V)就是变量副本。
线程如何同步?
-
为什么要使用同步?
Java允许多线程并发控制,当多线程同时操作一个可共享的资源变量时,将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程调用,从而保证了该变量的唯一性和准确性。 -
同步方法 synchronized
synchronized关键字修饰方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。 -
使用特殊域变量(volatile)实现线程同步
-
使用重入锁实现线程同步【ReentrantLock】
-
使用局部变量实现线程同步【ThreadLocal】
如果使用ThreadLocal管理变量,则每一个使用变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。 -
使用阻塞队列实现线程同步
(1)LinkedBlockingQueue
(2)ArrayBlockingQueue
JUC 包
并发容器类
- Java 5.0 在 java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能
ConcurrentHashMap
ConcurrentHashMap 同步容器类是 Java5 增加的一个线程安全的哈希表;介于 HashMap 与 Hashtable 之间。内部采用"锁分段"机制替代Hashtable的独占锁,进而提高性能。