创建线程的几种方式
-
继承Thread类,重写run方法
-
实现Runnable接口,实现run方法
-
实现Callable,实现call方法,配合FutureTask获取线程返回结果
-
通过ThreadPoolExecuter线程池获取线程资源
这几种方法的底层都是Runnable,Thread是Runnable接口的实现类,Callable配合FutureTask使用,是Runnable的实现类,线程池提供的工作线程也是Runnable的实现类
线程状态
这个问题可以从两个方面回答:
-
新建 → 【就绪|运行】 → 等待 → 结束 五种状态,这个是操作系统级别的线程状态
- 新建:
new Thread()
,创建线程对象后在启动线程之前为新建状态 - 就绪:
thread.start()
,启动线程后线程会进入就绪状态,因为线程的执行是需要CPU调度的,所以会先进入就绪状态 - 运行:CPU调度时为运行状态,CPU不可能一直停留在当前线程,所以该阶段会在【就绪|运行】状态中反复切换
- 等待:
Object.wait()
thread.join()
Thread.sleep()
,当线程由于某些原因暂停运行时为等待状态 - 结束:线程工作结束
- 新建:
-
新建 → 运行→ 【阻塞|等待|超时等待】 → 结束 六种状态,这个是Java线程枚举类中规定的几种状态
- 新建:同上
- 运行:在Java中执行
thread.start()
方法后就属于运行状态,不考虑CPU调度的问题 - 阻塞:BLOCKED,在获取到线程锁之前无法执行目标代码,此时线程处于阻塞状态
- 等待:WAITING,例如
Object.wait()
让线程进入等待状态,需要其他线程唤醒后才可继续执行 - 超时等待:TIMED_WAITING,例如
Thread.sleep()
让线程进入睡眠状态,规定时间后自动唤醒
停止线程的方法
-
可以通过
Thread.stop()
方法停止线程,不过由于该方法过于暴力执行后强制停止线程的运行,可能会导致一些不可预计的问题(例如资源未释放),虽然有效但目前已被废弃不推荐使用 -
可以在线程对象中维护个成员变量作为标志位,通过循环判断该标志位的状态,如果标志位被修改就停止线程运行,该方法可以在停止线程前进行一些善后工作
-
可以使用
thread.interrupt()
方法结束线程,该方法不同于Thread.stop()
强制停止,而是类似于第二种方法底层维护一个boolean类型的变量,通常来说interrupt需要配合sleep、wait、join等方法响应停止才会产生效果,可以通过thread.isInterrupted()
方法查看标志位的值,默认为false,为true即为停止状态
wait方法和sleep方法的区别
-
wait方法是Object对象提供的方法,而sleep是Thread提供的静态方法
-
wait方法是线程等待状态,需要其他线程唤醒后才可以继续执行,sleep方法是超时等待状态,对应时间结束后会自动唤醒并继续执行
-
wait方法必须在线程持有锁时才能执行,而sleep无论是否持有锁都可以执行
-
wait方法等待时会释放锁资源供其他线程使用,而sleep会带着锁一起进入等待状态
Java中都有哪些锁
-
重入锁与非重入锁
- 重入锁:重入锁又叫递归锁,指在已经拥有A锁的情况下执行另一处需要A锁的代码,可以顺利执行不会被阻塞,底层会记录对该锁的引用次数,Java中提供的syncronized、ReentrantLock,ReentrantReadWriteLock都属于重入锁
- 非重入锁:非重入锁是指在已经拥有A锁的情况下执行另一处需要A锁的代码,被阻塞无法执行,必须释放后重新获取才可继续执行,目前Java并没有提供可直接使用的非重入锁
-
乐观锁与悲观锁
- 悲观锁:获取不懂锁资源时线程会阻塞,等待其他线程释放锁后在尝试去获取锁
- 乐观锁:获取不到锁资源时会再次获取,直至获取成功
-
公平锁与非公平锁,假设有个任务列表,线程A和线程B负责执行:
- 非公平锁:线程A和线程B竞争,线程A获取到了锁资源执行业务,线程B等待,A执行完成释放锁后会重新参与竞争,可能会多次连续获取到锁,导致B线程始终等待,这种就属于非公平锁,线程B的状态又被称为饥饿线程
- 公平锁:线程A和线程B竞争,线程A获取到了锁资源执行业务,线程B等待,A执行完成释放锁后会排队重新参与竞争,等B释放锁后才会轮到A获取锁,保证任务的公平分配
-
互斥锁与共享锁,指的的ReentrantReadWriteLock
- 互斥锁:互斥锁又被称为独占锁,只能有一个线程获取该锁并执行任务
- 共享锁:共享锁可以被多个线程获取并执行代码,通常需要配合互斥锁一起使用,读写锁就是经典案例
synchronized和Lock的区别
-
syncronized是Java中提供的关键字,可以作用在方法上,也可以在代码中直接使用,在代码中直接使用为对象锁,需要手动维护一个锁对象,作用在成员方法上会以该对象为锁,作用在静态方法上会以该类为锁;而Lock锁是Java提供的类,需要new对象后使用
-
syncronized锁的作用范围是大括号,无论是类锁还是对象锁都是基于这个括号自动获取和释放锁,而Lock锁需要手动new对象后通过
lock.lock()
获取锁,然后通过lock.unlock()
释放锁,还可以获取锁的状态,以及超时获取锁等等,相比syncronized更灵活方便 -
syncronized锁默认为独享的非公平锁,而Lock锁是接口类,下面有很多实现,可以实现独享锁共享锁,以及公平锁和非公平锁,功能更强大
-
底层实现原理不同,技术太浅后面学习JVM后再深入了解
Java中提供了哪些线程池
-
fixedThreadPool
是定长的线程池,构建该线程池需要传入一个int值分别作为核心线程数和最大线程数 -
singleThreadExecutor
是单线程池,该线程池中有且仅有一个工作线程,多出的任务都会存在任务队列中 -
cachedThreadPool
是缓存线程池,正常流程是线程池接收到任务后交给线程处理,没有空闲线程时会存储到任务队列,该线程池没有核心线程,任务是直接进入队列然后初始化工作线程去处理,且工作线程最大数量没有限制,为Integer.MAX_VALUE
-
scheduleThreadPool
是定时线程池,它可以让任务等待至规定时间后执行,也可以让某一个任务周期性执行 -
workStealingPool
不同与前几个线程池,前几个都是多个线程从队列中获取任务,该线程池中每个线程都有一个自己的队列,底层使用了Fork/Join模式,当有空闲线程时时会帮助其他线程分担任务
为什么不推荐使用Java提供的线程池
因为Java提供的线程池的参数配置都是固定的,这些线程池对于任务数量和线程数量并没有做合适的限制,任务数量过多时可能会导致堆内存溢出,而线程数量过多时可能会导致栈内存溢出,一般推荐自定义线程池
构建线程池的参数列表主要有哪些
Java提供的线程池底层都是通过ThreadPoolExecutor
构建出来的,核心参数包含:
-
int corePoolSize
核心线程池数,该线程不会被回收 -
int maximumPoolSize
最大线程数,当队列中任务堆积时会动态创建线程进行工作,最大线程数不得超过这里的规定值 -
long keepAliveTime
线程超时时间,当动态创建的线程超过规定时间没有工作时就会被回收 -
TimeUnit unit
线程超时时间的单位 -
BlockingQueue<Runnable> workQueue
任务队列 -
ThreadFactory threadFactory
线程工厂,负责创建线程 -
RejectedExecutionHandler handler
线程池对于任务的拒绝策略
线程池的任务拒绝策略有哪些
-
AbortPolicy
:当线程池无法处理任务时,直接抛出异常 -
CallerRunsPolicy
:当线程池无法处理任务时,将任务交给调用方处理 -
DiscardPolicy
:当线程池无法处理任务时,直接将任务丢弃 -
DiscardOldestPolicy
:当线程池无法处理任务时,将队列中最早的任务丢掉,加入当前任务