目录
创建线程的三种方式
分别是继承Thread类、实现Runnable接口、实现Callable接口。
1、通过继承Thread类
- 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
2、通过实现Runnable接口
- 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
- 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。
- 调用线程对象的start()方法来启动该线程。
- 建议实现Runnable接口的方式
- 避免了类的单继承的局限性
- 更适合处理有共享数据的问题
- 实现了代码和数据的分离
3、通过实现Callable接口
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
Runnable接口和Callable接口的区别
- call方法可以有返回值,Callable使用泛型参数,可以指定call方法返回参数类型
- call方法可以用throw的方式处理异常
解决线程安全问题
多个线程间有共享的资源,所以会产生线程安全问题
synchronized同步机制
- 同步代码块
- synchronized(同步监视器){需要被同步的代码}
- 同步方法
- 用synchronized关键字修饰的方法
Lock锁
- ReentrantLock
- Java 5新增了一个java.util.concurrent包来支持同步,其中ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
- 1、创建Lock的实例对象,确保多个线程共用一个Lock实例对象,所以需要考虑用static final
- 2、执行lock方法,锁定对共享资源的调用
- 3、执行unlock方法,释放对共享资源的锁定
synchronized不管是同步代码块还是同步方法,都需要在结束一对{ }后,释放对同步监视器的调用
Lock是通过两个方法控制需要被同步的代码,更灵活
Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高
线程死锁
- 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
- 造成死锁的原因
- 循坏等待
- 不可抢夺
- 占用且等待
- 互斥
volatile关键字的作用
保证可见性
- 当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。
当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。当一个线程用到被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。
禁止指令重排
- 指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。非常经典的例子是在单例方法中同时对字段加入voliate,就是为了防止指令重排序。
线程通信
wait()、notify()、notifyAll()
- 这三个方法是Object类中声明的方法,必须在同步代码块或同步方法中使用(只有采用synchronized实现线程同步时才能使用这三个方法), 且这三个方法的调用者必须是同步监视器。
- wait():让当前线程进入等待(阻塞)状态,并释放锁(释放对同步监视器的调用)
- notify():唤醒一个正在等待的线程
- notifyAll():唤醒所有正在等待的线程
wait()和sleep()的区别
- sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
- sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
- sleep()不会释放锁,而wait()会释放锁(同步监视器),需要通过notify()/notifyAll()唤醒;
Thread 类中的 yield 方法有什么作用
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。
Java 中用到的线程调度算法是什么
线程调度是指按照特定机制为多个线程分配 CPU 的使用权。(Java是由JVM中的线程计数器来实现线程调度)
有两种调度模型:分时调度模型和抢占式调度模型。
- 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
- Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
ThreadLocal
每个线程都有一个线程隔离区
ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量,只能由当前 Thread 使用,其它 Thread 不可访问,那就不存在多线程间共享的问题。
逻辑关系 Thread->ThreadLocal-> ThreadLocalMap
每个线程都有一个ThreadLocal,ThreadLocal中又有ThreadLocalMap,其中key是ThreadLocal实例,value是我们set进去的值。
四种引用类型
绝大部分对象都是强引用
- 强引用 不会被GC
- 软引用 内存不足时被GC,软引用可用来实现内存敏感的高速缓存。
- 弱引用 JVM进行垃圾回收时就会被GC
- 虚引用 任何时候都可能被GC
- ThreadLocalMap中key是弱引用,方法执行完后threadLocal实例会被直接GC(不需要了)
- ThreadLocalMap中value是强引用,方法执行完后,避免数据丢失。
使用完ThreadLocal之后手动remove进行回收,否则可能会导致数据错乱或内存泄漏(该回收的数据没有被回收)