java线程
一、线程基本属性
1、线程的6种状态
NEW(新建):还没有调用start()开启线程实例所处的状态
RUNNABLE(运行):正在虚拟机中执行或者等待被执行的线程所处的状态,但是这种状态也包含线程正在等待处理器资源这种情况
BLOCKED(阻塞):等待在监视器锁上的线程所处的状态,比如进入synchronized同步代码块或同步方法失败
WAITING(等待):等待其它线程执行特定操作的线程所处的状态;
TIMED_WAITING(超时等待):等待其它线程执行超时操作的线程所处的状态
TREMINATED(结束):退出线程所处的状态
二、创建线程的4种基本方式
1、继承Thread类
重写run方法
优点:实现简单,只需要实例化继承类的实例,即可使用线程
缺点:扩展性不足,如果一个类已经继承其它的类,就无法通过这种方式自定义线程。
public class Mythread extends Thread{
@Override
public void run(){}
}
public class test{
Thread thread = new Thread();
thread.start();
}
2、实现Runnable接口
优点:
1.扩展性好,可以在此基础上继承其它类,实现其它必须的功能
2.对于多线程共享资源的场景,具有天然的支持,适用于多线程处理一份资源的场景
缺点:构造线程实例的过程相对繁琐一点
public class MyRunnable implements Runnable{
@Override
public void run(){
Sysout.out.print("线程1");
}
public static void main(String[] args){
//线程执行的目标对象
MyRunnable myRunnable = new MyRunnable();
//实际的线程对象
Thread thread = new Thread(myRunnable);
//启动线程
thread.start();
}
}
3、实现Callable接口
优点:
1.扩展性好
2.支持多线程处理同一份资源
3.具备返回值以及可以抛出受检查的异常
缺点:
相对于实现Runnable接口的实现方式,较为繁琐
public class MyCallable implements Callable<String>{
@Override
public String call() throws Exception{
return "这是有一个线程";
}
public static void main(String[] args){
//线程执行目标
MyCallable myCallable = new MyCallable();
//包装线程的执行目标,因为Thread的构造函数只能接受Runnable接口的实现类,而FutureTask类实现了Runnable接口
FutureTask<String> futureTask = new FutureTask<>(myCallable);
//传入线程执行目标,实例化线程对象
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
String result = null;
try{
}catch(InterruptedException e){
e.printStackTrace();
}catch(ExecutionException e){
e.printStackTrace();
}
Sysout.out.println(result);
}
}
4、内部类的方式
优点:
1.相比于前3中方法代码更加简洁,使用更加方便
缺点:
1.每次new Thread新建对象性能差
2.线程缺法统一管理,可能无限制创建新的线程,相互竞争,很有可能会占用过多的系统资源导致死机或oom
3.缺乏更多的功能如定期执行、定时执行、线程中断
//1.匿名内部类实现方式
new Thread(){
//重写run方法
@Override
public void run(){
Sysout.out.print("线程执行中");
}
}.start();
//2.线程接口runnable实现方式
Runnable r = new Runnable(){
Person p = new Person("某人");
@Override
public void run(){
Sysout.out.print("线程执行中");
}
}
new Thread(r).start();
//3.简化Runnable接口的实现方式
new Thread(new Runnable(){
@Overable
public void run(){
Sysout.out.print("线程执行");
}
}).start();
5、小结
1、采用实现Runnable、Callable接口的方式创建多线程
优势:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
2、使用继承Thread类的方式创建多线程
优势:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势: 线程类已经继承了Thread类,所以不能再继承其他父类。
3、Runnable和Callable的区别
(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3) call方法可以抛出异常,run方法不可以。
(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果future.get()。
三、4种线程池
1、Java通过Executors提供四种线程池
线程池 | |
---|---|
newCachedThreadPool | 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 |
newFixedThreadPool | 创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。 |
newScheduledThreadPool | 创建一个定长线程池,支持定时及周期性任务执行。 |
newSingleThreadExecutor | 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 |
2、创建线程池
(1)newCachedThreadPool:
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。示例代码如下:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
log.info(index);
}
});
}
(2)newFixedThreadPool:
需要指定线程池的大小,创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache示例代码如下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
log.info(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
(3)newScheduledThreadPool:
创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
log.info("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
表示延迟3秒执行。
定期执行示例代码如下:
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
log.info("delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS);
表示延迟1秒后每3秒执行一次。
ScheduledExecutorService比Timer更安全,功能更强大
(4)newSingleThreadExecutor:
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
log.info(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
结果依次输出,相当于顺序执行各个任务。
3、线程池的作用
程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
4、为什么要用线程池
(1)减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
(2)可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
(3)Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的
线程池接口是ExecutorService。
5、比较重要的几个类:
ExecutorService: 真正的线程池接口。
ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutor: ExecutorService的默认实现。
ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。
6、使用线程池和普通创建线程的区别
a. 每次new Thread新建对象性能差。
b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom(out of memory)。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,线程池的好处在于:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。
7、线程池参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
(1)corePoolSize:核心线程数
线程池中的常驻核心线程数,当一个任务提交到线程池时,线程池会创建一个线程来执行任务,即使空闲的线程数能够执行新任务也会创建新的的线程,直到执行任务数大于核心线程数时就不会再创建。如果调用线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程;
(2)maximumPoolSize:最大线程数
线程池允许创建的最大线程数,此值大于等于1;可以创建出超过核心线程数量的线程,在keepAliveTime空闲时间时终止;
(3)keepAliveTime:空闲时间
多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁 ,直到剩下corePoolSize个线程为止。默认情况下,只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,知道线程中的线程数不大于corepoolSIze,也可以手动设置到核心线程上;
(4)TimeUnit:时间单位
keepAliveTime的单位;
(5)runnableTaskQueue:任务队列
任务队列,被提交但尚未被执行的任务
(6)ThreadFactory:设置创建线程的工厂
表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可;
(7)Handler:拒绝策略
表示当线程队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略
Abort策略:
默认策略,新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。
CallerRuns策略:
为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务。
Discard策略:
新提交的任务被抛弃。
JDK内置的拒绝策略
当线程池中corePoolSize使用完毕之后,多出来的任务就会进入缓存队列;
当缓存队列也存放满了之后,还有大量的任务,这时将会创建线程池中支持的最大量的线程,然后执行缓存队列里面的任务,而外面的任务进入缓存队列中;当线程池中的线程达到最大时,缓存队列也存满之后,这时该线程池将会执行拒绝策略。
8、小结
线程池的优点:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。
四、线程并发
并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。
并行:是说在单位时间内多个任务同时在执行 。
在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。
五、线程锁
(1)枷锁目的
解决并发导致的共享资源错乱,避免多个线程同时对一个共享资源进行操作,保证公共资源的唯一性、正确性和真实性,使得同一时段内只有一个线程对公共资源进行操作;
(2)锁的种类
互斥锁:加锁失败后,线程会释放cpu,给其它线程,性能损耗大;
自旋锁:加锁失败后,线程会忙等待,直到它拿到锁,性能损耗小;
读写锁:写锁是独占锁,因为任何时刻只能有一个现场持有写锁,类似互斥锁和自旋锁;
悲观锁:多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁;
乐观锁:先修改后检查,修改后检查到没有冲突则操作完成,否则操作放弃,乐观锁全程并没有加锁,所以也叫做无锁线程,虽没有加锁但是一旦发生冲突,重试成本非常高;
注释:不管哪种锁,加锁的代码范围应该尽可能的小,减小颗粒度,提高执行速度;
(3)常用的加锁方式
synchronized关键字