JavaSE基础知识分享(十二)
写在前面
今天继续讲Java中的进程和线程的知识!
进程和线程概述
-
进程
进程是正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。
-
线程
线程是进程中的单个顺序控制流,是一条执行路径。一个进程如果只有一条执行路径,则称为单线程程序;如果有多条执行路径,则称为多线程程序。
Java程序运行原理
Java命令会启动Java虚拟机,即JVM,等同于启动了一个应用程序进程。该进程会自动启动一个“主线程”,然后主线程去调用某个类的main方法。因此,main方法运行在主线程中。在此之前的所有程序都是单线程的。
JVM虚拟机的启动是单线程的还是多线程的?
答:JVM的启动是多线程的,包括主线程和垃圾回收线程等。
多线程实现
-
如何创建一个线程对象?
- a. 自定义线程类继承Thread类,重写run方法。
- b. 自定义线程类实现Runnable接口,实现run方法。
- c. 自定义线程类实现Callable接口,借助线程池,实现run方法。
这里的run方法是针对一个线程对象它所干的事。
-
如何启动一个线程?
- 调用start()方法启动,Thread类中有start()方法来控制每个线程的开始。当然也有stop来控制线程的结束。如果单纯调用run方法则不是使用线程的思想来考虑问题,而是简单的对象调用成员方法!
Thread类的基本方法
-
为什么要重写run()方法?
- 实现每个线程该干的事。
-
启动线程使用的是哪个方法?
- 使用start()方法启动线程,而不是直接调用run()方法。
-
线程能不能多次启动?
- 不能,线程一旦启动就进入了就绪态,之后通过抢占式来夺取运行权。正在运行当中的线程可以通过相关操作进行阻塞回到就绪或者同步该进程。
-
run()和start()方法的区别
- run方法描述了线程具体执行的代码体,重写在继承Thread的子类或实现Runnable接口的类中。而start方法用于启动一个新线程,执行该线程的run方法。
Thread类中的成员方法
-
获取线程对象的名字
public final String getName();
-
设置线程对象名字的方式
- a. 通过父类的有参构造方法,在创建线程对象的时候设置名字。
- b. 线程对象调用setName(String name)方法,给线程对象设置名字。
-
获取线程的优先级
public final int getPriority();
-
设置线程优先级
public final void setPriority(int i);
- 在启动之前设置,优先级范围为1到10。
-
如何获取main方法所在的线程名称?
- 使用静态方法
Thread.currentThread().getName()
,这样可以获取任意方法所在的线程名称。
- 使用静态方法
Runnable接口
- 如何获取线程名称
- 如何给线程设置名称
实现Runnable接口的好处:
- 可以避免由于Java单继承带来的局限性。
- 适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码、数据有效分离,较好地体现了面向对象的设计思想。
Callable接口
- 和线程池执行Runnable对象的差不多。
- 好处:
- 可以有返回值。
- 可以抛出异常。
- 弊端:
- 代码比较复杂,所以一般不用。
注意
- 启动一个线程的时候,若直接调用run方法,仅仅是普通的对象调用方法,底层不会额外创建一个线程再执行。
- 从执行的结果上来看,Java线程之间是抢占式执行的,谁先抢到CPU执行权谁就先执行。
- 每次运行的结果顺序不可预测,完全随机的。
- 每个线程都有优先权。具有较高优先级的线程优先于优先级较低的线程执行。
调度模型
Java线程有两种调度模型:
-
分时调度模型
- 所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
-
抢占式调度模型
- 优先让优先级高的线程使用CPU,如果线程的优先级相同,则随机选择一个线程执行。优先级高的线程获取的CPU时间片相对多一些。
注意
- Java使用的是抢占式调度模型。
演示如何设置和获取线程优先级
public final int getPriority();
public final void setPriority(int newPriority);
设置线程优先级通过setPriority(int i)方法,在启动之前设置,优先级范围为1到10。
对象线程控制方法
-
线程休眠
public static void sleep(long millis);
-
线程加入
public final void join();
-
线程礼让
public static void yield();
-
后台线程
public final void setDaemon(boolean on);
-
中断线程
public final void stop(); public void interrupt();
线程的生命周期
线程的生命周期包括以下几个状态:
- 新建(New):线程对象创建后进入此状态,尚未开始执行。
- 就绪(Runnable):线程调用了start()方法后,进入此状态,等待CPU资源。
- 运行(Running):线程获得CPU时间片后,进入此状态,实际执行run()方法中的代码。
- 阻塞(Blocked):线程因等待某个资源而阻塞,例如等待I/O操作或锁。
- 等待(Waiting):线程在等待另一个线程的特定条件(如等待通知)时处于此状态。
- 超时等待(Timed Waiting):线程在指定的时间内等待,例如调用Thread.sleep()。
- 死亡(Terminated):线程完成执行或因异常终止后进入此状态。
解决线程安全问题的基本思想
问题判断
- 是否是多线程环境?
- 是否有共享数据?
- 是否有多条语句操作共享数据?
基本思想
让程序没有安全问题的环境。核心思想是确保同一时间只有一个线程能操作共享数据。
实现方式
-
同步代码块
- 格式:
synchronized (对象) { // 需要同步的代码 }
- 解释: 同步的根本原因在于锁住的对象。锁对象如同锁的功能,确保同一时间只有一个线程能够执行同步代码块。
- 同步的前提:
- 多个线程
- 多个线程使用的是同一个锁对象
- 同步的好处: 解决多线程安全问题。
- 同步的弊端: 当线程很多时,判断同步锁的开销高,可能降低程序运行效率。
同步代码块的对象可以是:
- 任意对象实例
- 当前对象(
this
) - 类对象(
Class
对象) - 常量对象
注意事项:
- 选择合适的锁对象:避免死锁,推荐使用实例对象或类对象。
- 锁的粒度:粒度过细会导致性能问题,粒度过粗可能导致不必要的线程阻塞。
- 避免死锁:确保获取锁的顺序一致。
- 格式:
-
同步方法
- 实例方法: 锁对象是当前实例 (
this
)。 - 静态方法: 锁对象是该类的
Class
对象。
建议: 如果锁对象是
this
,可以考虑使用同步方法。如果锁对象是其他对象,建议使用同步代码块。 - 实例方法: 锁对象是当前实例 (
-
Lock锁的使用
- 特点: 提供显式的加锁和解锁操作,避免隐式锁的开销。
- 接口:
void lock(); void unlock();
- 实现:
ReentrantLock
是常用的实现。import java.util.concurrent.locks.ReentrantLock; public static final ReentrantLock lock1 = new ReentrantLock(); public static final ReentrantLock lock2 = new ReentrantLock();
- 弊端:
- 效率低
- 同步嵌套可能导致死锁
死锁问题
- 定义: 两个或更多线程因争夺资源产生的互相等待现象。
线程的等待唤醒机制(生产者消费者模型)
- 需求: 只有当产品池中有数据时消费者才去消费,只有当产品池中没有数据时生产者才去生产。
- 实现: 使用等待唤醒模式,生产者等待消费者的唤醒才开始生产,消费者等待生产者的唤醒才开始消费。
线程的状态转换图
线程组和线程池
线程组
- 定义: Java中使用
ThreadGroup
来表示线程组,允许对一批线程进行分类管理。 - 获取线程组:
public final ThreadGroup getThreadGroup();
- 设置线程分组:
Thread(ThreadGroup group, Runnable target, String name);
线程池
- 优点: 提高性能,减少创建和销毁线程的开销,线程池中的线程复用。
- JDK5 之前: 手动实现线程池。
- JDK5 及以后: 提供
Executors
工厂类来创建线程池。- 方法:
public static ExecutorService newCachedThreadPool(); public static ExecutorService newFixedThreadPool(int nThreads); public static ExecutorService newSingleThreadExecutor();
- ExecutorService 方法:
Future<?> submit(Runnable task); <T> Future<T> submit(Callable<T> task);
- 方法:
常用线程池
-
固定大小线程池
static ExecutorService newFixedThreadPool(int nThreads);
- 重用固定数量的线程,适用于线程数已知的场景。
-
单线程池
static ExecutorService newSingleThreadExecutor();
- 只有一个线程运行任务,保证任务顺序执行。
-
缓存线程池
static ExecutorService newCachedThreadPool();
- 根据需要创建新线程,重用之前构造的线程。
-
调度线程池
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
- 用于调度任务的执行(定时或周期性)。
匿名内部类方式使用多线程
pool.submit(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
Thread t1 = Thread.currentThread();
System.out.println(t1.getName() + " - " + i);
}
}
});
定时器
- 类:
Timer
和TimerTask
- 方法:
- Timer:
public Timer(); public void schedule(TimerTask task, long delay); public void schedule(TimerTask task, long delay, long period);
- TimerTask:
public abstract void run(); public boolean cancel();
- 使用示例:
Timer timer = new Timer(); timer.schedule(new MyTask(), 10000); // 延迟10秒执行 timer.schedule(new MyTask(), 10000, 2000); // 延迟10秒后,每2秒执行一次
- Timer:
多线程面试题
-
多线程有几种实现方案?
- 三种:
- 继承
Thread
类,重写run
方法。 - 实现
Runnable
接口,重写run
方法。 - 实现
Callable
接口,通过线程池执行。
- 继承
- 三种:
-
同步有几种方式?
- 两种:
- 使用
synchronized
关键字。 - 使用
Lock
接口。
- 使用
- 两种:
-
启动一个线程是
run()
还是start()
?它们的区别?- 启动线程使用
start()
方法,start()
方法会创建一个新的线程,并调用run()
方法。直接调用run()
方法只是普通方法调用,不会创建新线程。
- 启动线程使用
-
sleep()
和wait()
方法的区别?sleep()
:线程进入睡眠状态,等待指定时间后自动恢复。wait()
:线程进入等待阻塞状态,必须由其他线程调用notify()
或notifyAll()
才能恢复。
-
为什么
wait()
,notify()
,notifyAll()
等方法定义在Object
类中?- 因为这些方法用于对象级别的线程协调,不依赖于具体的线程类型。
设计模式
创建型模式
-
简单工厂模式
- 用于创建对象的工厂类。
-
工厂方法模式
- 定义一个创建对象的接口,由子类决定实例化哪一个类。
-
单例模式
- 单例设计思想:保证类在内存中只有一个对象。
- 实现方式:
- 饿汉式:类加载时创建唯一实例。
- 懒汉式:通过静态方法在首次使用时创建实例。
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); // 不安全的线程操作 } return instance; }
- 懒汉式存在的线程安全问题**:
- 多线程环境下可能会出现多个实例。解决方案包括使用双重检查锁(Double-Checked Locking)或使用静态内部类。
行为型模式
-
观察者模式(Observer Pattern)
- 定义对象之间的一对多依赖关系,让多个观察者对象同时监听一个主题对象。
- 当主题对象状态发生变化时,所有依赖于它的观察者都会收到通知并自动更新。
-
策略模式(Strategy Pattern)
- 定义一系列算法,将每一个算法封装起来,并使它们可以互相替换。
- 策略模式让算法的变化不会影响到使用算法的客户。
-
模板方法模式(Template Method Pattern)
- 定义一个操作中的算法骨架,而将一些步骤延迟到子类中。
- 模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
-
状态模式(State Pattern)
- 允许一个对象在其内部状态改变时改变它的行为。
- 状态模式让对象看起来好像修改了它的类。
-
责任链模式(Chain of Responsibility Pattern)
- 使多个对象有机会处理请求,从而避免请求的发送者和接收者之间的耦合。
- 将处理请求的对象链起来,直到有一个对象处理请求。
结构型模式
-
适配器模式(Adapter Pattern)
- 将一个类的接口转换成客户希望的另一个接口。
- 使得原本由于接口不兼容而无法一起工作的类可以一起工作。
-
装饰器模式(Decorator Pattern)
- 动态地给一个对象添加一些额外的职责。
- 装饰器模式相比生成子类更加灵活,能够在不改变类的结构的情况下扩展功能。
-
代理模式(Proxy Pattern)
- 为其他对象提供一种代理以控制对这个对象的访问。
- 代理模式可以用来实现延迟加载、日志记录、权限控制等功能。
-
桥接模式(Bridge Pattern)
- 将抽象部分与实现部分分离,使它们可以独立变化。
- 桥接模式通过引入一个桥接接口来解耦抽象层和实现层。
-
组合模式(Composite Pattern)
- 允许将对象组合成树形结构以表示“部分-整体”的层次结构。
- 组合模式使得客户端对单个对象和对象集合的使用具有一致性。
-
外观模式(Facade Pattern)
- 为子系统中的一组接口提供一个统一的高层接口。
- 外观模式定义了一个更高层的接口,让子系统更易于使用。
-
享元模式(Flyweight Pattern)
- 使用共享对象来高效地支持大量细粒度的对象。
- 享元模式主要用于减少创建对象的开销,节省内存。
-
门面模式(Facade Pattern)
- 提供一个统一的接口来访问子系统中的一群接口。
- 使得子系统更易于使用和理解。
总结
多线程编程涉及到线程的生命周期、线程安全、线程池等多个方面。设计模式在软件设计中提供了解决特定问题的标准化方案,帮助实现代码的高效重用和扩展性。了解这些概念和技术有助于编写高效、可维护的代码。