Java 高级多线程

96 阅读4分钟

Java 高级多线程

线程池

为什么需要线程池?

  • 有非常多的任务需要多线程来完成,且每个线程不需要很长时间来运行,频繁的创建和销毁进程
  • 频繁创建和销毁线程比较消耗性能。有了线程池就不需要创建更多的线程来完成任务,因为线程可以重用

线程池原理

线程池用于维护一个队列,队列中保存着处于等待(空闲)状态的线程,不用每次都创建新的线程

线程池API

常用的线程池的接口和类在包java.util.concurrent

Executors:通过此类获得一个线程池

ThreadPool.png

⽅法名描述
newFixedThreadPool(int nThreads)获取固定数量的线程池。参数:指定线程池中线程的数量。
newCachedThreadPool()获得动态数量的线程池,如不够则创建新的,⽆上限。
newSingleThreadExecutor()创建单个线程的线程池,只有⼀个线程。
newScheduledThreadPool()创建固定⼤⼩的线程池,可以延迟或定时执⾏任务。

Callable接口

  • JDK5加入,与Runnable接口类似,实现之后代表一个线程任务
  • Callable具有泛型返回值、可以声明异常
public interface Callable<V>{
    public V call() throws Exception;
}

Callable接口的使用

public static void main(String[] args) throws ExecutionException, InterruptedException {
    Callable<Integer> callable = new Callable<>() {
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for (int i = 0; i <= 100; i++) {
                sum += i;
            }
            return sum;
        }
    };
    // 把Callable对象,转成可执行任务
    FutureTask<Integer> future = new FutureTask<>(callable);
    Thread thread = new Thread(future);
    thread.start();
    Integer sum = future.get();
    System.out.println(sum);
}
public static void main(String[] args) throws Exception {
    ExecutorService es = Executors.newFixedThreadPool(1);
    Future<Integer> result = es.submit(() -> {
            int sum = 0;
            for (int i = 0; i <= 100; i++) {
                sum += i;
            }
            return sum;
        }
    );
    Integer sum = result.get();
    System.out.println("1~100的和为:" + sum);
    es.shutdown();
}

Runnable接口与Callable接口的区别

  • Callable接口中call方法有返回值,Runnable接口中run方法没有返回值
  • Callable接口中call方法有声明异常,Runnable接口中run方法没有异常

Future接口

  • Future接口表示将要执行完任务的结果
  • get()以阻塞形式等待Future中的异步处理结果(Call()的返回值)

Lock接口

  • JDK5加入,与synchronized比较,显式定义,结构更灵活
  • 提供更多实用性方法,功能更强大,性能更优越

常用方法

⽅法名描述
void lock()获取锁,如锁被占⽤,则等待。
boolean tryLock()尝试获取锁(成功返回true。失败返回false,不阻塞)。
void unlock()释放锁。

重入锁

ReentrantLock:Lock接口的实现类,与sychronized一样具有互斥锁功能

读写锁

ReentrantReadWriteLock:

  • ⼀种⽀持⼀写多读的同步锁,读写分离,可分别分配读锁、写锁。
  • ⽀持多次分配读锁,使多个读操作可以并发执⾏。

互斥规则:

  • 写-写:互斥,阻塞。
  • 读-写:互斥,读阻塞写、写阻塞读。
  • 读-读:不互斥、不阻塞。
  • 在读操作远远⾼于写操作的环境中,可在保障线程安全的情况下,提⾼运⾏效率。
创建读写锁
// 创建读写锁
private ReentrantReadWriteLock rrw = new ReentrantReadWriteLock();
// 读锁
private ReentrantReadWriteLock.ReadLock readLock = rrw.readLock();
// 写锁
private ReentrantReadWriteLock.WriteLock writeLock = rrw.writeLock();
使用读写锁
readLock.lock();
readLock.unlock();

writeLock.lock();
writeLock.unlock();

线程安全集合

image.png

Collections⼯具类中提供了多个可以获得线程安全集合的⽅法。

注:JDK1.2提供,接⼝统⼀、维护性⾼,但性能没有提升,均以synchonized实现。

⽅法名
public static Collection synchronizedCollection(Collection c)
public static List synchronizedList(List list)
public static Set synchronizedSet(Set s)
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
public static SortedSet synchronizedSortedSet(SortedSet s)
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)

方法使用

// 以synchronizedList为例
ArrayList<String> arrayList = new ArrayList<>();
List<String> list = Collections.synchronizedList(arrayList);

CopyOnWriteArrayList

  • 线程安全的ArrayList,加强版读写分离。
  • 写有锁,读⽆锁,读写之间不阻塞,优于读写锁。
  • 写⼊时,先copy⼀个容器副本、再添加新元素,最后替换引⽤。
  • 使⽤⽅式与ArrayList⽆异。
使用
CopyOnWriteArrayList<String> strings = new CopyOnWriteArrayList<>();

CopyOnWriteArraySet

  • 线程安全的Set,底层使⽤CopyOnWriteArrayList实现。
  • 唯⼀不同在于,使⽤addIfAbsent()添加元素,会遍历数组。
  • 存在元素,则不添加扔掉副本)。
使用
CopyOnWriteArraySet<String> syncSet = new CopyOnWriteArraySet<>();

ConcurrentHashMap

  • 初始容量默认为16段(Segment),使⽤分段锁设计。
  • 不对整个Map加锁,⽽是为每个Segment加锁。
  • 当多个对象存⼊同⼀个Segment时,才需要互斥。
  • 最理想状态为16个对象分别存⼊16个Segment,并⾏数量16。
  • 使⽤⽅式与HashMap⽆异。

Queue

Collection的⼦接⼝,表示队列FIFO(First In First Out)。

常用推荐方法
⽅法名描述
boolean offer(E e)顺序添加⼀个元素 (到达上限后,再添加则会返回false)。
E poll()获得第⼀个元素并移除 (如果队列没有元素时,则返回null)。
E peek()获得第⼀个元素但不移除 (如果队列没有元素时,则返回null)。
ConcurrentLinkedQueue
  • 线程安全、可⾼效读写的队列,⾼并发下性能最好的队列。
  • ⽆锁、CAS⽐较交换算法,修改的⽅法包含三个核⼼参数(V,E,N)。
  • V:要更新的变量、E:预期值、N:新值。
  • 只有当V==E时,V=N;否则表示已被更新过,则取消当前操作。
BolckingQueue
  • Queue的⼦接⼝,阻塞的队列,增加了两个线程状态为⽆限期等待的⽅法。
  • 可⽤于解决⽣产⽣、消费者问题。
常用方法
⽅法名描述
void put(E e)将指定元素插⼊此队列中,如果没有可⽤空间,则等待。
E take()获取并移除此队列头部元素,如果没有可⽤元素,则等待。
ArrayBlockingQueue
  • 数组结构实现,有界队列。
  • ⼿⼯固定上限。
LinkedBlockingQueue
  • 链表结构实现,⽆界队列。
  • 默认上限Integer.MAX_VALUE。
  • 使⽤⽅法和ArrayBlockingQueue相同。