- 程序,进程,线程
- 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念;
- 进程是执行程序的一次执行过程,是一个动态的概念,是系统资源分配的单位;
- 通常在一个进程中可以包含若干个线程,线程是CPU调度和执行的单位;
- 若是单核cpu,则多线程是模拟出来的,在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,由于切换的块,就有同时执行的错觉;而 真正的多线程是指有多个cpu,即多核;
- 一些概念
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- main()为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,若开辟了多个线程,线程的运行是由调度器安排调度的,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的;
- 对同一份资源进行操作时,会存在资源抢夺的问题,需要加入并发的控制;
- 线程会带来额外的开销,如cpu调度时间,并发控制开销;
- 每个线程在自己的工作内存交互,内存控制不当回造成数据不一致;
- 创建线程的方法
继承Thread类:重写run()方法,调用start()方法开启线程,线程开启不一定立即执行,由cpu调度执行;由这种方法创建线程,子类继承Thread类具备多线程能力,但由于OOP单继承局限性不建议使用;
实现Runnable()接口:创建一个类实现Runnable接口的类,重写run()方法,创建实现类的对象,将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象,通过Thread类的对象调用start()方法,推荐使用此方法,
方便同一个对象被多个线程使用;
实现Callable接口,需要返回值类型,重写call方法,需要抛出异常,创建目标对象,创建执行服务,提交执行,获取结果,关闭服务。
- 静态代理:静态代理是定义父类或者接口,然后被代理对象(即目标对象)与代理对象一起实现相同的接口或者继承相同父类。代理对象与目标对象事项相同的接口,然后通过调用相同的方法来调用目标对象的方法;
优点:可不修改目标对象的功能,通过代理对象对目标功能扩展;
缺点:由于实现一样的接口,会有很多代理类,一旦接口增加方法,目标对象与代理对象都要维护。
- 函数式接口:任何接口,如果只包含唯一一个抽象方法,那么他就是一个函数式接口,对于函数式接口,就可以通过lambda表达式来创建该接口的对象;
- 线程的状态:
- 创建:程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态);
- 就绪:当线程对象调用了Thread.start()方法之后,该线程处于就绪状态;
- 运行:就绪之后,若抢到了cpu的资源分配,就进入了运行状态;
- 阻塞:线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态,阻塞状态可分为等待阻塞,同步阻塞和其他阻塞;
- 死亡:线程正常结束或者抛出未补货的异常都可以结束线程;
- 用sleep模拟倒计时
public class TestCountDown {
public static void main(String[] args) {
down();
}
public static void down(){
int num=10;
while (true){
try {
Thread.sleep(1000);
System.out.print(num--);
if(num<=0){
break;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println();
}
}
}
- yield():礼让线程,让当前正在执行的线程暂停,但不阻塞,将线程从运行状态转为就绪状态,让cpu重新调度,礼让不一定成功。
- join():使调用join()方法的线程进入等待池并等待线程执行完毕后才会被唤醒,并不影响同一时刻处在运行状态的其他线程。
- 观测线程的状态:thread.getState();
- 线程的优先级:优先级用数字表示,范围从1~10,用setPriority()设置优先级,优先级的设定建议在start()调度前,优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看cpu的调度;
- 用户线程和守护线程:守护线程(Daemon Thread)也被称之为后台线程或服务线程,守护线程是为用户线程服务的,当程序中的用户线程全部执行结束之后,守护线程也会跟随结束。
可以通过 Thread.setDaemon(true) 方法将线程设置为守护线程;默认情况下我们创建的线程或线程池都是用户线程,gc就是守护线程。
- 线程同步:由于统一进程的多个线程共享同一块存储空间,会有访问冲突的问题,为了保证数据被访问时的正确性,在访问的同时加入锁机制,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可。
- 同步方法默认用this或者当前类class对象作为锁;同步代码块选择会发生同步问题的部分代码进行锁住,锁会变化的对象。
- 产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用;
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺;
- 循环等待条件:若干进程形成一种头尾相接的循环等待资源关系;
- synchronized与lock对比
- lock是显示锁(手动开启与关闭),synchronized是隐式锁,出了作用域自动释放;
- lock只有代码块锁,synchronized有代码块锁和方法锁;
- 使用lock锁,JVM将话诶较少的时间来调度线程,性能更好,并有更好的扩展性;
- 优先使用顺序:lock,同步代码块,同步方法;
- 线程通信问题:
- wait():表示线程一直等待,知道其他线通知,与sleep不同,会释放锁;
- wait(long timeout):指定等待的毫秒数;
- notify():唤醒一个处于等待状态的线程;
- notifyAll():唤醒同一个对象上所有调用wait()方法的线程,优先级高的线程有限调度;
- 生产者消费者问题:是多线程同步问题的经典案例。也就是两个想爱你成在实际运行中会有互相通知的问题,解决这个问题可以利用管程法(缓冲区法)或者信号灯法;
- 为什么要有线程池?
经常创建和销毁线程会使用特别大的资源,尤其是并发情况下,对性能影响很大,因此要提前创建好多个线程,放入线程池中,使用获取,用完放回,高效利用。
java通过Executors提供四种线程池:分别为:
工厂方法 |
corePoolSize |
maximumPoolSize |
keepAliveTime |
workQueue |
应用场景 |
newCachedThreadPool |
0 |
Integer.MAX_VALUE |
60s |
SynchronousQueue |
执行数量多,耗时少的线程任务 |
newFixedThreadPool |
nThreads |
nThreads |
0 |
LinkedBlockingQueue |
控制线程最大并发数 |
newSingleThreadExecutor |
1 |
1 |
0 |
LinkedBlockingQueue |
单线程(不适合并发但可能引起IO阻塞或影响UI线程相应的操作,如数据库操作) |
newScheduledThreadPool |
corePoolSize |
Integer.MAX_VALUE |
0 |
DelayedWorkQueue |
执行定时/周期性任务 |
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
fixedThreadPool.execute(task);
- 总结:Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理可以更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 的 4 个功能线程有如下弊端:
FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
BlockingQueue queue = new LinkedBlockingQueue(10);
ThreadPoolExecutor executor = new ThreadPoolExecutor(4,10,2,TimeUnit.SECONDS,queue);
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("test");
}
});
合理配置线程池:需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。
对于CPU密集型任务(计算密集型):线程池中线程个数应尽量少,不应大于CPU核心数;
对于IO密集型任务(大量网络,文件操作):由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率;
对于混合型任务:可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)