线程池中提交一个任务的流程是怎样的?
- 当一个任务过来的时候首先会判断线程池的核心线程数,如果小于核心线程数就创建一个新的线程
- 如果大于就会把这个任务放到工作队列中去
- 如果工作队列也满了就会入队失败,就开启辅助线程
- 如果线程的个数大于等于最大线程数了会执行拒绝策略,拒绝这个任务
线程池中有几种状态,分别是如何变化的?
- Running:运行态可以接收新的任务也会处理任务
- Shutdown:不会接收新的任务,但是会继续处理队列中的任务
- stop:不会接收新的任务也不会处理队列中的任务,直接中断所有的线程
- tidying:所有线程都已停止,调用terminated()
- terminated:terminated()执行完之后转变成terminated状态
如何优雅的停止一个线程
- stop(),interrept()
hashmap的put方法
- 根据key通过hash算法算出数组下标
- 如果数组下标元素为空就会把这个元素放进去
- 如果不为空的话会看当前位置上的类型是红黑树还是链表吗,如果这个key存在就更新这个链表,如果不存在会对这个链表用尾插法,如果数组长度小于8链表长度大于64优先扩容,数组长度大于8链表长度大于64由数组转换为红黑树,如果是红黑树如果这个key存在就更新这个红黑树,不存在就直接增加
reentrantLock中lock和tryLock()方法的区别
- tryLock()表示尝试加锁,该方法不会阻塞线程,可以进行其他逻辑的处理,如果加到锁返回true,没有加到返回false
- lock()会阻塞直到加到锁才能执行
reentrantLock中公平锁和非公平锁的底层实现
- 公平锁就是线程在适用lock()加锁的时候会去看队列里面有没有现成在排队,如果有的话他就会去排队
- 非公平锁就是线程不管队列里面有没有线程在排队都会去竞争锁资源
ThreadLocal的底层原理
- 我们可以利用threadLocal将数据缓存在线程内部,将变量变为线程的私有数据
- threadLocal底层是通过threadLocalMap来实现的,map的key为threadLocal对象,value为需要缓存的值
- 可能会造成内存泄漏,因为如果使用完之后不释放很难进行回收,使用完 ThreadLocal方法后最好手动调用remove()方法
进程与线程
- 线程共享的区域有堆和方法区,线程私有的有本地方法栈、程序计数器、虚拟机栈,各个线程切换的负担比进程小很多
- 进程就是程序执行一次的单位
线程的生命周期及状态
当实例化一个Thread类之后线程处于一个新建的状态,调用start方法之后由新建状态转换成就绪状态,就绪状态的线程抢占CPU,获取到时间片之后会转换成运行态,运行态的线程调用sleep方法会进入超时等待状态,并且没有释放锁资源,一定时间之后会被自动唤醒,运行态的线程执行wait方法之后会进入等待状态,他释放了锁资源需要其他线程调用notify或者notifyAll方法对他进行唤醒,运行态的线程执行yield方法切换为就绪态,释放了锁,继续去抢占CPU资源,抢占到了会转换为运行态,最后线程执行完毕进入终止态,没有争夺到锁资源的线程会进入阻塞态。
sleep和wait的区别
- 来自不同的类 wait => Object sleep => Thread
- 关于锁的释放
wait 会释放锁,sleep 睡觉了,抱着锁睡觉,不会释放! - wait必须在同步代码块中,sleep可以在任何地方
Lock锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类, 把资源类丢入线程
Ticket2 ticket = new Ticket2();
// @FunctionalInterface 函数式接口,jdk1.8
//lambda表达式 (参数)->{ 代码 }
new Thread(() -> {
for (int i = 1; i < 40; i++)ticket.sale();}, "A").start();
new Thread(() -> {
for (int i = 1; i < 40; i++)ticket.sale();}, "B").start();
new Thread(() -> {
for (int i = 1; i < 40; i++)ticket.sale();}, "C").start();
}
}// Lock三部曲
// 1、 new ReentrantLock();
// 2、 lock.lock(); // 加锁
// 3、 finally=>lock.unlock(); // 解锁
class Ticket2 {
// 属性、方法
private int number = 30;
Lock lock = new ReentrantLock();
public void sale() {
lock.lock(); // 加锁
try {
// 业务代码
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了" +
(number--) + "票,剩余:" + number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); // 解锁
}
}
}
Sychronized和Lock的区别
- Synchronized 内置的Java关键字, Lock 是一个Java类。
- Synchronized会自动的加锁和释放锁,Lock需要手动的去加锁和释放锁lock,unlock。
- Synchronized是非公平锁,Lock可以选择公平锁或者非公平锁。
- Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁。
生产者和消费者问题
- Synchronized版
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
// 判断等待,业务,通知
class Data{ // 数字 资源类
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
while (number!=0){
//0
// 等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我+1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
while (number==0){ // 1
// 等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
this.notifyAll();
}
}
- Lock版
Condition 精准的通知和唤醒线程 能实现A->B->C->D顺序执行
public class C {
public static void main(String[] args) {
Data3 data = new Data3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printA();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printB();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printC();
}
}, "C").start();
}
}
class Data3 { // 资源类 Lock
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number = 1; // 1A 2B 3C
public void printA() {
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number != 1) {
// 等待
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "=>AAAAAAA");
// 唤醒,唤醒指定的人,B
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number != 2) {
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "=>BBBBBBBBB");
// 唤醒,唤醒指定的人,c
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
// 业务,判断-> 执行-> 通知
// 业务,判断-> 执行-> 通知
while (number != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "=>BBBBBBBBB");
// 唤醒,唤醒指定的人,c
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
synchronized和volatile的区别
- synchronized具有原子性,就是他可以保证一段代码在任何时间都只有一个线程去执行,其他线程因为没有争夺到锁资源只能阻塞
- volatile具有可见性和禁止指令重排的特点,不具有原子性。可见性就是当一个线程修改一个共享变量的时候其他线程可以看到他修改之后的结果,指令重排是这样的,比如说创建一个对象第一步需要申请一片内存空间,第二步初始化这个对象,第三步将这个对象指向这片内存空间,正常的顺序应该是1,2,3这样去执行,但是java编译器他在优化的时候可能会导致指令重排1,3,2这样的顺序去执行这样就会出问题,就可以用volatile这个关键字去避免这个问题。
volatile关键字特性
可见性
就是一个线程修改一个共享变量,其他线程也应该知道这个共享变量被修改之后的结果,这个就是可见性。可以用volatile关键字来保证变量的可见性,对于加了volatile的变量,线程在读取变量时会直接从主存中去读取,修改变量的时候同时会修改cpu告诉缓存和内存中的值。
- JMM
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
加 volatile 可以保证可见性:
import java.util.concurrent.TimeUnit;
public class JMMDemo {
// 不加 volatile 程序就会死循环!
// 加 volatile 可以保证可见性
private volatile static int num = 0;
public static void main(String[] args) { // main
new Thread(() -> {
// 线程 1 对主内存的变化不知道的
while (num == 0) {
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
有序性
- 多个线程执行的指令按顺序执行
- java编译器有时候进行编译优化的时候会进行指令的重排序,比如说饿汉式双重检验锁创建单例,创建一个对象第一步会先申请内存空间,第二步进行对象的初始化,第三步将这个对象指向这片内存空间,正常来说应该是按1,2,3的顺序执行指令,但是指令重排可能执行顺序会变成1,3,2,这样就会出问题,可能会返回一个未初始化的单例。解决办法是加volatile关键字,它是通过内存屏障的方式防止指令重排的。
双重检测锁单例模式不用volatile修饰会出现有序性问题。
public class LazyMan {
private LazyMan(){
}
private volatile static LazyMan lazyMan;
// 双重检测锁模式的 懒汉式单例DCL懒汉式
public static LazyMan getInstance(){
//提高效率,避免不必要的步骤
if (lazyMan==null){
synchronized (LazyMan.class){// lazyMan = new LazyMan() 不是一个原子性操作
//避免重复创建单例,可能存在多个线程通过了第一次判断等待锁释放
if (lazyMan==null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
/**
* 1. 分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 123
* 132 A
*
B // 此时lazyMan还没有完成构造
*/
不能保证原子性
// volatile 不保证原子性
public class VDemo02 {
// volatile 不保证原子性
private volatile static int num = 0;
public static void add() {
num++;
}
public static void main(String[] args) {
//理论上num结果应该为 2 万
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) { // main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
-
num++不是一个原子性操作
线程1在执行完第一步获得这个值之后进行线程切换线程2执行完了123三步num成功加1了,此时线程1被切换回来不会去获得最新的num值,因为之前已经获取过了不会去重新获取,它还会拿原来的num值。 -
如果不加 lock 和 synchronized,可以使用原子类,解决原子性问题。
public class VDemo02 {
// 原子类的 Integer
private volatile static AtomicInteger num = new AtomicInteger();
public static void add() {
// num++;
// 不是一个原子性操作
num.getAndIncrement(); // AtomicInteger + 1 方法, CAS
}
public static void main(String[] args) {
//理论上num结果应该为 2 万
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) { // main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
Runnable和Callable接口的区别
- Runnable不会返回结果和抛出异常,Callable会返回结果和抛出异常
如何创建线程池
- 用threadPoolExcutor构造方法创建(核心线程数,最大线程数,工作队列,生存时间、生存时间的单位、创建线程的工厂、拒绝策略)
- excutors工具类来实现,这里面有三种类型:1、固定数量的线程池,2、单个数量的线程池,3、可以根据实际情况调整数量的线程池(不安全)
创建线程的几种方式
-
继承Tread类,重写run方法,实例化这个子类就可以了,如果要启动线程可以使用start方法。
-
实现Runnable接口,重写run方法,当需要创建线程的时候将这个类实例作为Thread的构造方法参数传入,如果要启动线程调用start方法。
-
实现Callable接口,实现时要定义Callable泛型类型,表示最终返回的类型,重写call方法最终返回一个结果,当需要实例化线程的时候需要把Callable实现类的对象作为FutureTask的构造参数,FutureTask是Runnable和Future接口的实现类,实例化FutureTask后,作为Thread构造参数传入。如果线程执行完成想要获取线程执行结果,可以通过FutureTask的get方法获取。
-
用线程池
用threadPoolExcutor构造方法创建也可以用excutors工具类来实现,设定好核心线程数后,会一次性创建多个线程对象,创建好线程池后通过submit把线程需要执行的任务交给线程池。
线程池常用参数
三个最重要的参数:
- corePoolSize:当任务队列没满的时候最大可以同时运行的线程数量
- maximumPoolSize:最大线程数,核心线程数+辅助线程数
- workQueue:阻塞队列,新任务来了之后如果线程数量已经达到核心线程数就会被放在阻塞队列中。
为什么用阻塞队列,其他的结构不行吗?阻塞队列保证了在多线程的环境下,生产者往队列中提交任务和消费者消费队列中的任务是线程安全的。
ThrowsException:如果操作不能马上进行,则抛出异常
SpecialValue:如果操作不能马上进行,将会返回一个特殊的值,一般是true或者false
Blocks:如果操作不能马上进行,操作会被阻塞
TimesOut:如果操作不能马上进行,操作会被阻塞指定的时间,如果指定时间没执行,则返回一个特殊值,一般是true或者false
- ArrayBlockingQueue:基于数组实现的一个有界阻塞队列,我们必须在初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。生产者和消费者共用一把锁,所以不能同时操作这个队列。
BlockingQueue queue = new ArrayBlockingQueue(1024);
queue.put("1");
Object object = queue.take();
- LinkedBlockingQueue(无界队列):可以指定容量,如果不指定就是无界的,队列永远不会被放满,就不会有辅助线程。生产者和消费者各拥有一把锁,生产者和消费者可以同时操作这个队列。
BlockingQueue<String> unbounded = new LinkedBlockingQueue<String>();
BlockingQueue<String> bounded = new LinkedBlockingQueue<String>(1024);
bounded.put("Value");
//从队列中获取元素
String value = bounded.take();
- PriorityBlockingQueue:可以对指定的属性进行排序,确定任务的优先级,将优先级高的任务优先出队被线程消费。
其他参数:
5. keepAliveTime:存活时间,辅助线程销毁前等待的时间
6. unit:存活时间的单位
7. threadFactory:创建新线程的时候会用到
8. handler:拒绝策略
- AbortPolicy:默认抛异常
- CallerRunsPolicy:当线程池无法接受新任务的时候,会将任务回退到提交任务的线程中运行,就是由提交任务的线程去执行这个任务(main),这个策略可以避免任务丢失,但是可能会造成提交线程的执行速度变慢
- DiscardPolicy:直接丢弃
- 自定义拒绝策略:实现其中的
rejectedExecution(Runnable r, ThreadPoolExecutor executor)
方法来定义你自己的拒绝策略,可以灵活的处理,比如记录日志,回退到其他队列中。
线程池的核心线程数、最大线程数应该怎么设置
- CPU密集型任务:线程在执行任务的时候会一直利用CPU,对于这种情况应该尽量避免上下文切换,我们可以设置线程数为:CPU核数+1
- IO型任务:就是这个任务会花费大量的时间在IO上像sql的一些读写啊,文件的一些操作啊,但是CPU其实是不用花什么功夫的所以这种情况可以设置的线程数多一些,通常会设置成2*cpu的核数
在工作中可以进行压测,测出这个业务适合的一个线程数,如果是核心的业务核心线程数可以等于最大线程数,或者最大线程数大一些,如果是非核心的业务核心线程数可以设置为10,最大线程数可以设置成你压测的线程数
Java中常见的锁
- 乐观锁:一个线程操作的时候不会上锁,其他线程也可以操作,典型代表:CAS(比较和交换算法)
CAS原理:
- 创建内存中旧值副本
- 然后进行操作,计算最终需要设置的值
- 比较副本和内存中的值是否相等,如果相等的话修改内存中的值为自己刚刚操作过得到的结果,如果不相等就重新创建副本执行整个流程。
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 期望、更新
// public final boolean compareAndSet(int expect, int update)
// 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语!
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
atomicInteger.getAndIncrement();
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
底层是Unsafe类去实现的,它提供了硬件级别的原子操作。
- CAS中一直循环比较就是自旋锁
CAS会出现的问题: - 会导致ABA问题:
线程1从内存中取出A,线程2也从内存中取出A,线程2进行了一些操作变成了B,然后又变成了A,线程1执行完之后发现内存中还是A他就以为没有发生改变,然后他的操作就成功了,过程中其实是有其他线程进行修改的,就存在了并发问题。
举例:在网上支付的时候如果第一次支付因为网络原因没有立刻支付成功,用户又点了一次支付,此时就有两个线程去执行支付操作,第二次支付成功了,但是此时用户账户恰好又进账了相同的数目,网络通畅了,第一次支付又继续执行发现账户余额和取出时是一样的,那么此时会再扣一次钱。
解决方式:
可以在每次执行数据修改的时候带上版本号,当版本号和数据的版本号一致的时候才能进行修改。
public class CASDemo {
//AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
// 正常在业务操作,这里面比较的都是一个个对象
static AtomicStampedReference<Integer> atomicStampedReference = new
AtomicStampedReference<>(1, 1);
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println("a2=>" + atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1));
System.out.println("a3=>" + atomicStampedReference.getStamp());
}, "a").start();
// 乐观锁的原理相同!
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
System.out.println("b2=>" + atomicStampedReference.getStamp());
}, "b").start();
}
}
用数字举例会有问题
-
忙等待
如果并发的冲突比较大的话就会导致CAS一直在不断的重复执行,进入忙等待。 -
一次性只能保证一个共享变量的原子性
- 悲观锁:一个线程使用悲观锁时,其他线程只能等待锁释放才能操作,典型代表有synchronized关键字。
- 内置锁:Java语言自己带的一个锁的相关机制,synchronized
- 显示锁:Lock,需要手动的调用lock方法上锁,unlock方法解锁
synchronized和Lock的区别:
- synchronized是关键字,修饰方法、代码块,Lock是接口
- synchronized自动加锁、解锁,Lock需要手动的调用lock方法上锁,unlock方法解锁
- Lock可以判断是否上锁成功,tryLock方法
- synchronized碰到没有处理的异常会自动解锁不会出现死锁。Lock不会自动解锁可能会死锁。所以写Lock锁时都是把解锁放入到finally{}中。
- 重入锁和非重入锁
拿到外面的锁会自动拿到里面的锁(synchronized和Lock都是这样):
public class Demo02 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(() -> {
phone.sms();
}, "A").start();
new Thread(() -> {
phone.sms();
}, "B").start();
}
}
class Phone2 {
Lock lock = new ReentrantLock();
public void sms() {
lock.lock(); // 细节问题:lock.lock(); lock.unlock(); // lock 锁必须配对,否则就会死在里面
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "sms");
call(); // 这里也有锁
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
lock.unlock();
}
}
public void call() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "call");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
某个线程已经获得了某个锁,允许再次获得锁就是重入锁,不允许再次获得就是非重入锁。Java中的synchronized和ReentrantLock都是重入锁。
可重入锁的原理就是计数器,当一个线程持有某个锁数量就会加1,当线程再次获得锁时数量又会加1(如果是不可重入锁发现锁数量为1的话就不能再次获得锁了),当释放锁的时候会对锁数量减1直到减到0,表示完全释放了这个锁。
- 公平锁金和非公平锁
公平锁:严格按照排队执行,先排队先执行,先来的先执行。
非公平锁:不会去排队直接竞争,谁竞争成功谁获得锁。(一般用这个,随机性–>公平)
ReentrantLock构造方法可以选择是公平锁还是非公平锁,默认非公平锁。
- 读锁(共享锁)、写锁(排它锁)
读锁:多个读锁可以共同执行。
写锁:一个写锁进行执行,其他的锁线程必须等待。
死锁
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new MyThread(lockA, lockB), "T1").start();
new Thread(new MyThread(lockB, lockA), "T2").start();
}
}
class MyThread implements Runnable {
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() +
"lock:" + lockA + "=>get" + lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() +
"lock:" + lockB + "=>get" + lockA);
}
}
}
}
解决死锁:查看堆栈信息找到死锁问题