JUC部分需要深究的知识点

1、JAVA构建线程的所有方式

  • 通过继承Thread类创建线程。这种方式需要定义一个子类,继承Thread类,并重写run()方法,然后创建子类的对象,并调用start()方法启动线程。
  • 代码示例:
//定义一个子类,继承Thread类
class MyThread extends Thread {
    //重写run()方法
    @Override
    public void run() {
        //线程要执行的逻辑
        System.out.println("Hello, I am a thread.");
    }
}

//在主方法中创建子类对象,并调用start()方法启动线程
public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread(); //创建子类对象
        t.start(); //启动线程
    }
}
  • 通过实现Runnable接口创建线程。这种方式需要定义一个类,实现Runnable接口,并实现run()方法,然后创建该类的对象,并作为参数传递给Thread类的构造器,创建Thread对象,并调用start()方法启动线程。
  • 代码示例:
//定义一个类,实现Runnable接口
class MyRunnable implements Runnable {
    //实现run()方法
    @Override
    public void run() {
        //线程要执行的逻辑
        System.out.println("Hello, I am a thread.");
    }
}

//在主方法中创建该类对象,并作为参数传递给Thread类的构造器,创建Thread对象,并调用start()方法启动线程
public class Main {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable(); //创建该类对象
        Thread t = new Thread(r); //创建Thread对象
        t.start(); //启动线程
    }
}
  • 通过实现Callable接口创建线程。这种方式需要定义一个类,实现Callable接口,并实现call()方法,然后创建该类的对象,并作为参数传递给FutureTask类的构造器,创建FutureTask对象,并作为参数传递给Thread类的构造器,创建Thread对象,并调用start()方法启动线程。这种方式可以获取线程的返回值和异常。
  • 代码示例:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableDemo {

    public static void main(String[] args) throws Exception {
        // 创建一个Callable任务
        MyCallable task = new MyCallable();
        // 使用FutureTask包装Callable任务
        FutureTask<Integer> future = new FutureTask<>(task);
        // 创建一个线程并执行FutureTask
        Thread thread = new Thread(future);
        thread.start();
        // 获取Callable任务的返回结果
        Integer sum = future.get();
        System.out.println("The sum is: " + sum);
    }
}

// 实现Callable接口的类
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}
  • 通过使用线程池创建线程。这种方式可以避免频繁地创建和销毁线程,提高性能和资源利用率。可以使用Executor框架提供的各种线程池,如FixedThreadPool, CachedThreadPool, ScheduledThreadPool等,或者自定义线程池。然后将实现了Runnable或Callable接口的任务对象提交给线程池执行。

2、run和start的区别是什么

在java线程中,run和start的区别主要有以下几点:

3、线程池

3.1、ThreadPoolExecutor的使用

ThreadPoolExecutor是一个实现了ExecutorService接口的类,它可以创建和管理一个线程池,执行Runnable或Callable任务

要使用ThreadPoolExecutor,你可以直接创建它的实例,或者使用Executors类的工厂方法来获取它的实例

下面是一个例子,它直接创建了一个ThreadPoolExecutor的实例,并设置了核心线程数、最大线程数、空闲线程存活时间、任务队列和拒绝策略

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorExample {

    public static void main(String[] args) {
        // 创建一个有四个核心线程、六个最大线程、一秒空闲存活时间、有十个容量的阻塞队列和默认拒绝策略的线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 6, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));

        // 提交十五个打印任务
        for (int i = 0; i < 15; i++) {
            executor.execute(new PrintTask("Task " + i));
        }

        // 关闭线程池
        executor.shutdown();
    }
}

// 定义一个打印任务类,实现Runnable接口
class PrintTask implements Runnable {

    private String name;

    public PrintTask(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(name + " is running on " + Thread.currentThread().getName());
    }
}

输出结果如下图所示

 你可以看到,当提交第十二个任务时,由于队列已满,而且没有空闲的线程,所以触发了拒绝策略,抛出了RejectedExecutionException异常。这说明你需要根据你的任务数量和性能要求来合理地配置线程池的参数。

3.2 FixedThreadPool线程池的使用例子

FixedThreadPool的核心线程数和最大线程数都被设置成相同的固定值。

使用无界队列LinkedBlocking作为线程池的工作队列,队列的容量为Integer.MAXVALUE,由于使用无界队列,线程池并不会拒绝任务。

FixedThreadPool是一种线程池,它可以创建一个固定数量的线程来执行任务。如果所有的线程都在忙,那么新的任务就会在队列中等待。如果有空闲的线程,那么队列中的任务就会被分配给它们

要使用FixedThreadPool,你可以使用Executors类的newFixedThreadPool方法来创建一个ExecutorService实例,然后调用它的execute或submit方法来提交Runnable或Callable任务

下面是一个简单的例子,它创建了一个有两个线程的FixedThreadPool,并提交了五个打印任务

执行流程如下:

1、如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务

2、在线程池中完成预热之后,将任务加入任务队列当中

3、线程执行完1中的任务后,会不断从任务队列中拿取任务执行

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {

    public static void main(String[] args) {
        // 创建一个有两个线程的线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交五个打印任务
        for (int i = 0; i < 5; i++) {
            executor.execute(new PrintTask("Task " + i));
        }

        // 关闭线程池
        executor.shutdown();
    }
}

// 定义一个打印任务类,实现Runnable接口
class PrintTask implements Runnable {

    private String name;

    public PrintTask(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(name + " is running on " + Thread.currentThread().getName());
    }
}

输出如下

 你可以看到,只有两个线程在交替执行五个任务。这样可以避免创建过多的线程,提高资源利用率和性能。

3.3 SingleThreadExecutor

SingleThreadExecutor是一种ExecutorService,它使用一个单独的线程来执行所有提交的任务。它类似于一个线程数为1的FixedThreadPool,但是它有一些额外的特性,比如保证任务的顺序执行和维护一个隐藏的任务队列使用SingleThreadExecutor可以避免多线程的同步问题,比如在访问共享文件系统时2下面是一个使用SingleThreadExecutor的例子

同样,SingleThreadExecutor采用的也是无界队列LinkedBlockingQueue作为线程池的工作队列。同样不会拒绝任务。

1、如果当前运行线程数少于核心线程数,则创建一个新线程来执行任务

2、当线程池完成预热之后,将任务加入无界队列中

3、线程执行完1中的任务后,会从任务队列中拿任务来继续执行

// 创建一个SingleThreadExecutor
ExecutorService executor = Executors.newSingleThreadExecutor ();

// 提交三个任务
executor.execute (new Runnable () {
  @Override
  public void run () {
    System.out.println ("Task 1");
  }
});

executor.execute (new Runnable () {
  @Override
  public void run () {
    System.out.println ("Task 2");
  }
});

executor.execute (new Runnable () {
  @Override
  public void run () {
    System.out.println ("Task 3");
  }
});

// 关闭executor
executor.shutdown ();

输出结果如下

 3.4 CachedThreadPool

CachedThreadPool是一个可缓存的线程池,它的工作机制如下:

代码示例 :

// 创建一个可缓存的线程池
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 定义一个简单的任务
Runnable task = () -> {
    System.out.println("Hello from " + Thread.currentThread().getName());
};
// 提交10个任务到线程池
for (int i = 0; i < 10; i++) {
    cachedPool.execute(task);
}
// 关闭线程池
cachedPool.shutdown();

CachedThreadPool适合执行大量的短期小任务,或者负载较轻的服务器。但是要注意控制并发的任务数,否则可能会创建过多的线程,导致性能问题。另外,CachedThread不适合执行长时间或不确定时间的任务,例如IO操作。

3.5 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor是一个可以执行定时任务或周期性任务的线程池,它的工作机制如下:

代码示例:

// 创建一个核心线程数为2的定时线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
// 定义一个简单的任务
Runnable task = () -> {
    System.out.println("Hello from " + Thread.currentThread().getName());
};
// 在延迟5秒后执行一次任务
scheduledPool.schedule(task, 5, TimeUnit.SECONDS);
// 在延迟10秒后开始周期性地每隔15秒执行一次任务
scheduledPool.scheduleAtFixedRate(task, 10, 15, TimeUnit.SECONDS);
// 在延迟20秒后开始周期性地每隔10秒加上上次任务执行时间执行一次任务
scheduledPool.scheduleWithFixedDelay(task, 20, 10, TimeUnit.SECONDS);

ScheduledThreadPoolExecutor适合执行需要定时或周期性触发的任务,例如定时清理缓存、定时发送消息等但是要注意控制并发的任务数和执行时间,否则可能会导致任务堆积或错过预期的触发时间。另外,ScheduledThreadPoolExecutor不支持修改已经提交的定时或周期性任务的触发时间或频率

4、AQS队列

AQS队列是一个双向链表,用来存放等待获取锁的线程节点。每个节点有一个waitStatus属性,表示节点的等待状态,有以下几种取值:

  • CANCELLED (1):表示当前节点已取消调度。当超时或者中断情况下,会触发变更为此状态,进入该状态后的节点不再变化。
  • SIGNAL (-1):表示当前节点释放锁的时候,需要唤醒下一个节点。或者说后继节点在等待当前节点唤醒,后继节点入队时候,会将前驱节点更新给signal。
  • CONDITION (-2):表示当前节点在条件队列中。当其他线程调用了condition的signal方法后,condition状态的节点会从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE (-3):表示当前场景下后续的acquireShared能够得以执行。共享模式下,前驱节点不仅会唤醒其后继节点,同时也可能唤醒后继的后继节点。
  • 0 :新节点入队时候的默认状态。

根据锁的模式,AQS队列有两种功能:独占和共享。

  • 独占锁:每次只能一个线程持有锁,比如ReentrantLock就是独占锁。独占锁中,只有头结点才能尝试获取锁,其他节点都会被阻塞。当头结点释放锁时,会将其后继节点的waitStatus设置为SIGNAL,并唤醒后继节点。后继节点被唤醒后,会检查自己是否是头结点的后继节点,如果是,则尝试获取锁;如果不是,则自旋等待直到成为头结点的后继节点或者被取消。
  • 共享锁:允许多个线程持有锁,并发访问共享资源,比如ReentrantReadWriteLock。共享锁中,头结点和其后继节点都可以尝试获取锁,如果成功,则向后传播唤醒其他共享模式的节点;如果失败,则检查自己的waitStatus是否为SIGNAL,如果是,则阻塞等待被唤醒;如果不是,则自旋等待直到被设置为SIGNAL或者被取消。

因此,AQS队列中的节点并不是一直在自旋,而是根据自己的waitStatus和锁的模式来决定是否自旋、阻塞或者尝试获取锁。

5、park\unpark\wait\notify的区别

  • thread.park是LockSupport类的方法,用于阻塞和唤醒线程,不需要获取对象的锁;wait是Object类的方法,用于让线程等待和通知,必须在同步块或者同步方法中调用。
  • thread.park不会抛出InterruptedException异常,但是会响应中断,即当线程被中断时,thread.park会立即返回;wait会抛出InterruptedException异常,需要调用者处理。
  • thread.park可以先于unpark调用,即先让线程获取一个许可证,然后再调用park时不会阻塞;wait必须先于notify或者notifyAll调用,否则可能导致线程永久等待。
  • thread.park不会释放持有的锁;wait会释放持有的锁,让其他线程有机会获取锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值