多线程基础知识--线程并发库


Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包。这个包包含有一系列能够让 Java 的并发编程变得更加简单轻松的类。在这个包被添加以前,你需要自己去动手实现自己的相关工具类。下面带你认识下java.util.concurrent 包里的这些类,然后你可以尝试着如何在项目中使用它们。本文中将使用 Java 6 版本,我不确定这和 Java 5 版本里的是否有一些差异。我不会去解释关于 Java 并发的核心问题 – 其背后的原理,也就是说,如果你对那些东西感兴趣,参考《Java 并发指南》。

1 Java 的线程并发库介绍

Java5 的多线程并有两个大发库在 java.util.concurrent 包及子包中,子包主要的包有一下两个

1) java.util.concurrent 包 (多线程并发库)

  • java.util.concurrent包含许多线程安全、测试良好、高性能的并发构建块。不客气地说,创建java.util.concurrent 的目的就是要实现Collection 框架对数据结构所执行的并发操作。通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性,后面、我们会做介绍
  • 如果一些类名看起来相似,可能是因为 java.util.concurrent 中的许多概念源自 Doug Lea 的util.concurrent 库。

2) java.util.concurrent.atomic 包 (多线程的原子性操作提供的工具类)

  • 查看 atomic 包文档页下面的介绍,它可以对多线程的基本数据、数组中的基本数据和对象中的基本数据进行多线程的操作(AtomicInteger、AtomicIntegerArray、AtomicIntegerFieldUpDater…)

  • 通过如下两个方法快速理解 atomic 包的意义:

    • 1) AtomicInteger 类的 boolean compareAndSet(expectedValue,
      updateValue);
    • 2)AtomicIntegerArray 类的 int addAndGet(int i, int delta);
  • 顺带解释 volatile 类型的作用,需要查看 java 语言规范。

  • 1)volatile 修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。(具有可见性)

  • 2)volatile 没有原子性。

3) java.util.concurrent.lock 包 (多线程的锁机制)

为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。该框架允许更灵活地使用锁和条件。本包下有三大接口,下面简单介绍下:

  • Lock 接口:支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand over-hand
    和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。
  • ReadWriteLock 接口:以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
  • Condition 接口:描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。

2 Java 的并发库入门

下面我们将分别介绍 java.util.concurrent 包下的常用类的使用。

1) java.util.concurrent 包

java.util.concurrent 包描述:
在并发编程中很常用的实用工具类。此包包括了几个小的、已标准化的可扩展框架,以及一些提供有用功能的类。此包下有一些组件,其中包括:

  • 执行程序(线程池)
  • 并发队列
  • 同步器
  • 并发 Collocation

下面我们将 java.util.concurrent 包下的组件逐一简单介绍:

A. 执行程序

Executors 线程池工厂类

线程池的作用

线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程 排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程 池中有等待的工作线程,就可以开始运行了;否则进入等待队列

为什么要用线程池

  • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
  • 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)

Executors 详解

Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。ThreadPoolExecutor 是 Executors 类的底层实现。我们先介绍下 Executors。
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源
Java5 中并发库中,线程池创建线程大致可以分为下面三种:

//创建固定大小的线程池
ExecutorService fPool = Executors.newFixedThreadPool(3);
//创建缓存大小的线程池
ExecutorService cPool = Executors.newCachedThreadPool();
//创建单一的线程池
ExecutorService sPool = Executors.newSingleThreadExecutor();

下面我们通过简单示例来分别说明:
固定大小连接池

package com.wjl.test.mythreadLocal;

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

/**
 * 固定大小的线程池
 */
public class SizeThreadPool {
    public static void main(String[] args) {
        //创建一个可重用固定线程数的线程池
        ExecutorService pool = Executors.newFixedThreadPool(2);
        //创建实现了 Runnable 接口对象,Thread 对象当然也实现了 Runnable 接口
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        Thread t3 = new MyThread();
        Thread t4 = new MyThread();
        Thread t5 = new MyThread();
        //将线程放入池中进行执行
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);
        pool.execute(t5);
        //关闭线程池
        pool.shutdown();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"正在执行。。。");
    }
}
运行结果:
pool-1-thread-2正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-2正在执行。。。

从上面的运行来看,我们 Thread 类都是在线程池中运行的,线程池在执行 execute 方法来执行 Thread 类中的 run 方法。不管 execute 执行几次,线程池始终都会使用 2 个线程来处理。不会再去创建出其他线程来处理run 方法执行。这就是固定大小线程池。

单任务连接池

 ExecutorService pool = Executors.newSingleThreadExecutor();

补充:在 java 的多线程中,一但线程关闭,就会成为死线程。关闭后死线程就没有办法在启动了。再次启动就会出现异常信息:Exception in thread “main” java.lang.IllegalThreadStateException。那么如何解决这个问题呢?我们这里就可以使用 Executors.newSingleThreadExecutor()来再次启动一个线程。(面试)

可变连接池

//创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
        ExecutorService pool = Executors.newCachedThreadPool();
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-3正在执行...
pool-1-thread-4正在执行...
pool-1-thread-5正在执行...

运行结果看出,可变任务线程池在执行 execute 方法来执行 Thread 类中的 run 方法。这里 execute 执行多次,线程池就会创建出多个线程来处理 Thread 类中 run 方法。所有我们看到连接池会根据执行的情况,在程序运行时创建多个线程来处理,这里就是可变连接池的特点。

那么在上面的三种创建方式,Executors 还可以在执行某个线程时,定时操作。那么下面我们通过代码简单演示
下。
延迟连接池

package com.wjl.test.mythreadLocal;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 延时连接池
 */
public class DelayThreadPool {
    public static void main(String[] args) {
        //创建一个线程池,它可以安排在给定延时后运行命令或者定期的执行
        ScheduledExecutorService pool= Executors.newScheduledThreadPool(2);
        //创建实现了 Runnable 接口对象,Thread 对象当然也实现了 Runnable 接口
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        Thread t3 = new MyThread();
        Thread t4 = new MyThread();
        Thread t5 = new MyThread();
        //把线程放入池中进行执行
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        //使用定时执行风格的方法
        pool.schedule(t4, 10000, TimeUnit.MILLISECONDS); //t4 和 t5 在 10 秒后执行
        pool.schedule(t5, 10000, TimeUnit.MILLISECONDS);
        //关闭线程池
        pool.shutdown();
    }
}
运行结果:
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-1正在执行...

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值