【Java并发】(2)使用线程和线程池

创建线程

1. 实现Runnable接口

public class MyRunnable implements Runnable {
    public void run() {
		// ... 
	}
}
public static void main(String[] args) {
	MyRunnable instance = new MyRunnable(); 
	Thread thread = new Thread(instance); 
	thread.start();
}

2. 实现Callable接口

Callable使用FutureTask包装,可以在当前线程获取Callable线程的返回值,get()方法会阻塞当前线程,直到获取到返回值

public class MyCallable implements Callable<Integer> {
    public Integer call() {
		return 123; 
	}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
	thread.start();
	System.out.println(ft.get()); 
}

3. 继承Thread类

public class MyThread extends Thread {
    public void run() {
		// ... 
	}
}
public static void main(String[] args) { 
	MyThread mt = new MyThread(); 
	mt.start();
}

继承Thread类和实现Runnable接口哪个更好?

实现接口会更好一些,因为:
Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
类可能只要求可执行就行,继承整个 Thread 类开销过大。

Thread类解析

1. 线程优先级

  • 使用thread.getPriority(),thread.setPriority(int)来获取、设置线程优先级
  • priority的取值为1到10的整数,默认值为5。
  • 优先级高的线程分配时间片的数量要多于优先级低的线程。
  • 设置线程优先级时,针对频繁阻塞(休眠或者I/O操 作)的线程需要设置较优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较的优先级,确保处理器不会被独占。
  • 操作系统可能会忽略线程优先级的设置。

2. Daemon(守护)线程

  • 通过thread.isDaemon,thread.setDaemon(boolean)来获取、设置是否为守护线程
  • 守护线程的作用是在程序运行时在后台为其他线程提供服务不属于程序中不可或缺的部分,因此当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
  • main线程属于非守护线程
  • Daemon属性需要在启动线程之前设置,不能在启动线程之后设置
  • 在构建守护线程时,不能依靠finally块中的内容来确保执行关闭或清理资源
    的逻辑,因为守护线程可能会被JVM立即终止。

3. sleep方法

  • 通过Thread.sleep()调用
  • 休眠当前线程指定的秒数,让出CPU占用,时间到后,返回RUNNABLE状态。

4. yield方法

  • 通过静态方法Thread.yield()调用。
  • 通知线程调度器,当前线程需要让出对CPU的占用,但调度器也有可能忽略这个通知
  • 该方法很少使用,一般在设计并发框架或者调试时使用。

5. start和run方法

  • 启动新线程应当调用start方法,不能调用run方法
  • 因为启动线程的过程是:线程创建后进入NEW状态,此时在当前线程调用线程2的start方法,该方法会进行一些准备操作,然后由JVM来调用线程2的run方法,这样两个线程就会并发执行。
  • run方法知识thread的一个普通方法调用,还是会在原线程执行。

6. interrupted和interrupt方法

  • 通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞,为了能够响应中断,线程内部需要实现响应中断的逻辑,如下面第7点所示。
  • 调用线程的interrupt()方法后,该线程的isInterrupted()方法会返回true,相当于做了一个标记位。
  • 如果线程被中断(即抛出InterruptedException),那么该标记位会复原,即isInterrupted()方法会返回false.

7. 安全的终止线程

  • 当外部调用线程的interrupt()方法后,该线程可以在内部通过Thread.currentThread.isInterrupted()来获取中断标志位,从而实现相关中断逻辑。
  • 也可以公开一个方法,供外部调用。
private static class Runner implements Runnable {
	private volatile boolean on = true;
	@Override
	public void run() {
		//获取中断标志位
		while (on && !Thread.currentThread().isInterrupted())
		{
		//do something 
		}
	}
	//公开一个方法
	public void cancel() { 
		on = false;
	} 
}

8.其他属性和方法

属性/方法解释
name线程名,方便调试
id线程id,方便调试
getState返回6个状态之一
Thread.currentThread()获取当前线程引用

线程间通信

1.join方法

  • 当多个线程的执行结果有依赖关系时,需要调用join去协调线程的执行
  • 例:在当前线程调用线程2的join方法,可以理解为将线程2的执行插入到这里,然后顺序执行,当前线程必须等待线程2执行完成后,当前线程才继续执行。
public static void main(String[] args) throws Exception {
        Thread thread2 = new Thread(() -> {
            System.out.println("thread2");
        });
        thread2.start();
        thread2.join();
        System.out.println("main");
}
//输出为
//thread2
//main
  • join(millis)可以添加一个时间参数,表示超时时间,如果线程2在指定的时间内没有结束,那么当前线程就不再等待了,但还是继续并行执行

2. 等待通知机制

  • notify() 通知随机一个在对象上等待的线程从wait()方法返回,返回的前提是该线程获取了对象的锁,如果调用notify()的线程没有释放锁,那么调用wait()方法的线程需要等待锁的释放,获取锁后,才从wait()方法返回。
  • notifyAll():通知全部在对象上等待的线程从wait()方法返回,返回的前提是该线程获取了对象的锁,跟上面一样。
  • wait():使线程进入WAITING状态,并释放锁,等待另外线程的通知或中断才返回。
  • wait(millis):等待一段时间,没有通知自动返回。
public class TestWaitNotify {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        Object lock = new Object();
        Runnable waitRunnable = () -> {
            synchronized (lock) {
                try {
                    System.out.println(Thread.currentThread() + "Wait");
                    lock.wait();
                    System.out.println(Thread.currentThread() + "notified");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Runnable notifyRunnable = () -> {
            synchronized (lock) {
                try {
                    System.out.println(Thread.currentThread() + "Sleep");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "notify");
                lock.notify();
                //while (true) {
                //}
            }
        };
        threadPool.execute(waitRunnable);
        threadPool.execute(waitRunnable);
        threadPool.execute(notifyRunnable);
        threadPool.shutdown();
        threadPool.awaitTermination(2000, TimeUnit.SECONDS);
    }
}

这个例子中,两个线程等待,另外一个线程1秒后通知。

  1. 当使用notify()时,其中一个等待线程的wait()方法被返回。
  2. 当使用notifyAll()时,两个等待线程wait()方法都被返回。
  3. 当notify()后,不释放锁,等待线程继续等待。

线程池

1.好处

  • 降低资源消耗:重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控

2.ThreadPoolExecutor

  • ThreadPoolExecutor是Java线程池的实现类,它实现了ExecutorService接口,该接口定义类管理线程和线程池的方法。
  • 线程池的生命周期
生命周期解释
RUNNING运行中,接受新任务,处理排队任务
SHUTDOWN关闭,不接受新任务,处理排队任务和处理中的任务
STOP停止,不接受新任务,不处理排队任务,中断处理中的任务
TIDYING整理,所有任务终止时触发
TERMINATED终止,terminal()方法执行完成
  • 成员变量
  1. private final AtomicInteger ctl= new AtomicInteger(ctlOf(RUNNING,0))
    ctl变量通过二进制高位和地位同时表达了workerCountrunState的含义;workCount表示Worker线程的数量,runState表示线程池状态。
  2. private volatile ThreadFactory threadFactory;
    线程工厂,用于生成Worker线程
  3. private volatile int maximumPoolSize
    最大线程数
  4. private volatile RejectedExecutionHandler handler
    拒绝任务的处理策略,具体见下文
  5. private volatile long keepAliveTime
    非核心线程空闲后,可以存活多久
  6. private volatile int corePoolSize
    核心线程数
  7. private int largestPoolSize
    曾经同时运行过线程的最大数量
  8. private final BlockingQueue<Runnable> workQueue;
    等待处理的任务队列
  9. private final HashSet<Worker> workers;
    所有的工作线程

在这里插入图片描述

  • 线程池的执行过程(execute方法)
    在这里插入图片描述
  1. 调用execute方法提交任务时,首先判断工作线程数(workerCount)是否已达到核心线程数(corePoolSize),没达到,表明核心线程池未满,可以直接创建线程,执行任务,否则进行下一步。
  2. 判断等待队列(BlockingQueue)是否满,没满的话,加入等待队列,否则,进行下一步。
  3. 判断工作线程数(workerCount)是否已经达到线程池的最大容量(maximumPoolSize),没达到,创建线程,已达到,进行下一步。
  4. 此时线程池满了,等待队列也满了,需要按照指定的饱和策略(RejectedExecutionHandler),处理任务。
  • 饱和策略
  1. 在工作线程数量达到最大线程数量,并且等待队列也满的情况下,需要执行饱和策略(拒绝执行处理),有4种策略
策略解释
AbortPolicy终止策略,抛出RejectedExecutionException异常
CallerRunsPolicy直接在调用execute方法的线程执行,如主线程,执行时,主线程会被阻塞
DiscardPolicy不处理,直接丢弃,不抛异常,不执行
DiscardOldestPolicy从队列中弹出头部元素,然后继续执行execute方法
  1. 也可以自定义策略,实现如日志记录、持久化储存
static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        //自己的策略,如日志记录、持久化储存等
    }
}
  • 其他方法
  1. submit
    提交异步任务,Futrue对象获取返回值
  2. shutdown
    关闭线程池,不再接受新任务,但队列中的任务会继续执行完
  3. shutdownNow
    关闭线程池,不再接受新任务,队列中的任务不会执行,返回队列中等待的任务,正在执行的任务中断
  4. isShutdown
    调用shutdown后, isShutdown为true
  5. isTerminated
    调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

3.ScheduledThreadPoolExecutor

  • 主要用来在给定的延迟后运行任务,或者定期执行任务
  • 比Timer好在哪?
  1. Timer 对系统时钟的变化敏感,ScheduledThreadPoolExecutor不是;
  2. Timer 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。ScheduledThreadPoolExecutor 可以配置任意数量的线程。
  3. 在TimerTask 中抛出的运行时异常会杀死一个线程,从而导致计划任务将不再运行。ScheduledThreadExecutor 不仅捕获运行时异常,还允许您在需要时处理它们(通过重写 afterExecute 方法ThreadPoolExecutor),抛出异常的任务将被取消,但其他任务将继续运行。

4.Executors

  • Executors是一个工具类,可以方便的创建常见的线程池,如FixedThreadPool、SingleThreadExecutor、CachedThreadPool
线程池解释
FixedThreadPool固定大小的线程池
SingleThreadExecutor单线程的线程池
CachedThreadPool根据需要创建新线程的线程池
  • FixedThreadPool和SingleThreadExecutor使用无界队列(队列容量为Integer.MAX_VALUE),不会拒绝任何任务,在任务比较多时可能会OOM
  • CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。
  • 因此创建线程池不要使用Executors,应该直接使用ThreadPoolExecutor的构造方法

5. 确定线程池大小

  • 线程池大小对系统的影响?
  1. 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的!CPU 根本没有得到充分利用。
  2. 但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
  • 怎样确定线程池大小?
  1. CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

  2. I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值