概述
- 线程就是进程中的一个独立单元,线程控制着进程的执行,一个进程中至少有一个线程。
什么是进程呢?
- 进程是程序运行的最小单元,表示正在运行中的程序,每个进程都有一个执行顺序,也就是执行路径,或者叫控制单元。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。
多线程的创建
- 继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
}
}
- 实现Runnable接口
public class MyThread implements Runnable{
@Override
public void run() {
}
}
- 实现Callable接口
public class MyCallable {
Callable<String> callable = new Callable<String>() {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
@Override
public String call() throws Exception {
return "hi callable";
}
};
public void test(String[] args) {
FutureTask futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();//启动线程
//取消线程
futureTask.cancel(true);
}
}
Thread类中的start()方法和run()方法有什么区别?
- 调用start()方法会创建一个新的线程去实质性我们的代码,如果直接使用run()方法,它的行为就和普通的方法一样,还是在当前线程中执行我们的代码。
用Runnable还是Thread?
- 我们知道继承Thread类和实现Runnable接口都可以创建一个线程,那么使用哪种方式更好呢?因为Java不支持多继承,但是支持多实现,所以当然是实现Runnable接口更好
Runnable和Callable有什么不同?
- Runnable是重写run方法,Callable是重写call方法,另外call有返回值,并且支持抛出异常,Runnable没有这些功能。但是在新的线程中new Thread(Runnable r)类只支持Runnable,不支持直接使用Callable。我们可以把Callable存储在FutureTask()中,然后将task传入Thread()。是因为FutureTask实现了RunnableFuture这个接口。它等价于可以携带结果的Runnable,并且有三个状态:等待、运行和完成。完成包括所有计算以任意方式结束,包括正常结束,取消和异常结束。
volatile关键字
- volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程缺少同步类的情况下,多线程对同步变量操作对其他线程是透明的,volatile变量可以保证下一个读取操作会在前一个写操作之后发生。线程都会直接从内存中读取该变量,并且不再缓存它。这就确保了线程读到的变量是同内存一致的。所以volatile是用来解决可见性问题的。
ThreadLocal
- 什么是ThreadLocal?
- ThreadLocal类提供了线程本地变量,这些变量不同于普通变量,访问线程本地变量的每个线程都有其自己独立初始化的变量副本,因此ThreadLocal没有多线程竞争的问题,不需要单独进行加锁
- 使用场景
- 每个线程都需要有数据自己的实例数据(线程隔离)
- 框架跨层数据的传递
- 需要参数全局传递的复杂调用链路的场景
- 数据库连接的管理,在AOP的各种嵌套中调用保证事务的一致性
多线程的优缺点
优点
- 可以提高程序的执行效率
- 提高了CPU,内存等资源利用率
缺点
- 占用内存空间
- CPU开销增大
- 程序复杂度上升
正是因为多线程提高了CPU和内存等资源的利用率,但是资源是有限的,所以我们也不能随意创建线程,才引入了线程池的概念。
线程池的实现
// 重写ThreadPoolExecutor的构造方法
public class MyThread extends ThreadPoolExecutor {
//当前线程号
static int poolNum=0;
/**
*
* @param corePoolSize:核心线程数
* @param maximumPoolSize:最大线程数
* @param keepAliveTime:线程闲置时的超时时间
* @param workQueueSize:阻塞队列
* TimeUnit.MILLISECONDS:keepAliveTime的时间单位
* threadFactory:线程工厂
* new ThreadPoolExecutor.AbortPolicy():拒绝策略
*/
public MyThread(int corePoolSize, int maximumPoolSize, long keepAliveTime,
int workQueueSize) {
super(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(workQueueSize),
new ThreadFactory(){
@Override
public Thread newThread(Runnable runnable){
return new Thread(runnable, "FlowThread"+"-"+poolNum++);
}
},
new ThreadPoolExecutor.AbortPolicy()
);
}
}
线程池的关闭
- ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和showdownNow()。
- showdown():不会立即终止线程池,而是要等待所有任务缓存队列都执行完毕再终止,但是再也不会接受新的任务
- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并清空任务缓存队列,返回尚未执行的任务。
Executors提供四种线程池
- 一般这四种线程池我们都不会使用,所以这里我们就先不聊了
线程调度策略
抢占式调度策略
Java运行时系统的线程调度算法是抢占式的。Java运行时系统支持一种简单地固定的优先级的调度算法。
如果一个优先级比其他线程优先级高的线程进入就绪状态,那么系统运行时就会选择该线程运行。
优先级高的线程抢占了其他线程的资源。但是Java运行时系统并不抢占同优先级的线程,
对于处于就绪状态的同优先级的线程,Java采用一种简单的,非抢占式的调度顺序。
时间片轮转调度策略
有些系统的线程调度采用把时间片轮转调度策略。这种调度策略是对于处于就绪状态的线程选择优先级最高的线程,
分配一定的CPU时间。该时间过了之后,在选择其他线程运行。只有当线程运行结束,
放弃CPU或者由于某种原因进入阻塞状态,低优先级的线程才会有机会执行。对于优先级相同的线程都在等待CPU,
则以轮转的方式选择运行的线程。
线程的流转状态
线程的状态可以分为7种
new:新建状态
ready:就绪状态
Running:可运行状态
Terminated:终止状态
Waiting:等待状态
TimedWaiting:超时等待状态
Blocked:阻塞状态
新建状态
- new 一个线程实例后,线程就进入了新建状态 (new Thread)
就绪状态(Ready)
- 线程对象创建成功后,调用该线程的start()函数,线程进入就绪状态
运行状态(Running)
- 线程调度程序从可运行线程池中选择一个线程,是该线程获得CPU时间片,该线程就进入运行状态。
- 当线程CPU时间片执行完或者调用yield()函数,该线程就回到就绪状态。
终止状态(Terminated)
- 线程运行,直到执行结束或者执行过程中因意外终止,就是使该线程进入终止状态
阻塞状态(Blocked)
- 运行状态的线程获取同步锁失败或发出I/O请求,该线程就进入阻塞状态。如果是获取同步锁失败,JVM还会吧该线程放入锁的同步队列
等待状态(Waiting)
- 运行状态的线程执行wait(),join(),LockSupport.park()任意函数,该线程进入等待状态。其中wait(),join()函数会让线程释放掉锁,进入等锁池。处于这种状态的线程不会被分配CPU时间片,它们要等待被主动唤醒,否则会一直处于等待状态。
- 执行LockSupport.unpark(t)函数唤醒指定线程,该线程就回到了就绪状态,等待分配CPU时间片。
- 而通过notify()、notifyAll()、join()线程执行完毕方式,会唤醒等锁池线程,从等锁池回到就绪状态
超时等待状态(Timed waiting)
- 超时等待状态和等待状态一样,唯一的区别就是多了超时等待机制,不会一直等待被其他线程唤醒,而是到达指定时间,主动唤醒。
- 使用wait(long),join(long),LockSupport.parkNanos(long),LockSupport.parkUtil(long),sleep(long)会触发线程进入超时等待状态
sychronized
- 通过加sychronized的方法和代码块,在多线程访问的时候,同一时刻只能有一个线程访问。
相关面试题
sychronized和Lock的区别?
Lock是一个接口,sychronized是一个关键字
Lock加锁的时候,必须使用unlock释放锁,否则会造成死锁。sychronized不需要手动释放锁。
lock的使用更加灵活,可以有中断响应,有超时时间等等,sychronized会一直等待获取锁,直到获取锁成功。
Lock lock = new ReentrantLock(); lock.lock(); try { } catch (Exception e) { } finally { lock.unlock(); }
什么是死锁
- 死锁指的是两个以上的线程在执行的过程中,在资源的竞争而造成的一种阻塞现象。若无外力作用,它们都无法进行下去,这时候就产生了死锁。
怎么预防死锁
如果获取一个锁获取不到,直接return中断当前线程执行,简单粗暴。
为什么要使用线程池
- 如果我们在一个方法中选择去new一个线程,这个方法被调用很多次的时候,我们就会创建很多线程,这样不仅会消耗大量的系统资源,比如CPU资源和内存资源,还会降低系统稳定性。所以我们选择使用线程池。因为线程池可以合理的使用线程,可以带来一下好处:
- 1.降低资源消耗,通过重复利用创建好的线程,降低创建线程和销毁线程带来的资源消耗
- 2.提供响应速度,通过使用创建好的线程,减少了线程状态的转换,提升了响应速度
- 3.增加了线程的可管理性,线程是稀缺资源,,使用线程池可以统一分配,调优和监控。
线程池的核心属性有哪些?
- ThreadFactory(线程工厂):用于创建线程的工厂
- corePoolSize(核心线程数):当线程池运行的线程数小于核心线程数,不管其他线程处不处于空闲状态,都将创建一个新的线程来处理请求
- workQueue(阻塞队列):用于保留任务,如果当前线程数已经达到了核心线程数,则判断阻塞队列是否已满,如果未满,将新的任务移交到阻塞队列
- maxumunPoolSize(最大线程数):如果阻塞队列满了,当再有新的任务进来的时候,判断是否达到最大线程数,如果未达到最大线程数,则创建一个新的线程来处理这个任务
- handler(拒绝策略):当阻塞队列已经满了,并且已经达到最大线程数上限,则会采取拒绝策略。注意:在线程池的线程数已经超过corePoolSize,并且有的线程的空闲时间超过了keppAliveTime(存活时间),这个线程也会触发拒绝策略,被终止掉。直到线程池中的线程数量不大于corePoolSize的时候。
sychronized的加锁场景范围
//作用于非静态方法,锁住的对象实例是this,每一个对象实例有一个锁
public sychronized void method() {}
//作用于静态方法,锁住的是类的Class对象。
public static sychronized void method() {}
//作用于Lock.class,锁住的是Lock的Class对象
sychronized (Lock.class) {}
//作用于this,锁住的是对象实例
sychronized(this) {}
//作用于静态成员变量,锁住的是该静态成员变量
public static Object monitor = new Object();
sychronized (monitor) {};
线程池的状态有哪些?
- running:能够接受新的任务,并且也能处理阻塞队列中的任务
- shutdown:不会立即终止线程池,不再接受新的任务,但是会继续处理各种缓存队列中的任务。调用showdown()方法进入该状态
- stop:不接受新的任务,也不处理该队列中的任务,中断正在执行的任务。当线程池处于Running状态或者shutdown状态的时候,调用shutdownNow()方法使线程池进入此状态
- tidying:如果所有任务都已经终止了,有效线程数为0,线程池进入该状态后会调用terminated()方法进入terminated状态。
- terminated(终止):在terminated()方法执行之后进入该状态。
Java中如何保证线程的安全
- 使用sychronized加锁
- 使用Lock加锁
- 使用concurrent下边的类
wait()和sleep()的区别
- sleep()属于Thread类,wait()属于Object类
- sleep不会让当前线程释放锁,wait()会使当前线程释放锁
- sleep可以使用在任何地方,wait()只能使用在同步方法或者代码块里
- sleep()时间到了,会自动唤醒,wait()需要其他线程调用同一对象的notify()或者notifyAll()才可以唤醒
线程的sleep()方法和yield()方法的区别
- sleep()是当前线程进入超时等待状态,yield()使当前线程进入就绪状态。
- sleep()给其他线程执行机会的时候不考虑优先级,yield()给其他线程执行机会的时候只会给与自己优先级相同或者优先级比自己高的线程机会
线程的join()方法时干啥用的
- 等待当前线程终止,如果一个线程A执行了threadB.join();则A线程就会等待线程B执行完毕之后继续往下执行。
什么是Executor框架?
- 无限制的创建线程会以前你程序内存移除,所以我们通常使用线程池的方式来管理线程。Executor框架就是一个创建线程池的框架
这里有篇阿里的文章,有兴趣可以看一下!!