文章目录
- 线程有那些状态?
- 一个程序来显示状态切换过程
- 正常执行流程
- 阻塞执行流程
- 等待执行流程
- 说说线程池的核心参数
- wait和sleep的区别
- Lock和synchronized的区别
- Lock中Condition的使用
- 说说Java中的悲观锁与乐观锁
- 乐观锁
- 悲观锁
- Hashtable和ConcurrentHashMap的区别?
- 【Java面试】谈一谈你对ThreadLocal的理解
线程有那些状态?
操作系统层面将线程状态分为5种,分别是:新建,就绪,运行,阻塞,死亡
但是在Java的Thread类中,线程的状态枚举如下:
也就是Java将线程分为了六种状态:创建,可运行,阻塞,等待,限时等待和终结
新建状态指的是我们使用new方法新建出来一个线程对象的时候,此时Java还没有将其与OS关联起来,那么这个线程就不会被分配CPU去执行代码,只有调用了start方法之后才会与真正的线程关联起来,此时就从新建状态变为了可运行(就绪)状态,等待分配CPU去执行任务,如果CPU分配给了当前线程,当前线程就会执行对应的任务,任务执行完毕之后,当前线程死亡(终结),注意是死亡,此时这个任务对应的线程以及资源都会被释放。
而并不是所有的任务都会被执行,因此会有部分任务进行阻塞,例如多线程访问同一个锁资源的时候,如果当前线程没有得到锁,那么当前线程就需要进行阻塞,此时CPU被分配给得到锁的线程去执行任务,任务执行完毕之后,锁释放,线程再次去争夺锁,得到锁的话就可以从阻塞状态变为可运行状态,此时只需要等待CPU分配资源就可以运行了。
而当前持锁线程可能执行任务后发现有些条件不满足,那么这个线程不能总是占有锁,因此可以选择调用wait方法去释放锁,然后让自己进入等待状态去等待条件的满足,此时其他线程就可以去竞争锁从而执行他们的任务,等到条件满足,就可以由另一个线程调用notify方法去提醒这个线程再次竞争锁去完成任务。
最后一种是等待的一种情况,叫做现时等待,也就是这次等待可以设定时间。
方法是使用wait方法并且设定等待的时间,再次之前如果时间到了或者另一个线程调用了notify方法,那么这个线程就会被唤醒。
还有一种是sleep方法,这种方法和wait不一样,sleep是不会释放当前线程的锁的,其他线程都得陪着这个线程等,这个线程等待完毕之后在继续执行接下来的任务,调用了sleep方法相对于是告诉CPU你现在先不用执行我的任务了,你可以先去干其他的,相当于让出CPU资源。
一个程序来显示状态切换过程
正常执行流程
首先来两个断点,一个断点打在主线程上,另一个断点打在分支上。
并且设定触发断点的条件为Thread,这样子我们进入debug状态之后就可以以线程作为操控条件了。
主线程断点设置在了最后一句,所以此时前面的语句都已经执行完毕了,但是由于还有一个断点打在了分支线程里面,所以如果我不让他执行,他就得等待
现在切换到分支线程执行语句
此时分支线程的任务已经执行完毕了,那么只有主线程还有任务了。
让主线程执行断点处的语句
可以发现在没有阻塞的情况下,线程状态为NEW->RUNNABLE->TERMINATED
这几个状态都是Thread类中定义的枚举类型。
阻塞执行流程
要让线程阻塞,就让他获取锁失败就行了
首先先创建线程,然后开启线程,但是不让线程执行任务
开启任务之后,先让分支线程执行一下,然后让他触碰到获取锁的位置但是先不执行,然后让主线程去执行获取锁的方法,然后再切换到分支线程去获取锁,此时分支线程获取锁失败,那么再让主线程打印分支线程的状态,此时的状态就是阻塞了。
之后主线程释放锁,分支线程继续执行任务,分支线程变为RUNNABLE状态,然后执行完成任务后死亡。
等待执行流程
大致流程也是差不多的,先让分支线程获取到锁,然后此时分支线程调用wait方法把锁让出去,那么主线程就能得到锁,然后查看此时分支线程的状态,主线程把分支线程唤醒之后,分支线程的状态从WAITTING变为了BLOCKING,因为此时分支线程要继续执行任务的话就需要获得锁。
得到锁之后继续重新执行任务,此时主线程发现分支线程的状态就是RUNNABLE了,然后分支线程执行完毕任务,死亡。
说说线程池的核心参数
线程池的执行流程也就是主线程(业务线程)提交任务到线程池之后,任务的处理流程。
下面来看execute方法
这个方法首先判断要执行的任务是否是空,如果是,返回一个空指针异常。否则,判断当前的工作线程数,如果工作线程数小于核心线程数,那么直接创建一个工作线程执行任务,如果不是,那么判断当前工作队列是否不满,如果是,那么将这个任务放入到工作队列中,如果不是,判断当前是否还有可用的非核心线程(判断maximumPoolSize-corePoolSize>0)即工作线程数是否大于最大线程数,如果不是,那么创建一个非核心线程来执行当前任务,如果是,那么就执行拒绝策略。流程图如下:
wait和sleep的区别
- 共同点:
wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态 - 方法归属不同
sleep(long)是 Thread的静态方法
而wait(),wait(long)都是Object的成员方法,每个对象都有,因此每一个对象都可以作为锁来调用这个方法 - 醒来时机不同
执行 sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
wait(long)和 wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去
它们都可以被打断唤醒 - 锁特性不同
wait方法的调用必须先获取wait对象的锁,而sleep 则无此限制
wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃,但你们还可以用),而sleep如果在 synchronized代码块中执行,并不会释放对象锁(我放弃,你们也用不了)
Lock和synchronized的区别
- 语法层面
synchronized是关键字,源码在jvm 中,用c++语言实现
Lock是接口,源码由jdk 提供,用java语言实现
使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁 - 功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock提供了许多synchronized不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock - 性能层面
在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不赖
在竞争激烈时,Lock的实现通常会提供更好的性能
Lock中Condition的使用
说说Java中的悲观锁与乐观锁
1.悲观锁的代表是synchronized和Lock锁
- 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
- 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
- 实际上,线程在获取synchronized和Lock锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
2.乐观锁的代表是Atomiclnteger,使用cas来保证原子性
- 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
- 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
- 它需要多核cpu支持,且线程数不应超过cpu核数
乐观锁
乐观锁的典型操作就是CAS(compare and set)
这里按照乐观锁的要求,不加锁,每次失败的时候都重试,直到成功
当然,如果想要进行原子操作,需要保证共享变量的可见性,所以一般需要要求变量是volatile类型的。
public class SyncVsCas {
static final Unsafe U = Unsafe.getUnsafe();
//得到偏移位置
static final long BALANCE = U.objectFieldOffset(Account.class,"balance");
static class Account{
volatile int balance=10;
}
public static void main(String[] args) {
Account account = new Account();
while (true) {
int o = account.balance;
int n = o+5;
//第一个参数为:要修改的变量是那个对象的 第二个是偏移量
//第三个参数为旧值 第四个参数为新值
//这个方法会把o值和我们共享变量的值进行比对 如果一样那么就修改
if(U.compareAndSetInt(account, BALANCE, o, n)){
//原子性的
break;
}
}
System.out.println(account.balance);
}
}
悲观锁
悲观锁就是很典型的使用synchronized
public static void sync(Account account){
Thread t1 = new Thread(()->{
synchronized (account){
int o = account.balance;
int n = o-5;
account.balance=n;
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (account){
int o = account.balance;
int n = o+5;
account.balance=n;
}
},"t2");
t1.start();
t2.start();
System.out.println(account.balance);
}