- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于
可运行线程池中,变得可运行,等待获取CPU的使用权。
3 .运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进
入就绪状态,才有机会转到运行状态。
- 死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。
2.阻塞的情况又分为三种:
(1)、等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待
池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤
醒,wait是object类的方法
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放
入“锁池”中。
(3)、其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状
态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
sleep是Thread类的方法
3. wait 和 sleep 的区别
共同点
- wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
-
方法归属不同
-
- sleep(long) 是 Thread 的静态方法
- 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
- sleep(long) 是 Thread 的静态方法
-
醒来时机不同
-
- 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
- wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
- 它们都可以被打断唤醒
- 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
-
锁特性不同(重点)
-
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
- 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
4. volatile的作用和原理
4.1 JMM内存模型
JMM让java程序与硬件指令进行了隔离
由于JVM运行程序的实体是线程,创建每个线程时,java 内存模型会为其创建一个工作内存(我们一般称为栈),工作内存是每个线程的私有数据区域。
Java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。
但线程对变量的操作(读取、赋值等)必须在工作内存中进行。因此首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写会主内存中。
4.2 Java并发编程要解决的三个问题(三大特征)
原子性
一个线程在CPU中操作不可暂定,也不可中断,要不执行完成,要不不执行
内存可见性
默认情况下变量,当一个线程修改内存中某个变量时,主内存值发生了变化,并不会主动通知其他线程,即其他线程并不可见
有序性
程序执行的顺序按照代码的先后顺序执行。
4.3 Volatile
volatile帮我们解决了:
内存可见性问题
指令重排序问题
不能保证变量操作的原子性(Atomic)
- 被volatile修饰的共享变量对所有线程总是可见的,也就是当一个线程修改了一个被volatile修
饰共享变量的值,新值总是可以被其他线程立即得知。(会主动通知)
我们可以通过如下案例验证
import java.util.Date;
public class MyData {
private boolean flag=false;
public void setFlag(boolean flag) {
this.flag = flag;
}
public boolean isFlag() {
return flag;
}
public static void main(String[] args) throws Exception {
MyData myData = new MyData();
// 线程1 修改值
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 子线程3s 后 修改为true
myData.setFlag(true);
}
}).start();
System.out.println(new Date());
while (!myData.isFlag()){
// (如果不用 volatile) 理论上 3s 这里的死循环会结束,但是 实际上3s 后主线程一直在死循环
// 如果不用 volatile 主线程并没有感知到 子线程修改了变量
}
System.out.println("已经被修改了"+new Date());
}
}
注意: volatile 并不保证线程安全的,即多个线程同时操作某个变量依旧会出现线程安全问题
如下案例
public class MyData {
private volatile int number=1;
public void addNum(){
number++;
}
public static void main(String[] args) {
MyData myData = new MyData();
// 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
for (int i=0; i<20; i++) {
new Thread(() -> {
for (int j=0; j<1000; j++) {
myData.addNum();
}
}).start();
}
// 程序运行时,有主线程和垃圾回收线程也在运行。如果超过2个线程在运行,那就说明上面的20个线程还有没执行完的,就需要等待
while (Thread.activeCount()>2){
Thread.currentThread().getThreadGroup().activeCount();
Thread.yield();// 交出CPU 执行权
}
System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
}
}
- 禁止指令重排序优化。
int a = 0;
bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,
再到线程1,这时候a才赋值为2,很明显迟了一步。
但是用 flag 使用 volatile修饰之后就变得不一样了
使用volatile关键字修饰后,底层执行时会禁止指令重新排序,按照顺序指令
5.为什么用线程池?解释下线程池参数?
1、降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
2、提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
/*
corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会
消除,而是一种常驻线程
maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程
数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但
是线程池内线程总数不会超过最大线程数
keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会
消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过keepAliveTime 、
unit 表示超出核心线程数之外的线程的空闲存活时间,
也就是核心线程不会 setKeepAliveTime 来设置空闲时间
workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放
入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建
工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择
自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这
时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程
池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提
交的任务时,这是也就拒绝
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
java 中常见的几种线程池
// 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,
//若无可回收,则新建线程。
Executors.newCachedThreadPool();//
//创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
Executors.newFixedThreadPool(10);
//创建一个定长线程池,支持定时及周期性任务执行。
Executors.newScheduledThreadPool(10);// 核心线程数10
//创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,
//保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
Executors.newSingleThreadExecutor();
//看源码,解释一下这三个创建线程池方法的作用
Executors.newFixedThreadPool(1);
Executors.newCachedThreadPool();
Executors.newSingleThreadExecutor();
6.项目中线程池的使用?
- tomcat 自带线程池
- CompletableFuture 创建线程时指定线程池,防止创建线程过多
CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(()->{
result.setCluesNum(reportMpper.getCluesNum(beginCreateTime, endCreateTime, username));
return null;
},指定线程池);
案例:CompletableFuture异步和线程池讲解 - 不懒人 - 博客园
7 synchronized
synchronized 锁释放时机
● 当前线程的同步方法、代码块执行结束的时候释放
-
正常结束
-
异常结束出现未处理的error或者exception导致异常结束的时候释放
● 程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁
8. Sychronized和ReentrantLock的区别
- sychronized是⼀个关键字,ReentrantLock是⼀个类
- sychronized的底层是JVM层⾯的锁(底层由C++ 编写实现),ReentrantLock是API层⾯的锁 (java 内部的一个类对象)
- sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
- sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
注: 假设多个线程都要获取锁对象,满足先等待的线程先获得锁则是公平锁,否则是非公平锁
- sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识
来标识锁的状态
- sychronized底层有⼀个锁升级的过程(访问对象线程数由少到多,竞争由不激烈到激烈,底层会通过一种锁升级机制 无锁->偏向锁->轻量级锁->重量级锁,保证性能) ,会使用自旋 线程频繁等待唤醒会浪费性能,特别是锁的获取也许只需要很短的时间 ,不限于等待,直接执行简单代码while(true)执行完抢锁 来优化性能
代码演示
可重入演示
public static void main(String[] args) {
// 可重入锁演示
save();
}
public synchronized static void save() {
System.out.println("save");
update();
}
public synchronized static void update() {
System.out.println("update");
}
ReentrantLock 使用演示
public class TestDemo {
public static void main(String[] args)throws Exception {
ReentrantLock lock = new ReentrantLock();
// 线程1
new Thread(()->{
lock.lock(); // 加锁
add();
lock.unlock();// 解锁
}).start();
// 线程2
new Thread(()->{
lock.lock(); // 加锁
add();
lock.unlock();// 解锁
}).start();
Thread.sleep(3000);
System.out.println(i);
}
static int i =0;
public static void add() {
i++;
}
}
公平/非公平锁演示
package com.huike;
import java.util.concurrent.locks.ReentrantLock;
public class TestDemo {
public static void main(String[] args)throws Exception {
ReentrantLock lock = new ReentrantLock(false);
// 线程1
new Thread(()->{
lock.lock(); // 加锁
add();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();// 解锁
},"t1").start();
// 线程2
new Thread(()->{
lock.lock(); // 加锁
add();
lock.unlock();// 解锁
},"t2").start();
new Thread(()->{
lock.lock(); // 加锁
add();
lock.unlock();// 解锁
},"t3").start();
for (int j = 0; j < 100000; j++) {
new Thread(()->{
lock.lock(); // 加锁
add();
lock.unlock();// 解锁
}).start();
}
Thread.sleep(30000);
System.out.println(i);
}
static int i =0;
public static void add() {
i++;
System.out.println(Thread.currentThread().getName()+"获得了锁");
}
}
10. 悲观锁 vs 乐观锁
要求
- 掌握悲观锁和乐观锁的区别
对比悲观锁与乐观锁
-
悲观锁的代表是 synchronized 和 Lock 锁
-
- 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
- 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
- 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
- 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
-
乐观锁的代表是 AtomicInteger AtomicStampReference,使用 cas 来保证原子性
-
- 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
- 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
- 它需要多核 cpu 支持,且线程数不应超过 cpu 核数
- 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
12. ConcurrentHashMap的原理
12.1 JDK1.7
- 数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
- 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了(默认16,可以指定)
- 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
- 其他Segment首次创建小数组时,会以Segment[0] 为原型为依据,数组长度,扩容因子都会以原型为准
12.2 JDK1.8
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
2.1 JDK1.7
- 数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
- 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了(默认16,可以指定)
- 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
- 其他Segment首次创建小数组时,会以Segment[0] 为原型为依据,数组长度,扩容因子都会以原型为准
12.2 JDK1.8
[外链图片转存中…(img-X0ZM3Fsz-4701981442954)]
[外链图片转存中…(img-06R0Ksde-4701981442955)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!