10.1 程序、进程、线程

程序(program)是为完成特定任务、用某种语言编写的一段静态代码。

进程(process)是程序的一次执行。进程是资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

线程(thread)是进程的进一步细化。若一个进程可以同时并行执行多个线程,就是支持多线程的。线程作为调度和执行的单位,拥有独立的运行栈和程序计数器(pc),线程切换的开销更小。一个进程中的多个线程共享相同的内存单元,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享资源可能带来安全隐患。

并行:多个CPU同时执行多个任务。

并发:一个CPU以时间片为单位轮流执行多个任务,宏观上看是同时执行的。

何时需要多线程:

  • 程序需要同时执行两个或多个任务;
  • 程序需要执行耗时操作,如用户输入、文件读写操作、网络操作、搜索等;
  • 程序需要后台运行。

    10.2 线程的生命周期

    image.png
    新建状态:Thread被创建时

就绪状态:线程对象调用start()之后,此时它已具备了运行的条件,只是没分配到CPU。

运行状态:就绪状态的线程获得 CPU 资源并执行 run()。

阻塞状态:线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源。在睡眠时间已到或获得资源后可以重新进入就绪状态。

死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

10.3 线程创建和使用

每个线程都通过run()方法来完成操作的,run()由JVM自动调用,不可确定时间。通过对象的start()来启动,start()仅可调用一次。

  1. // Thread类的部分构造器
  2. Thread()
  3. Thread(String threadname)
  4. Thread(Runnable target):指定创建线程的目标对象,它实现了Runnablerun方法

方法一:继承Thread类

  1. 创建子类继承Thread;
  2. 重写Thread类的run()方法,写明线程执行逻辑
  3. 创建Thread类对象并调用start方法启动线程

方法二:实现Runnable接口

  1. 定义声明Runnable接口的类
  2. 重写run方法
  3. 将声明Runnable接口的子类对象作为实际参数传递给Thread类的构造器
  4. 调用Thread类的start方法启动线程

两种创建方式的对比:
Runnable天然就可实现多个线程共享同一个接口实现类的对象,非常适合多个相同线程处理共享资源,继承实现就必须加static。开发当中优先选择Runnable方式。

10.4 线程调度

Java的调度方法:同优先级线程组成先进先出队列,使用时间片策略。对高优先级,使用优先调度的抢占策略。

Java 线程优先级取值范围是整数 1(Thread.MIN_PRIORITY )到10 (Thread.MAX_PRIORITY )。默认优先级为 NORM_PRIORITY(5),线程在创建时继承父线程的优先级。线程优先级不能保证线程执行顺序,非常依赖于平台。

10.5 线程的有关方法

  1. void start():启动线程,并执行对象的run()方法
  2. run():线程被调度时的执行逻辑
  3. String getName()
  4. void setName(String name)
  5. static Thread currentThread():返回当前线程。
  6. static void yield():线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
  7. join() :调用join()方法的线程执行之前,其他线程都将被阻塞,等同于将并发改为串行
  8. static void sleep(long millis): 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
  9. stop():强制线程生命期结束,不推荐使用
  10. boolean isAlive()
  11. getPriority()
  12. setPriority()

10.6 线程的同步

当多个线程在操作同一共享数据时,一个线程的语句只执行了一部分,另一个线程就开始执行。这有可能会导致共享数据发生错误,产生线程不安全问题。
image.png
解决办法:对操作共享数据的多条语句,要求一个线程在执行完之前其他线程不可参与执行。Java提供了同步机制。

  1. // 同步代码块
  2. synchronized(同步监视器){操作共享数据的代码}
  3. // 同步方法
  4. public synchronized void show (){...}

同步监视器,俗称“锁”。任何一个类对象都可以作为锁,同步的多个线程必须使用同一把“锁”。在声明Runnable接口的类中,直接在run方法的同步监视器中传入this即可保证多个此线程并发时的线程安全。使用继承Thread的方法需要将同步方法声明为static,同步监视器是对象本身。使用synchronized包含的代码块既不能多也不能少,多了导致性能下降,少了则不能保证同步。

只有在线程的同步代码执行结束或在同步代码中执行了wait()方法才会释放锁;线程执行同步代码时调用sleep()、yield()暂停当前线程或其他线程调用了该线程的suspend()方法将该线程挂起均不会释放锁。

为了防止死锁问题的出现,应尽量减少同步资源的定义、避免使用嵌套同步。

Lock(锁)通过显示定义同步锁来实现同步,同步锁使用Lock对象充当。ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,可以显式加锁和释放锁。

  1. class A{
  2. private final ReentrantLock lock = new ReenTrantLock();
  3. public void m(){
  4. lock.lock();
  5. try{
  6. //保证线程安全的代码;
  7. }
  8. finally{
  9. lock.unlock();
  10. }
  11. }
  12. }
  13. // 注意:一定要使用unlock方法释放锁,否则线程同步将不会停止。如果同步代码有异常,要将unlock()写入finally语句块
  • synchronized与Lock的比较:
    • Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放。
    • Lock只有代码块锁,synchronized有代码块锁和方法锁
    • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且提供了更多子类。

优先使用顺序:Lock -> 同步代码块 -> 同步方法

10.7 线程通信

例:使用两个线程打印1-100。线程1、2交替打印。

  1. class Communication implements Runnable {
  2. int i = 1;
  3. public void run() {
  4. while (true) {
  5. synchronized (this) {
  6. notify();
  7. if (i <= 100) {
  8. System.out.println(Thread.currentThread().getName() + ":" + i++);
  9. } else
  10. break;
  11. try {
  12. wait();
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }
  18. }
  19. }

一开始两个线程均处于就绪状态,当线程A抢到资源时,其执行notify唤醒线程B,由于锁的原因,B只能等待,当A执行wait阻塞并释放了锁,此时没有和B抢资源的线程,B必定获得资源,因此B进入又唤醒了A。

  • wait():令当前线程挂起并放弃CPU、锁并等待,直到被另一线程对象唤醒为止。
  • notify:唤醒正在排队等待的同步资源的线程中优先级最高的线程
  • notifyAll:唤醒正在排队等待资源的所有线程

这三个方法只有在线程同步方法和代码块中才能使用,而且由同步监视器进行调用。因为这三个方法必有锁对象调用,而任意对象都可以作为同步锁,因此这三个方法在Object类中声明。

  • sleep和wait方法的异同
    • 相同点:一旦执行方法,都可使得当前线程进入阻塞状态
    • 不同点:两个方法声明的位置不同,Thread类中声明sleep,object类中声明wait
    • sleep可在任何需要的场景下调用,wait只能在同步代码块或同步方法中调用
    • 如果两个方法都使用在同步代码块或同步方法中,sleep不会释放锁,wait会释放锁 ```java 生产者/消费者问题 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果满了,店员会叫停生产者,如果有空位了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

这里可能出现两个问题:

生产者比消费者快时,消费者会漏掉一些数据没有取到。 消费者比生产者快时,消费者会取相同的数据。

分析: 共享数据:产品(线程同步) 共享操作:生产产品(生产者线程)与消费产品(消费者线程) 涉及到满或空时,唤醒或停止线程(线程通信) 涉及对象Productor(线程)、Customer(线程)、Clerk(由生产者和消费者公用,存放共享数据与同步代码块)

  1. ```java
  2. class Clerk { // 售货员
  3. private int product = 0; // 共享数据
  4. // 下面的两个方法进行同步,是共享的操作,同步监视器是Clerk
  5. // 生产产品
  6. public synchronized void addProduct() {
  7. if (product >= 20) { // 超出则停止生产,等待唤醒
  8. try {
  9. wait();
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. } else {
  14. product++;
  15. System.out.println("生产者生产了第" + product + "个产品");
  16. notifyAll(); // 唤醒对面的消费者
  17. }
  18. }
  19. // 消费产品
  20. public synchronized void getProduct() {
  21. if (this.product <= 0) {
  22. try {
  23. wait(); // 不足等待唤醒
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. } else {
  28. System.out.println("消费者取走了第" + product + "个产品");
  29. product--;
  30. notifyAll(); // 取走则唤醒对面的生产者
  31. }
  32. }
  33. }
  34. class Productor implements Runnable { // 生产者
  35. Clerk clerk;
  36. public Productor(Clerk clerk) {
  37. this.clerk = clerk;
  38. }
  39. public void run() {
  40. System.out.println("生产者开始生产产品");
  41. while (true) {
  42. try {
  43. Thread.sleep((int) Math.random() * 1000);
  44. } catch (InterruptedException e) {
  45. e.printStackTrace();
  46. }
  47. clerk.addProduct();
  48. }
  49. }
  50. }
  51. class Consumer implements Runnable { // 消费者
  52. Clerk clerk;
  53. public Consumer(Clerk clerk) {
  54. this.clerk = clerk;
  55. }
  56. public void run() {
  57. System.out.println("消费者开始取走产品");
  58. while (true) {
  59. try {
  60. Thread.sleep((int) Math.random() * 1000);
  61. } catch (InterruptedException e) {
  62. e.printStackTrace();
  63. }
  64. clerk.getProduct();
  65. }
  66. }
  67. }
  68. public class ProductTest {
  69. public static void main(String[] args) {
  70. Clerk clerk = new Clerk();
  71. Thread productorThread = new Thread(new Productor(clerk));
  72. Thread consumerThread = new Thread(new Consumer(clerk));
  73. productorThread.start();
  74. consumerThread.start();
  75. }
  76. }

10.8 JDK5.0新增用于创建线程方式

新增方式一:声明Callable接口
该方法相比run方法可以有返回值;可以抛出异常;支持泛型;需要借助FutureTask类。

Future接口可以对Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等,FutureTask同时实现了Runnable和Future接口,它既可以作为Runnable被线程执行,也可以作为Future得到的Callable的返回值

  1. class NumThread implements Callable{
  2. //实现callable接口并实现其call方法
  3. public Object call() throws Exception{
  4. int sum = 0;
  5. return sum; // 返回对象
  6. }
  7. }
  8. public class ThreadNew{
  9. public static void main(String[] args){
  10. // 创建实现Callable接口的对象
  11. NumThread numThread = new NumThread;
  12. // 创建FutureTask对象
  13. FutureTask futureTask = new FutureTask(numThread);
  14. // 放入Runnable接口后启动线程
  15. new Thread(futureTask).start();
  16. try{
  17. // 获取线程执行返回值
  18. Object sum = futureTask.get();
  19. }
  20. catch(Exception e){}
  21. }
  22. }

新增方式二:使用线程池
经常创建和销毁线程将会快速的消耗系统资源,对性能影响大。提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,实现重复利用,从而提高响应速度、降低资源消耗、便于线程管理。

  1. ExecutorService:线程池接口。常见子类ThreadPoolExecutor
  2. void execute(Runnable command) :执行一个指定线程任务,没有返回值
  3. <T> Future<T> submit(Callable<T> task):执行任务,有返回值
  4. void shutdown() :关闭连接池
  5. Executors:工具类,用于创建并返回不同类型的线程池,返回ExecutorService
  6. Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
  7. Executors.newFixedThreadPool(n); 创建一个固定线程数的线程池
  8. Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
  9. Executors.newScheduledThreadPool(n):创建在给定延迟后运行命令或者定期执行的线程池。

可以将ExecutorService强制转化为ThreadPoolExecutor,这样可以在执行前调用一些set方法进行属性设置,对线程进行管理。如:corePoolSize(核心池的大小),maximunPoolSize(最大线程数),keepAliveTime(线程没有任务时最多保持多长时间后停止)等。