Java复习(十四)----多线程(四)

前段时间有事情,已经好久没有更新过了,现在我们继续来看看多线程的最后一部分

ReentrantLock

前面我们讲了synchronized关键字来加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制

java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁,我们来看一下传统的synchronized代码:

public class Test{
    private int count;

    public void add(int n) {
        synchronized(this) {
            count += n;
        }
    }
}

如果用ReentrantLock替代,可以把代码改造为:

public class Test{
    private final Lock lock = new ReentrantLock();
    private int count;

    public void add(int n) {
        lock.lock();
        try {
            count += n;
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock实现的锁,我们必须先获取锁,然后在finally中正确释放锁。

顾名思义,ReentrantLock是可重入锁,它和synchronized一样,一个线程可以多次获取同一个锁

synchronized不同的是ReentrantLock可以尝试获取锁

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。
所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁

小结:

  • ReentrantLock可以替代synchronized进行同步;
  • ReentrantLock获取锁更安全;
  • 必须先获取到锁,再进入try {…}代码块,最后使用finally保证释放锁;
  • 可以使用tryLock()尝试获取锁。

Condition

synchronized可以配合wait和notify实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock我们怎么编写wait和notify的功能呢?
答案是使用Condition对象来实现wait和notify的功能。

class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue<String> queue = new LinkedList<>();

    public void addTask(String s) {
        lock.lock();
        try {
            queue.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String getTask() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}

使用Condition时,引用的Condition对象必须从Lock实例newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。

Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的:

  • await()会释放当前锁,进入等待状态;
  • signal()会唤醒某个等待线程;
  • signalAll()会唤醒所有等待线程;
  • 唤醒线程从await()返回后需要重新获得锁。

对于锁,还有悲观锁和乐观锁,大家可以去看看这位大佬写的博客,很详细很有用。传送门

线程池

当一个程序中若创建大量线程,并在任务结束之后销毁,这样会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃。那我们要怎么解决呢?这时我们便可以使用线程池来解决这个问题。

那什么是线程池呢?线程池的作用是什么呢?我们要怎么创造线程池呢?

什么是线程池

首先创建一些线程,他们的集合成为线程池,当服务器接受到一个客户的请求之后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中

在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池拿到任务之后,他就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务

线程池大家可以将它当做为美团平台,其中的线程为骑手。当平台接到客户的订单之后(任务是提交给整个线程池),他就会找看看有哪个骑手是空闲的(在内部找有无空闲的线程),找到之后,就把订单分给这个骑手。骑手接完一单之后肯定不会下班的吧,人家还要继续赚钱(这就是服务完之后不关闭该线程),一个骑手一次只能接一次单,接完一单才能接下一单,骑手在配送中,客户可能会叫骑手顺路买点东西的吧,这时骑手为了五星好评,只要不是太难,基本都会答应(一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务)。

线程池的作用

线程池有两个主要的作用:

  1. 控制线程数量
  2. 重用线程

控制线程数量呢,就是控制一下骑手的数量,不可能所有人都成为骑手吧,这样就乱套了,所以得控制一下数量。重用线程呢,每个骑手不可能上了一天的班就辞职是吧,他们得赚钱,得养家。

线程池的实现

Java标准库提供了ExecutorService接口表示线程池

线程池有以下几个实现方法:

  • 创建一个可重用的固定线程集合的线程池,以共享的无界队列方式来运行这些线程(线程数固定的线程池):
Executors.newFixedThreadPool(int nThreads);

我们来看一个小栗子:

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 创建一个固定大小为4的线程池:
        ExecutorService es = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 6; i++) {
            es.submit(new Task("" + i));
        }
        // 关闭线程池:
        es.shutdown();
    }
}

class Task implements Runnable {
    private final String name;

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

    @Override
    public void run() {
        System.out.println("start task " + name);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        System.out.println("end task " + name);
    }
}

我们一次性放入6个任务,由于线程池只有固定的4个线程,因此,前4个任务会同时执行,等到有线程空闲后,才会执行后面的两个任务

这里说一下:
线程池在程序结束的时候要关闭。使用shutdown()方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow()会立刻停止正在执行的任务awaitTermination()则会等待指定的时间让线程池关闭

  • 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用他们(线程数根据任务动态调整的线程池):
Executors.newCashedThreadPool();

如果我们把线程池改为CachedThreadPool,由于这个线程池的实现会根据任务数量动态调整线程池的大小,所以6个任务可一次性全部同时执行。

如果我们想把线程池的大小限制在4~10个之间动态调整怎么办?我们查看Executors.newCachedThreadPool()方法的源码:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}

因此,想创建指定动态范围的线程池,可以这么写:

int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,
        60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
  • 创建一个线程池,他可安排给定延迟后运行命令或者定期地执行:
Executors.newScheduledThreadPool(int corePoolSize);

ScheduledThreadPool需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的

创建一个ScheduledThreadPool仍然是通过Executors类:

ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);

对于ScheduledThreadPool的理解,大家可以看看这篇博客传送门

  • 创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程:
Executor.newSingleThreadExecutor();

使用BlockingQueue

对于BlockingQueue双缓冲队列,因为知识点有点多,我就不写了,以后有时间的话我会写一下。这里给大家推荐几篇关于BlockingQueue的博客,
传送门1
传送门2

对于多线程的其他小知识,大家可以去我的主页看看,算上这篇总共写了四篇。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值