多线程那点事

@生活冷战士
用最朴实的语言写出最有用的知识

与我一起调BUG吧

线程与进程

进程:资源分配和调度的基本单位。
线程:CPU调度和分配的基本单位。
关系:线程是进程的实体。一个进程可以有多个线程,称为多线程。
多线程的优势:

  1. 更好的用户体验,
  2. 最大限度的提高系统利用效率。

线程创建四种方式

  • 继承Thread类;
    代码整起
package com.thread;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

/**
 * @author chengzh
 */
public class TestThread extends Thread{
    String url;
    String name;
    public TestThread(String url,String name){
        this.url=url;
        this.name=name;
    }
    @Override
    public void run() {
        WebDownLoad webDownLoad=new WebDownLoad();
        webDownLoad.download(url,name);
        System.out.println("下载了文件名为:"+name);
    }

    public static void main(String[] args) {
        TestThread t1= new TestThread("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1604982542947&di=564d263bedac82f6ffb4bb2b34124059&imgtype=0&src=http%3A%2F%2Fpn.gexing.com%2Fshaitu%2F20120803%2F1257%2F501b5a2e136dd.jpg","1.jpg");
        TestThread t2= new TestThread("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1604982542947&di=564d263bedac82f6ffb4bb2b34124059&imgtype=0&src=http%3A%2F%2Fpn.gexing.com%2Fshaitu%2F20120803%2F1257%2F501b5a2e136dd.jpg","2.jpg");
        TestThread t3= new TestThread("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1604982542947&di=564d263bedac82f6ffb4bb2b34124059&imgtype=0&src=http%3A%2F%2Fpn.gexing.com%2Fshaitu%2F20120803%2F1257%2F501b5a2e136dd.jpg","3.jpg");
        t1.start();
        t2.start();
        t3.start();
    }
}
/**
 * 下载器
 */

class WebDownLoad{
    /**
     * 下载方法
     * @param url
     * @param name
     */
    public void download(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("io异常,download方法下载异常");
        }
    }
}

  • 实现Runable接口;
package com.thread;

/**
 * @author chengzh
 */
public class TestThread1 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i <= 200 ; i++) {
            System.out.println("我在看代码"+i);
        }
    }

    public static void main(String[] args) {
        TestThread1 thread1 = new TestThread1();
        new Thread(thread1).start();
        for (int i = 0; i <= 300 ; i++) {
            System.out.println("我在看Video"+i);
        }
    }
}



  • 实现Callable接口。
package com.thread;


import java.util.concurrent.*;

/**
 * @author chengzh
 */
public class TestCallable implements Callable <Boolean>{
    String url;
    String name;
    public TestCallable(String url,String name){
        this.url=url;
        this.name=name;
    }
    @Override
    public Boolean call() throws Exception {
        WebDownLoad webDownLoad=new WebDownLoad();
        webDownLoad.download(url,name);
        System.out.println("下载了文件名为:"+name);
        return true;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1= new TestCallable("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1604982542947&di=564d263bedac82f6ffb4bb2b34124059&imgtype=0&src=http%3A%2F%2Fpn.gexing.com%2Fshaitu%2F20120803%2F1257%2F501b5a2e136dd.jpg","1.jpg");
        TestCallable t2= new TestCallable("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1604982542947&di=564d263bedac82f6ffb4bb2b34124059&imgtype=0&src=http%3A%2F%2Fpn.gexing.com%2Fshaitu%2F20120803%2F1257%2F501b5a2e136dd.jpg","2.jpg");
        TestCallable t3= new TestCallable("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1604982542947&di=564d263bedac82f6ffb4bb2b34124059&imgtype=0&src=http%3A%2F%2Fpn.gexing.com%2Fshaitu%2F20120803%2F1257%2F501b5a2e136dd.jpg","3.jpg");
        /**
         *创建执行服务
         */
        ExecutorService ser = Executors.newFixedThreadPool(3);
        /**
         * 提交执行
         */
        Future<Boolean> r1 = ser.submit(t1);
        Future<Boolean> r2 = ser.submit(t2);
        Future<Boolean> r3 = ser.submit(t3);
        /**
         * 获取结果
         */
        boolean res1 = r1.get();
        boolean res2 = r2.get();
        boolean res3 = r3.get();
        /**
         * 关闭服务
         */
        ser.shutdownNow();
    }
}



  • 线程池

线程的状态

拿图说话在这里插入图片描述

(1)新生状态: Born 创建线程对象之后,尚未调用其 start()方法之前,这个线程就有了生命,此时线程仅仅 是一个空对象,系统没有为其分配资源。此时只能启动和终止线程,任何其他操作都会引发 异常。

(2)运行状态:Run 当调用 start()方法启动线程之后,系统为该线程分配除 CPU 以外的所需资源,这个线程 就有了运行的机会,线程处于可运行的状态,在这个状态当中,该线程对象可能正在运行, 也可能尚未运行。对于只有一个 CPU 的机器而言,任何时刻只能有一个处于可运行状态的 线程占用处理机,获得 CPU 资源,此时系统真正运行线程的 run()方法。

(3)阻塞状态:Blocked 一个正在运行的线程因某种原因不能继续运行时,进入阻塞状态。阻塞状态是一种“不 可运行”的状态,而处于这种状态的线程在得到一个特定的事件之后会转回可运行状态。导 致一个线程被阻塞有以下原因:
 调用了 Thread 类的静态方法 sleep()。

 一个线程执行到一个 IO 操作时,如果 IO 操作尚未完成,则线程将被阻塞。

 如果一个线程的执行需要得到一个对象的锁,而这个对象的锁正被别的线程占用, 那么此线程会被阻塞。

 线程的 suspend()方法被调用而使线程被挂起时,线程进入阻塞状态。但 suspend() 容易导致死锁,已经被 JDK 列为过期方法,基本不再使用。 处于阻塞状态的线程可以转回可运行状态,例如,在调用 sleep()方法之后,这个线程的 睡眠时间已经达到了指定的间隔,那么它就有可能重新回到可运行状态。或当一个线程等待 的锁变得可用的时候,那么这个线程也会从被阻塞状态转入可运行状态。

(4)死亡状态:Dead 一个线程的 run()方法运行完毕、stop()方法被调用或者在运行过程中出现未捕获的异常 时,线程进入死亡状态。

线程调度

当同一时刻有多个线程处于可运行状态,它们需要排队等待 CPU 资源,每个线程会自 动获得一个线程的优先级(Priority),优先级的高低反映线程的重要或紧急程度。可运行状 态的线程按优先级排队,线程调度依据建立在优先级基础上的“先到先服务”原则。 线程调度管理器负责线程排队和在线程间分配 CPU,并按线程调度算法进行调度。当线 程调度管理器选中某个线程时,该线程获得 CPU 资源进入运行状态。 线程调度是抢占式调度,即在当前线程执行过程中如果有一个更高优先级的线程进入可 运行状态,则这个更高优先级的线程立即被调度执行。

.线程优先级

线程的优先级用 1~10 表示,10 表示优先级最高,默认值是 5。
每个优先级对应一个 Thread 类的公用静态常量。
例如:
public static final int NORM_PRIORITY=5;
public static final int MIN_PRIORITY=1;
public static final int MAX_PRIORITY=10;
每个线程的优先级都介于 Thread.MIN_PRIORITY 和 Thread.MAX_PRIORITY 之间。
线程的 优先级可以通过 setPriority(int grade)方法更改,此方法的参数表示要设置的优先级,它必须
是一个 1~10 的整数。例如,t.setPriority(3);将线程对象 t 的优先级别设置为 3。

. 实现线程调度的方法

(1)join()方法 join()方法使当前线程暂停执行,等待调用该方法的线程结束后再继续执行本线程。它 有 3 种重载形式。
public final void join();
public final void join(long mills);
public final void join(long mills, int nanos);
下面通过示例具体介绍 join()方法的应用
示例 3 使用 join()方法阻塞线程。 实现步骤如下。
1)定义线程类,输出 5 次当前线程的名称。
2)定义测试类,使用 join()方法阻塞主线程。

    /**
     * 插队
     */
    static class MyJoin implements Runnable{

        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println(Thread.currentThread().getName()+"VIP线程开始执行"+i);
            }

        }
    }

    MyJoin myJoin = new MyJoin();
        Thread thread = new Thread(myJoin);
        thread.start();
        for (int i = 0; i < 1000; i++) {
            if(i==100){
                thread.join();
            }
            System.out.println("main开始执行"+i);
        }

注意:使用 join()方法阻塞指定的线程直到另一个线程完成以后再继续执行。
其 中 t.join()表示让当前线程(主线程)加到 t 线程的末尾,主线程被阻塞,t 线程执行完以后 主线程才能继续执行。

  • 从线程返回数据时也经常使用到 join()方法。
  • 使用 join()方法实现两个线程间的数据传递。
public class MyThread4 extends Thread {
	public String firstName;
	 public String lastName;
	  @Override public void run() { 
	    firstName = "双鞭大将"; 
	    lastName="呼延灼"; 
	    } 
	  }
	  public class Test4 { public static void main(String[] args) { 
	   MyThread4 t = new MyThread4(); t.start();
	   System.out.println("firstName:"+t.firstName); 					
	   System.out.println("lastName:"+t.lastName); 
	 } 
}

(2)sleep()方法
sleep()方法的语法格式如下。
public static void sleep(long millis)
sleep()方法会让当前线程睡眠(停止执行)millis 毫秒,
线程由运行中的状态进入不可 运行状态,睡眠时间过后线程会再次进入可运行状态。
示例 5 使用 sleep()方法阻塞线程,实现在控制台中每隔一秒输出一次系统时间。
实现步骤如下。
1)定义线程。
2)在 run()方法中使用 sleep()方法阻塞线程。

public class TestSleep implements Runnable{
    private int ticketNums = 10;
    @Override
    public void run() {
        while(true){
            if(ticketNums <= 0){
                break;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"---get"+ticketNums-- +"票");
        }
    }

(3)yield()方法
yield()方法的语法格式如下。
public static void yield()

  • yield()方法可让当前线程暂停执行,允许其他线程执行,但该线程仍处于可运行状态, 并不变为阻塞状态
  • 系统选择其他相同或更高优先级线程执行,若无其他相同或更高 优先级线程,则该线程继续执行!
  • 使用 yield()方法暂停线程
    /**
     *  Thread.yield();暂停线程
     */
    static class MyYield implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"线程开始执行");
            Thread.yield();
            System.out.println(Thread.currentThread().getName()+"线程停止执行");
        }
    }

  • 调用了 yield()方法之后,当前线程并不是转入被阻塞状态,它可以与其他 等待执行的线程争 CPU 资源,如果此时它又抢占到 CPU 资源,就会出现连续运行几次的情 况。
  • sleep()方法与 yield()方法在使用时容易混淆,这两个方法之间的区别

在这里插入图片描述

守护线程

  • 进程结束(程序退出)的标志是进程中所有的线程都停止。
  • 但子线程在不断进行监视, 导致程序无法终止,这时需要将子线程标记为守护线程,在调用 start()方法之前,
  • 调用 setDaemon(true)可以将线程标记为守护线程。 在所有非守护线程结束后,程序就会终止执行。
package com.thread;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author chengzh
 */
public class TestSleep implements Runnable{
    private int ticketNums = 10;
    @Override
    public void run() {
        while(true){
            if(ticketNums <= 0){
                break;
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"---get"+ticketNums-- +"票");
        }
    }

    /**
     * 倒计时
     * @throws InterruptedException
     */
    public void TenDown() throws InterruptedException {
        int num = 10;
        while(true){
            Thread.sleep(1000);
            System.out.println(num--);
            if(num==0){
                break;
            }
        }

    }

    /**
     *  Thread.yield();暂停线程
     */
    static class MyYield implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"线程开始执行");
            Thread.yield();
            System.out.println(Thread.currentThread().getName()+"线程停止执行");
        }
    }

    /**
     * 插队
     */
    static class MyJoin implements Runnable{

        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println(Thread.currentThread().getName()+"VIP线程开始执行"+i);
            }

        }
    }

    /**\
     * 线程优先级
     */
    static class ThreadPolicy implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"--->"+
                    Thread.currentThread().getPriority());
        }
    }

    /**
     * 守护线程
     */
    static class Daemon implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"--->"+
                    Thread.currentThread().getPriority());
        }
    }
    static class God implements Runnable{

        @Override
        public void run() {
            while(true){
                System.out.println("上帝保佑着你");
            }
        }
    }
    static class You implements Runnable{

        @Override
        public void run() {
            for (int i = 0; i < 30000; i++) {
                System.out.println("googbye");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
/*        TestSleep testSleep = new TestSleep();
        new Thread(testSleep,"黄").start();
        new Thread(testSleep,"旺").start();
        new Thread(testSleep,"西奥").start();
        try {
            testSleep.TenDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/

/*        //打印当前系统时间
        Date date = new Date(System.currentTimeMillis());
        while(true){
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
                //更新当前时间
                date = new Date(System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }*/
/*        MyYield myYield = new MyYield();
        new Thread(myYield,"a").start();
        new Thread(myYield,"b").start();
        new Thread(myYield,"c").start();
        new Thread(myYield,"d").start();*/
/*        MyJoin myJoin = new MyJoin();
        Thread thread = new Thread(myJoin);
        thread.start();
        for (int i = 0; i < 1000; i++) {
            if(i==100){
                thread.join();
            }
            System.out.println("main开始执行"+i);
        }*/
/*
        System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
        ThreadPolicy policy = new ThreadPolicy();
        Thread thread1 = new Thread(policy);
        Thread thread2 = new Thread(policy);
        Thread thread3 = new Thread(policy);
        Thread thread4 = new Thread(policy);
        Thread thread5 = new Thread(policy);
        Thread thread6 = new Thread(policy);
        *//**
         * 先设置优先级,再启动
         *//*
        thread1.start();
        thread2.setPriority(1);
        thread2.start();
        thread3.setPriority(Thread.MAX_PRIORITY);
        thread3.start();
        thread4.setPriority(-1);
        thread4.start();
        thread5.setPriority(11);
        thread5.start();
        thread6.setPriority(4);
        thread6.start();*/

        Thread thread1 = new Thread(new God());
        thread1.setDaemon(true);
        thread1.start();
        new Thread(new You()).start();
    }
}

线程同步

  • 请使用多线程模拟王英和扈三娘同时取款的过程。

实现步骤如下。
(1)定义银行账户 Account 类
(2)定义取款线程类。
(3)定义测试类,
实例化王英取款的线程实例和扈三娘取款的线程实例

package com.thread;

/**
 *  银行账户类 
 * @author chengzh
 */
public class Account {

    /**
     * 余额500元
     */
    
    private int balance = 500;
     public int getBalance() { 
         return balance; }public void withdraw(int amount) { 
         // 取款
         balance = balance - amount;
     }
}
/**
 *取款的线程类
 */

class TestAccount implements Runnable{
        /**
         * /所有的TestAccount对象创建的线程共享同一个账户对象 
         */
     private Account account = new Account(); 
     @Override public void run() { 
         for (int i = 0; i < 5; i++) {
             //取款100元
             makeWithdrawal(100); 
         } 
     }
          private void makeWithdrawal(int amt) { 
         if(account.getBalance()>=amt) { 
             System.out.println(Thread.currentThread().getName()+"准备取款"); 
             try {
                 //0.5秒后开始取款
                 Thread.sleep(500);
              }catch(InterruptedException e) {
                 e.printStackTrace(); 
             }
             account.withdraw(amt);
             System.out.println(Thread.currentThread().getName()+"完成取款"); 
         }else { 
             System.out.println("余额不足,"+Thread.currentThread().getName()+"的取 款,余额为"+account.getBalance());
         } 
     } 
}
/**
 *测试类
 */

class TestWithdrawal { 
    public static void main(String[] args) { 
     //创建两个线程 
     TestAccount thread =new TestAccount(); 
     Thread one = new Thread(thread); 
     Thread two = new Thread(thread); 
     one.setName("王英"); two.setName("扈三娘"); 
     //启动线程
     one.start(); two.start(); 
    } 
}


.实现线程同步

  • 当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源在某一时刻只能 被一个线程使用的方式称为线程同步。
  • 采用同步来控制线程的执行有两种方式,即同步方法和同步代码块。这两种方式都使用 synchronized 关键字实现。

(1)同步方法
通过在方法声明中加入 synchronized 关键字来声明同步方法。
使用 synchronized 修饰的方法控制对类成员变量的访问。
每一个实例对应一把锁,方法 一旦执行,方法就独占该锁,直到该方法返回时才将锁释放,此后被阻塞的线程方能获得该 锁,重新进入可执行状态。
这种机制确保了同一时刻对应一个实例,其所有声明为 synchronized 的方法只能有一个处于可执行状态,从而有效地避免了类成员变量的访问冲突。
同步方法的语法格式如下
访问修物符 synchronized 返回类型 方法名{}
或者synchronized 问修饰符 返回类型 方法名{}
 synchronized 是同步关健字
 访问修饰符是指 public,private 等

private synchronized void makeWithdrawal(int amt) {
 //省略与示例7相同部分 
 }
  • makeWithdrawal() 方法成为同步方法后,同一个时刻只允许一个线程调用该方法,如果多个线程都在调用该方 法,那么其它线程需要等待正在调用该方法的线程调用结束后再进入该方法

  • synchronized 关键字相当于为方法添加了一把锁头,只有拿到钥匙的线程才能访问 这个方法。

  • 如果是静态方法,那么钥匙就是当前类的元数据,即类名.calss,由于当前类的 元数据只有一个,因此钥匙只有一把,不会造成非线程安全。

  • 如果是实例方法,那么钥匙就 是调用该方法的对象,而对象可以创建多个,导致钥匙有多把,也可能造成非线程安全。

  • 同步方法的缺陷。

  • 如果将一个运行时间比较长的方法声明成synchronized将会影响效率。

  • 例如,将线程中的 run()方法声明成 synchronized,由于在线程的整个生命周期内它一直在运 行,这样就有可能导致 run()方法会执行很长时间,那么其他的线程就得一直等到 run()方法 结束了才能执行。

(2)同步代码块 同步代码块的语法格式如下。

`synchronized(obj){ 
//需要同步访问控制的代码 
} 
  • synchronized`块中的代码必须获得对象 obj 的锁才能执行,具体实现机制与同步方法一 样。
  • 由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
  • 示例使用同步代码块的方式解决示例 的访问冲突问题。
private synchronized void makeWithdrawal(int amt) { 
//同步代码块
 synchronized (account) {
  if (account.getBalance() >= amt) { 
     System.out.println(Thread.currentThread().getName() + "准备取款"); try {Thread.sleep(500);
  // 0.5秒后开始取款 
  } catch (InterruptedException e) { 
     e.printStackTrace(); 
  }
     account.withdraw(amt); 
     System.out.println(Thread.currentThread().getName() + "完成取款"); 
  } else { 
     System.out.println("余额不足," + Thread.currentThread().getName() + " 的取款,余额为" + account.getBalance()); 
  }
 }
}

(3)死锁

  • 多线程在使用同步机制时,存在“死锁”的潜在危险。
  • 如果多个线程都处于等待状态而 无法唤醒时,就构成了死锁(DeadLock),此时处于等待状态的多个线程占用系统资源,但 无法运行,因此不会释放自身的资源。
  • 在编程时应注意死锁的问题,避免死锁的有效方法是:线程因某个条件未满足而受阻, 不能让其继续占有资源;
  • 如果有多个对象需要互斥访问,应确定线程获得锁的顺序,并保证 整个程序以相反的顺序释放锁。
  • 至此,任务 4 已经全部完成。本任务通过线程同步解决了多线程中的数据完整性问题。

任务五:生产者消费者问题

步骤
(1)分析生产者消费者问题。
(2)使用 3 种方法实现线程间通信。

.实现线程间通信

  • Java 提供了 3 个方法来实现线程之间的通信,即 wait()方法、notify()方法和 notifyall()方 法。
  • wait()方法:调用 wait()方法会挂起当前线程,并释放共享资源的锁。
  • notify()方法:调用任意对象的 notify()方法会在因调用该对象的 wait()方法而阻塞的 线程中随机选择一个线程解除阻塞,但要等到获得锁后才可真正执行。
  • notifyAll()方法:调用了 notifyAll()方法会将因调用该对象的 wait()方法而阻塞的所有 线程一次性全部解除阻塞。 wait()、notify()和 notifyAll()这 3 个方法都是 Object 类中的 final 方法,被所有的类继承且 不允许重写。这 3 个方法只能在同步方法或者同步代码块中使用,否则会抛出异常。 示例 11 使用 wait()方法和 notify()方法实现线程间通信。
  • 实现步骤如下。
  • (1)使用 wait()方法挂起线程。
  • (2)使用 notify()方法唤起线程。
public class CommunicationThread implements Runnable {
 public static void main(String[] args) { 
 CommunicationThread thread = new CommunicationThread(); 
 Thread ta = new Thread(thread, "线程ta"); 
 Thread tb = new Thread(thread, "线程tb");
  ta.start(); tb.start();
 }
 @Override synchronized public void run() { 
 for (int i = 0; i < 5; i++) { 
 System.out.println(Thread.currentThread().getName() + i); 
 if (i == 2) {
  try {wait();// 退出运行状态,放弃资源锁,进入等待队列
   } catch (InterruptedException e) { 
   e.printStackTrace(); 
   }
  }
  if (i == 1) {
   notify();
   // 从等待序列中唤醒一个线程 
   }
   if (i == 4) { 
   notify();
   // 从等待序列中唤醒一个线程
    } 
   } 
  } 
}

使用线程间通信解决生产者消费者问题。
实现步骤如下。
(1) 定义共享资源类。
(2) 定义生产者线程类。
(3) 定义消费者线程类。
(4) 定义测试类

//共享资源类 
public class SharedData { 
private char c;
 private boolean isProduced = false; //标志位,true表示商品未消费,false表示已消费 
 // 同步方法 
 public synchronized void putShareChar(char c) { 
 // 如果产品还未消费,则生产者等待 
 if (isProduced) { 
 try {
   System.out.println("消费者还未消费,因此生产者停止生产");
    wait(); //生产者等待
     } 
     catch (Exception e) {
      e.printStackTrace(); 
      }
     }
      this.c = c; isProduced = true;// 标记已生产 
      notify();// 通知消费者已经生产,可以消费 
      System.out.println("生产者生产了产品" + c + "通知消费者消费......"); 
      }
      // 同步方法 
      public synchronized char getShareChar() { 
      // 如果产品还未生产,则消费者等待 
      if (!isProduced) { 
      try {
      System.out.println("生产者还未生产,因此消费者停止消费"); 
      wait();
       } 
       catch (Exception e) { 
       e.printStackTrace(); 
       } 
       }
       isProduced = false; // 标记已消费 notify(); // 通知生产者已经消费,可以生产 
       System.out.println("消费者消费了产品" + c + ",通知生产者生产");
        return this.c; 
     }
 }
//生产者线程类
 public class Producer extends Thread { 
 private SharedData s;
  public Producer(SharedData s) {
   this.s = s; 
   }
   @Override public void run() { 
   for (char ch = 'A'; ch <= 'D'; ch++) { 
   try {
   Thread.sleep((int) (Math.random()*3000));
    } 
    catch (Exception e) {
     e.printStackTrace(); 
     }
     s.putShareChar(ch);// 将产品放入仓库 
     } 
   }
 }
//消费者线程类 
public class Consumer extends Thread { 
	private SharedData s; 
	public Consumer(SharedData s) { 
		this.s = s; 
	}
	@Override public void run() { 
		char ch; 
		do {
			try {
			Thread.sleep((int) (Math.random() * 3000)); 
			} 
			catch (Exception e) { 
			e.printStackTrace(); 
			}
		ch = s.getShareChar();//从仓库中取出商品 
		} 
		while (ch != 'D'); 
	}
 }
//测试类
 public class CommunicationDemo {
  public static void main(String[] args) { 
  //共享同一个资源
	SharedData s =new SharedData();
	//消费者线程 
	new Consumer(s).start(); //生产者线程
	 new Producer(s).start(); 
	 } 
}

小结:
(1) java.lang 包下提供了 3 种标准的注解类型,称为内建注解,
分别是@Override 注解、 @Deprecated 注解以及@SuppressWarnings 注解。
(2) java.lang.annotation 包提供了 4 种元注解,用来修饰其他的注解定义。
分别是@Target 注解、@Retention 注解、@Documented 注解以及@Inherited 注解。
(3) 线程是进程中执行运算的最小单位。一个进程在其执行过程中可以产生多个线程, 而线程必须在某个进程内执行。
(4) 定 义 一 个 线 程 类 通 常 有 两 种 方 法 ,
分 别 是 继 承 java.lang.Thread 类 和 实 现 java.lang.Runnable 接口。
(5) 线程有新生、运行、阻塞、死亡 4 种状态。
(6) 线程的优先级用 1~10 表示,10 表示优先级最高,默认值是 5。
每个优先级对应一 个 Thread 类的公用静态常量。
(7) 使用 join()方法、sleep()方法、yield()方法可以改变线程的状态。
(8) 线程同步有两种方式,
即同步方法和同步代码块。这两种方式都使用 synchronized 关键字来实现
(9) Java 提供了 3 个方法来实现线程之间的通信,
即 wait()方法、notify()方法和 notifyAll() 方法。

线程池

1 线程池的优势

总体来说,线程池有如下的优势:

(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2 线程池的使用

线程池的真正实现类是ThreadPoolExecutor,其构造方法有如下4种:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

可以看到,其需要如下几个参数:

  • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
  • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
  • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
  • unit(必需):指定keepAliveTime参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  • workQueue(必需):任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该参数中。其采用阻塞队列实现。
  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
  • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

线程池的使用流程如下:

// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 线程执行的任务
    }
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

3 线程池的工作原理

下面来描述一下线程池工作的原理,同时对上面的参数有一个更深的了解。其工作原理流程图如下:
在这里插入图片描述
通过上图,相信大家已经对所有参数有个了解了。下面再对任务队列、线程工厂和拒绝策略做更多的说明。

4 线程池的参数

4.1 任务队列(workQueue)
任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在Java中需要实现BlockingQueue接口。但Java已经为我们提供了7种阻塞队列的实现:

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
  • LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为Integer.MAX_VALUE。
  • PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现Comparable接口也可以提供Comparator来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
  • DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现Delayed接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
  • SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用take()方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用put()方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
  • LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样FIFO(先进先出),也可以像栈一样FILO(先进后出)。
  • LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue和SynchronousQueue的结合体,但是把它用在ThreadPoolExecutor中,和LinkedBlockingQueue行为一致,但是是无界的阻塞队列。
    注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置maximumPoolSize没有任何意义。

4.2 线程工厂(threadFactory)
线程工厂指定创建线程的方式,需要实现ThreadFactory接口,并实现newThread(Runnable r)方法。该参数可以不用指定,Executors框架已经为我们实现了一个默认的线程工厂:

/**
 * The default thread factory.
 */
private static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
 
    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }
 
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

4.3 拒绝策略(handler)

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现

  • RejectedExecutionHandler接口,并实现rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法。不过Executors框架已经为我们实现了4种拒绝策略:
  • AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
  • CallerRunsPolicy:由调用线程处理该任务。
  • DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
  • DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

5 功能线程池

嫌上面使用线程池的方法太麻烦?其实Executors已经为我们封装好了4种常见的功能线程池,如下:

  • 定长线程池(FixedThreadPool)
  • 定时线程池(ScheduledThreadPool )
  • 可缓存线程池(CachedThreadPool)
  • 单线程化线程池(SingleThreadExecutor)
    5.1 定长线程池(FixedThreadPool)
    创建方法的源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}
  • 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
  • 应用场景:控制线程最大并发数。
    实例:
// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);

5.2 定时线程池(ScheduledThreadPool )

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
 
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}
 
public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
}
  • 特点:核心线程数量固定,非核心线程数量无限,执行完闲置10ms后回收,任务队列为延时阻塞队列。
  • 应用场景:执行定时或周期性的任务。
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务

5.3 可缓存线程池(CachedThreadPool)

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}
  • 特点:无核心线程,非核心线程数量无限,执行完闲置60s后回收,任务队列为不存储元素的阻塞队列。
  • 应用场景:执行大量、耗时少的任务。
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);

5.4 单线程化线程池(SingleThreadExecutor)

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}
  • 特点:只有1个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
  • 应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作、文件操作等。
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);

5.5 对比
在这里插入图片描述
6 总结

  • Executors的4个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
  • 其实Executors的4个功能线程有如下弊端:
  • FixedThreadPool和SingleThreadExecutor:主要问题是堆积的请求处理队列均采用LinkedBlockingQueue,可能会耗费非常大的内存,甚至OOM。
  • CachedThreadPool和ScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

Android多线程:线程池ThreadPool 全面解析

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值