多线程与并发编程回顾总结

132 阅读31分钟

一、多线程的概述

线程是比进程更小的能独立运行的基本单位,它是进程的一部分,一个进程可以拥有多个线程,但至少要有一个线程,即主执行线程(Java 的main 方法)。

多线程可以共享内存、充分利用CPU,通过提高资源(内存和CPU)使用率从而提高程序的执行效率。

CPU 使用抢占式调度模式在多个线程间进行着随机的高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而CPU 在多个线程间的切换速度相对我们的感觉要快很多,看上去就像是多个线程或任务在同时运行。

二、线程的创建

Java多线程提供两种编程方式,继承Thread 类与实现Runnable接口

1.继承Thread 类

自定义一个类去继承Thread 类,并重写run 方法,在该方法内实现具体业务功能

public class Thread1 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(this.getName() + ":" + i);
            }
        }
    }
public class Thread2 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(this.getName() + ":" + i);
            }
        }
    }

创建两个线程对象并分别启动,能够清晰观察到,CPU在两个线程之间快速随机切换,也就是平时说的在同时运行。

 public static void main(String[] args) {
 
        Thread1 thread1=new Thread1();
        thread1.setName("线程A");
        Thread2 thread2=new Thread2();
        thread2.setName("线程B");
        
        thread1.start();
        thread2.start();
    }
线程A:0
线程B:0
线程B:1
线程B:2
线程B:3
线程B:4
线程B:5
线程A:1
线程B:6
线程B:7
线程B:8
线程B:9
线程A:2
线程A:3
线程A:4
线程A:5

2.实现Runnable接口

自定义一个类去继承Runnable 接口,并重写run 方法,在该方法内实现具体业务功能。

多线程环境中常使用实现Runnable接口方式,便于扩展,毕竟Java是单继承。

public class Runnable1 implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
public class Runnable2 implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }

创建两个线程对象并分别启动,能够清晰观察到,CPU在两个线程之间快速随机切换,也就是平时说的在同时运行。

    public static void main(String[] args) {
    
        Thread thread1=new Thread(new Runnable1());
        thread1.setName("线程C");
        
        Thread thread2=new Thread(new Runnable2());
        thread2.setName("线程D");
        
        thread1.start();
        thread2.start();
    }
线程D:0
线程C:0
线程D:1
线程C:1
线程C:2
线程C:3
线程C:4
线程C:5
线程D:2
线程D:3
线程D:4
线程D:5
线程C:6
线程C:7
线程D:6
线程C:8

3.匿名内部类创建线程

	public static void main(String[] args) {
	
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println("run() i:" + i);
				}
			}
		}).start();

		for (int i = 0; i < 10; i++) {
			System.out.println("main() i:" + i);
		}
	}

4.常用方法与构造函数

方法名描述
start()启动线程
currentThread()获取当前线程对象
getID()获取当前线程ID,默认线程名:Thread-编号 ,该编号从0开始
getName()获取当前线程名称
sleep(long mill)线程指定时间休眠
Stop()停止线程
构造函数描述
Thread()分配一个新的Thread 对象
Thread(String name)分配一个新的 Thread对象,并且指定线程名称
Thread(Runable r)分配一个新的Thread对象
Thread(Runable r, String name)分配一个新的 Thread对象,同时指定线程名称

5.继承Tread类与实现Runnable接口区别

1.继承Tread类,实例化一个线程时,调用start()只能启动一个线程。实例化多个线程实例,每个实例调用start()可以启动多个线程,但线程中的资源也是多份的。

2:实现Runnable接口只需要实例化一个线程类就可以创建多个线程,并且多个线程共享同一份资源。

3:继承Tread类后不能同时继承其它类,而实现了Runnable接口后还可实现其它接口和继承其它类。

4:Thread本身也是Runnable接口的一个实现类。

三、线程的运行状态

线程运行有五个状态:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。

线程从创建、运行到结束总是处于五个状态之一,多数情况在就绪状态、运行状态、阻塞状态之间来回切换。

1.新建状态

当创建一个线程时, 如new Thread(createRunnable ),线程还没有开始运行,此时线程处在新建状态。 

2.就绪状态

线程调用start()方法后,即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。

处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。

当有多个线程处于就绪状态时,对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。

3.运行状态

当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.

4.阻塞状态

线程运行过程中,可能由于各种原因进入阻塞状态

线程通过调用sleep方法进入睡眠状态;

线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;

线程试图得到一个锁,而该锁正被其他线程持有;

线程在等待某个触发条件;

5.死亡状态

有两个原因会导致线程死亡

1.run方法正常退出而自然死亡

2.一个未捕获的异常终止了run方法而使线程猝死。

判断当前线程是否存活

使用isAlive方法可判断当前线程是否存活(是否可运行,是否被阻塞),是返回true。

若线程处于new状态且不是可运行的, 或者线程死亡,即返回false

四、线程其他相关

线程的停止

1.使用退出标志(flag),使线程正常退出,也就是当run方法完成后线程终止。

2.使用stop方法强行终止线程(不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。

3.使用interrupt方法中断线程。
public class StopThreadDemo {

    private static boolean flag = true;

    static class StopThread implements Runnable {
        @Override
        public synchronized void run() {
            for (int i = 0; i < 20; i++) {
                if (!flag) {
                    break;
                }
                System.out.println("thread run..");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                    flag=false;
                }
            }
        }
    }


    public static void main(String[] args) throws Exception {
        StopThread stopThread = new StopThread();
        Thread thread = new Thread(stopThread);
        thread.start();
        Thread.sleep(2000);
        //flag=false;
        //thread.stop();
        thread.interrupt();
    }
}

守护线程

Java中有两种线程,一种是用户线程,另一种是守护线程。

当进程不存在或主线程停止,守护线程也会被停止。

使用setDaemon(true)方法设置为守护线程

守护线程就是进程线程(主线程停止了) 守护线程也会被自动销毁。

public class DaemonThread {

	public static void main(String[] args) throws Exception{
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				while (true) {
					try {
						Thread.sleep(500);
					} catch (Exception e) {
						e.printStackTrace();
					}
					System.out.println("子线程...");
				}
			}
		});
		thread.setDaemon(true);
		thread.start();

		Thread.sleep(2000);
		System.out.println("主线程执行完毕!");
	}
}

线程的优先级

现代操作系统基本采用时分的形式调度运行的线程,线程分配得到的时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。

	public static void main(String[] args) throws InterruptedException {
		Thread thread1 = new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println(Thread.currentThread().getName());
				}
			}
		},"线程1");

		Thread thread2 = new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println(Thread.currentThread().getName());
				}
			}
		},"线程2");

		thread1.start();
		//设置了优先级,不代表每次都一定会被执行,只是CPU调度会有限分配
		thread1.setPriority(10);

		thread2.start();
		//设置了优先级,不代表每次都一定会被执行,只是CPU调度会有限分配
		thread2.setPriority(1);
	}

join()方法与Yield()方法

join作用是让其他线程变为等待状态,等到当前线程执行完毕后,其他线程才能执行。
	public static void main(String[] args) throws InterruptedException {
		Thread thread1 = new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println(Thread.currentThread().getName());
				}
			}
		},"线程1");

		Thread thread2 = new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println(Thread.currentThread().getName());
				}
			}
		},"线程2");

		thread1.start();
		//子线程thread1执行完毕后,thread2与主线程才能执行
		thread1.join();

		thread2.start();
		for (int i = 0; i < 20; i++) {
			System.out.println("main....");
		}
	}

Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)

yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

五、多线程的应用

使用多线程处理数据,提高执行效率。

class MsgThread extends Thread {

    private List<HashMap<String, Integer>> list;

    /**
     * 通过构造函数传入每个线程需要处理的数据
     *
     * @param list
     */
    public MsgThread(List<HashMap<String, Integer>> list) {
        this.list = list;
    }

    /**
     * 批量处理数据
     */
    @Override
    public void run() {
        for (HashMap<String, Integer> map : list) {
            try {
                sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("threadName:" + Thread.currentThread().getName() + "----------数据ID:" + map.get("id"));
        }
    }
}
 /**
     * 初始化构造数据
     *
     * @return
     */
    public static List<HashMap<String, Integer>> init() {
        List<HashMap<String, Integer>> mapList = new ArrayList<HashMap<String, Integer>>();
        HashMap<String, Integer> map;
        for (int i = 1; i <= 10; i++) {
            map = new HashMap<String, Integer>();
            map.put("id", i);
            mapList.add(map);
        }
        return mapList;
    }

    /**
     * @param list     切割集合
     * @param pageSize 分页长度
     * @param <T>      泛型类型
     * @return List<List < T>> 返回分页数据
     */
    public static <T> List<List<T>> splitList(List<T> list, int pageSize) {
        int listSize = list.size();
        int page = (listSize + (pageSize - 1)) / pageSize;
        List<List<T>> listArray = new ArrayList<List<T>>();
        for (int i = 0; i < page; i++) {
            List<T> subList = new ArrayList<T>();
            for (int j = 0; j < listSize; j++) {
                int pageIndex = ((j + 1) + (pageSize - 1)) / pageSize;
                if (pageIndex == (i + 1)) {
                    subList.add(list.get(j));
                }
                if ((j + 1) == ((j + 1) * pageSize)) {
                    break;
                }
            }
            listArray.add(subList);
        }
        return listArray;
    }

    public static void main(String[] args) {
        //初始化数据
        List<HashMap<String, Integer>> mapList = init();
        //指定每个线程处理的数据总数
        int dealCount = 2;
        //总共10数据,假设每个线程处理2条数据,使用list集合切割分成5个List,即5个线程
        List<List<HashMap<String, Integer>>> splitList = splitList(mapList, dealCount);
        //splitList.size():线程数量
        for (int i = 0; i < splitList.size(); i++) {
            List<HashMap<String, Integer>> list = splitList.get(i);
            MsgThread MsgThread = new MsgThread(list);
            //执行数据处理
            MsgThread.start();
        }

    }

六、线程安全

多个线程操同一个共享资源(全局变量或静态变量),但是线程之间是彼此独立、互相隔绝的,做写的操作时可能会发生数据冲突问题,因此就会出现数据(共享资源)不能同步更新的情况,这就是线程安全问题。但是做读操作是不会发生数据冲突问题。

线程安全演示

public class SellTickets implements Runnable{

    //表示10张票
    //多个线程同时共享的资源
    private static int maxNumber=10;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (maxNumber > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出一张票,票编号为:" + maxNumber);
                maxNumber--;

                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

创建两个线程对对象,同时进行卖票操作,出现重复售票的情况,该情况就是线程安全问题造成的。

    public static void main(String[] args) {
    
     SellTickets sellTickets = new SellTickets();
     
        Thread thread1 = new Thread(sellTickets);
        thread1.setName("线程A");
       
        Thread thread2 = new Thread(sellTickets);
        thread2.setName("线程B");
        
        thread1.start();
        thread2.start();
    }
线程B卖出一张票,票编号为:10
线程B卖出一张票,票编号为:9
线程B卖出一张票,票编号为:8
线程B卖出一张票,票编号为:7
线程B卖出一张票,票编号为:6
线程A卖出一张票,票编号为:10
线程A卖出一张票,票编号为:4
线程A卖出一张票,票编号为:3
线程B卖出一张票,票编号为:5
线程B卖出一张票,票编号为:1
线程A卖出一张票,票编号为:2

解决线程安全问题

同步机制(锁)

Java提供了一个同步机制(锁)来解决线程安全问题,即让操作共享数据的代码在某一时间段,只被一个线程执行(锁住),在执行过程中,其他线程不可以参与进来,这样共享数据就能同步了。简单来说,就是给某些代码加把锁。

锁是什么?

锁的专业名称叫监视器monitor,其实Java 为每个对象都自动内置了一个锁(监视器monitor),当某个线程执行到某代码块时就会自动得到这个对象的锁,那么其他线程就无法执行该代码块了,一直要等到之前那个线程停止(释放锁)。需要特别注意的是:多个线程必须使用同一把锁(对象)。

使用同步机制(锁)解决线程安全

使用多线程之间同步(当多个线程共享同一个资源,不会受到其他线程的干扰)或使用锁(lock)可解决线程安全问题。

解决线程安全的原理

原理是将可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,然后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。

Java的同步机制提供两种实现方式

同步代码块:即给代码块上锁,变成同步代码块

同步方法:即给方法上锁,变成同步方法

这两种方式本质上差不多,都是通过synchronized 关键字来实现的。

同步代码块

同步代码块的语法:

synchronized(锁){...业务代码...}。

将可能会发生线程安全问题的代码,给包括起来

public class SellTickets implements Runnable {

	 //定义多线程同步锁
    private Object object=new Object();

    //表示10张票
    private static int maxNumber = 10;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
        //使用自定义锁
		//synchronized (object)
		
            synchronized (this) {
                if (maxNumber > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖出一张票,票编号为:" + maxNumber);
                    maxNumber--;
                }
            }
        }
    }
}
    public static void main(String[] args) {

        SellTickets sellTickets = new SellTickets();

        Thread thread1 = new Thread(sellTickets);
        thread1.setName("线程A");


        Thread thread2 = new Thread(sellTickets);
        thread2.setName("线程B");

        thread1.start();
        thread2.start();
    }
线程A卖出一张票,票编号为:10
线程A卖出一张票,票编号为:9
线程B卖出一张票,票编号为:8
线程A卖出一张票,票编号为:7
线程A卖出一张票,票编号为:6
线程A卖出一张票,票编号为:5
线程A卖出一张票,票编号为:4
线程A卖出一张票,票编号为:3
线程B卖出一张票,票编号为:2
线程B卖出一张票,票编号为:1

同步方法

在方法上使用synchronized 修饰的方法称为同步方法/同步函数, 同步函数使用this锁

public class SellTickets implements Runnable {

    //表示10张票
    private static int maxNumber = 10;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            this.sellTickets();
        }
    }

    private synchronized void sellTickets() {
        if (maxNumber > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票编号为:" + maxNumber);
            maxNumber--;
        }
    }
}
    public static void main(String[] args) {

        SellTickets sellTickets = new SellTickets();

        Thread thread1 = new Thread(sellTickets);
        thread1.setName("线程A");


        Thread thread2 = new Thread(sellTickets);
        thread2.setName("线程B");

        thread1.start();
        thread2.start();
    }
线程A卖出一张票,票编号为:10
线程A卖出一张票,票编号为:9
线程A卖出一张票,票编号为:8
线程B卖出一张票,票编号为:7
线程A卖出一张票,票编号为:6
线程B卖出一张票,票编号为:5
线程B卖出一张票,票编号为:4
线程A卖出一张票,票编号为:3
线程A卖出一张票,票编号为:2
线程A卖出一张票,票编号为:1

同步函数使用this锁的证明

1.让一个线程使用同步代码块(使用自定义多线程同步锁),另一个线程使用同步函数(使用this锁)。

2.让一个线程使用同步代码块(使用this锁),另一个线程使用同步函数(使用this锁)。	

静态同步函数

方法上加上static关键字,使用synchronized关键字修饰。
	
静态的同步函数使用的锁是该函数所属字节码文件对象 	
        private static  synchronized void sellTickets() {
            if (maxNumber > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出一张票,票编号为:" + maxNumber);
                maxNumber--;
            }
        }

七、多线程死锁

多线程死锁:同步中嵌套同步,导致锁无法释放。

死锁解决办法:不要在同步中嵌套同步。

示例1:

	/**
	 * 最先flag:true=> 第一个线程获取得到object锁,再拿到this锁,执行成功
	 * 等会儿flag:false=> 第二个线程获取得到this锁,再拿到object锁,执行成功
	 * 在执行过程中突然出现线程2拿不到object锁,而线程1拿不到this锁
	 */
class ThreadTrain implements Runnable {
	//多个线程同时共享的资源
	private  int trainCount = 100;
	public boolean flag = true;
	//自定义多线程锁
	private Object object=new Object();
	@Override
	public void run() {
		if (flag) {
			while (true) {
				synchronized (object) {
					sale();
				}
			}
		} else {
			while (true) {
				sale();
			}
		}
	}

	/**
	 * 窗口卖票
	 */
	public  synchronized void sale() {
			synchronized (object){
				if (trainCount > 0) {
					try {
						Thread.sleep(40);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + ":出售第" + (100 - trainCount + 1) + "张票");
					trainCount--;
				}
			}
	}
}
	public static void main(String[] args) throws InterruptedException {
		ThreadTrain threadTrain = new ThreadTrain();
		Thread thread1 = new Thread(threadTrain, "窗口1");
		Thread thread2 = new Thread(threadTrain, "窗口2");
		thread1.start();
		Thread.sleep(40);
		threadTrain.flag = false;
		thread2.start();
	}

示例2:

	static Object a = new Object();
	static Object b = new Object();

	public static void main(String[] args) throws InterruptedException {
		new Thread(() -> {
			synchronized (a) {
				System.out.println("获得使用a锁,准备获取b锁。。。");
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (b) {
					System.out.println("获得了 a 和 b");
				}
			}
		}).start();
		
		Thread.sleep(1000);
		
		new Thread(() -> {
			synchronized (b) {
				System.out.println("获得使用b锁,准备获取a锁。。。");
				synchronized (a) {
					System.out.println("获得了 a 和 b");
				}
			}
		}).start();
	}
获得使用a锁,准备获取b锁。。。
获得使用b锁,准备获取a锁。。。

八、多线程间通信

多个线程并发执行时, 在默认情况下CPU 是随机性的在线程之间进行切换的,但是有时候希望它们能有规律的执行, 那么,多线程之间就需要一些协调通信来改变或控制CPU的随机性。

Java 提供了等待唤醒机制来解决这个问题,具体来说就是多个线程依靠一个同步锁,然后借助于wait()和notify()方法就可以实现线程间的协调通信。

同步锁相当于中间人的作用,多个线程必须用同一个同步锁(认识同一个中间人),只有同一个锁上的被等待的线程,才可以被持有该锁的另一个线程唤醒,使用不同锁的线程之间不能相互唤醒,也就无法协调通信。

实现线程间的协调通信的方法

public final void wait(); 让当前线程释放锁,然后处于等待状态。

public final native void wait(long timeout); 让当前线程释放锁,并等待xx毫秒

public final native void notify(); 唤醒持有同一锁的某个线程

public final native void notifyAll(); 唤醒持有同一锁的所有线程

在调用wait 和notify 方法时,当前线程必须已经持有锁,然后才可以调用,否则将会抛出IllegalMonitorStateException 异常。

创建一个锁对象

为了保证两个线程一定是使用的同一个锁,可以创建一个对象作为静态属性放到一个类中,这个对象就用来充当锁。

public class CustomLock {
        private static CustomLock lock=new CustomLock();

    public static CustomLock getLock() {
        return lock;
    }
}

创建线程

public class ThreadTest {
    //读写次数
    private static int num = 1;

    //flag=false:则写入 flag=true:则读取
    private static boolean flag;

    /**
     * 线程1进行写入
     */
    static class Thread1 extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (CustomLock.getLock()) {
                    //flag=true则线程1等待,实则线程2执行,类似于sleep,让当前线程从运行状态变为休眠状态
                    //在多线程之间同步需要和synchronized一起使用
                    //wait可以释放锁 ,sleep不能释放锁
                    if (flag) {
                        try {
                            CustomLock.getLock().wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //flag=false,进行写入数据
                    for (int i = num; i < 11; i++) {
                        System.out.println("第" + num + "次写入");
                        //写入完毕,通知另一个线程,唤醒从阻塞状态变为运行状态
                        //notify和wait一起使用
                        flag = true;
                        CustomLock.getLock().notify();
                        break;
                    }
                }
            }
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (CustomLock.getLock()) {
                    //flag=false,则线程2等待,实则线程1执行
                    if (!flag) {
                        try {
                            CustomLock.getLock().wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //flag=true,则线程2读入线程1写入数据
                    System.out.println("第" + num + "次读取");
                    //读写次数自增
                    num++;
                    //读取完毕,则唤醒另一个线程
                    flag = false;
                    CustomLock.getLock().notify();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();
        thread1.start();
        thread2.start();
    }
}

执行测试

1次写入
第1次读取
第2次写入
第2次读取
第3次写入
第3次读取
第4次写入
第4次读取
第5次写入
第5次读取
第6次写入
第6次读取
第7次写入
第7次读取
第8次写入
第8次读取
第9次写入
第9次读取
第10次写入
第10次读取

sleep与wait区别

sleep():属于Thread类中的。wait():属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

Lock的使用

使用Lock接口以及相关实现类用来实现锁功能,Lock 接口提供了与 synchronized 关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。

public class ThreadTest {
    //读写次数
    private static int num = 1;

    //flag=false:则写入 flag=true:则读取
    private static boolean flag;

    //定义重入锁
    private static Lock lock = new ReentrantLock();
    
    //定义condition,使线程等待or唤醒
    private static Condition condition = lock.newCondition();

    /**
     * 线程1进行写入
     */
    static class Thread1 extends Thread {
        @Override
        public void run() {
            while (true) {
                //手动获取锁
                lock.lock();
                try {
                    //flag=true则线程1等待,实则线程2执行
                    if (flag) {
                        condition.await();
                    }

                    //flag=false,进行写入数据
                    for (int i = num; i < 11; i++) {
                        System.out.println("第" + num + "次写入");
                        //写入完毕,通知另一个线程,唤醒从阻塞状态变为运行状态
                        flag = true;
                        condition.signal();
                        break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
               		// 一定在finally中释放锁
                    //手动释放锁
                    lock.unlock();
                }

            }
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            while (true) {
                //手动加锁
                lock.lock();
                try {
                    //flag=false,则线程2等待,实则线程1执行
                    if (!flag) {
                        condition.await();
                    }
                    //flag=true,则线程2读入线程1写入数据
                    System.out.println("第" + num + "次读取");
                    //读写次数自增
                    num++;
                    //读取完毕,则唤醒另一个线程
                    flag = false;
                    condition.signal();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();
        thread1.start();
        thread2.start();
    }
}

Lock接口与synchronized关键字的区别

Lock接口可以尝试非阻塞地获取锁:当前线程尝试获取锁。如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。

Lock接口能被中断地获取锁: 与 ynchronized不同,获取到锁的线程能够响应中断,当获取到的锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。

Lock接口在指定的截止时间之前获取锁,如果截止时间到了依旧无法获取锁,则返回。

Condition用法

Condition的功能类似于在传统的线程技术中的,Object.wait()和Object.notify()的功能.
Condition condition = lock.newCondition();
condition.await();  类似wait
Condition. Signal() 类似notify

九、线程的三大特性

三大特性

多线程有三大特性: 原子性可见性有序性

原子性

原子性即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性其实就是保证数据一致、是线程安全一部分。

可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

若有2个线程1个变量i,线程1改变了i的值还没刷新到主内存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

有序性
程序执行的顺序按照代码的先后顺序执行。

一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

Java内存模型

共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

Volatile

可见性

Volatile关键字的作用是变量在多个线程之间可见。

public class ThreadVolatile {
    public static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
    
        Thread threadVolatile = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("开始执行子线程....");
                while (flag) {
                }
                System.out.println("子线程停止...");
            }
        });

        threadVolatile.start();
        Thread.sleep(1000);
        flag = false;
        Thread.sleep(500);
        System.out.println(flag);
    }
}
不使用volatile修饰时,将结果设置为fasle后,程序还一直在运行。

原因:线程之间是不可见的,读取的是副本,没有及时读取到主内存结果。
开始执行子线程....
false
使用Volatile关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值
开始执行子线程....
子线程停止...
false

非原子性

创建5个线程,同时对count操作。

public class VolatileNoAtomic {
    static volatile int count = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        //count++;
                        count = atomicInteger.incrementAndGet();
                    }
                    System.out.println(Thread.currentThread().getName() + "------" + count);
                }
            }).start();
        }
    }
}

使用count++,由于Volatile不用具备原子性,count最终结果错误。

Thread-0------15750
Thread-2------19179
Thread-1------21169
Thread-3------22976
Thread-4------23020

JDK1.5提供了java.util.concurrent.atomic包(简称Atomic包),包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式

原子性

AtomicInteger: 原子更新整型

public AtomicInteger():                 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue):  初始化一个指定值的原子型Integer

int get():                              获取值
int getAndIncrement():                   以原子方式将当前值加1,返回自增前的值。
int incrementAndGet():                   以原子方式将当前值加1,返回自增后的值。
int addAndGet(int data):                 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):                以原子方式设置为newValue的值,并返回旧值

使用Atomic包下的AtomicInteger原子类,具有原子性,count最终结果正确。

Thread-3------43511
Thread-0------46779
Thread-1------48217
Thread-4------48530
Thread-2------50000

volatile与synchronized

1.volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法

2.volatile只能保证数据的可见性,不能用来同步,当多个线程并发访问volatile修饰的变量不会阻塞。synchronized不仅保证可见性,还保证原子性,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。

3.线程安全性包括两个方面,可见性与原子性。仅仅使用volatile并不能保证线程安全性,而synchronized则可实现线程的安全性。

ThreadLoca

ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLoca接口

void set(Object value)设置当前线程的线程局部变量的值。

public Object get()该方法返回当前线程所对应的线程局部变量。

public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用。当线程结束后,对应该线程的局部变量将自动被垃圾回收。

protected Object initialValue()返回该线程局部变量的初始值,是protected方法,显然是让子类覆盖而设计的。是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。
public class ThreadLocalTest {
    static Integer count = 0;

    /**
     * 设置本地局部变量, 和其他线程局部变量隔离开。互不影响。
     */
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            // 设置当前线程局部变量初始化值
            return 0;
        }
        ;
    };

    static Integer getCount() {
        count = threadLocal.get() + 1;
        threadLocal.set(count);
        //count++;
        return count;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 3; j++) {
                        //System.out.println(Thread.currentThread().getName()+"----------"+getCount());
                        System.out.println(Thread.currentThread().getName() + "----------" + getCount());
                    }
                }
            }).start();
        }
    }
}

不使用ThreadLocal时:

Thread-1----------1
Thread-0----------2
Thread-1----------3
Thread-0----------4
Thread-1----------5
Thread-0----------6

ThreadLocal时:

Thread-1----------1
Thread-0----------1
Thread-1----------2
Thread-1----------3
Thread-0----------2
Thread-0----------3

十、线程池

线程池概述

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。

线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求。

线程池中的每个线程只有被分配一个任务,一旦任务已经完成了,线程回到池子中并等待下一次分配任务。

线程池作用

 1. 线程池改进了一个应用程序的响应时间。由于线程池中的线程已经准备好且等待被分配任务,应用程序可以直接拿来使用而不用新建一个线程。

  2. 线程池节省了CLR 为每个短生存周期任务创建一个完整的线程的开销并可以在任务完成后回收资源。

  3. 线程池根据当前在系统中运行的进程来优化线程时间片。

  4. 线程池允许我们开启多个任务而不用为每个线程设置属性。

  5. 线程池允许我们为正在执行的任务的程序参数传递一个包含状态信息的对象引用。

  6. 线程池可以用来解决处理一个特定请求最大线程数量限制问题。

Executor和ExecutorService

Executor

Executor接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法

public interface Executor {
	void execute(Runnable command);
}

ExecutorService

ExecutorService是一个更通用的线程池接口,它可以接收任务,然后根据配置来分配线程,并控制其调度,可以对线程统一管理。

public interface ExecutorService extends Executor{
		//关闭线程池,不再接收新的任务,但是会将当前线程池中的所有任务执行完毕后再关闭
		void shutdown();
		
		//立即关闭,不再接收新的任务,抛弃线程池中还未执行的任务并中断所有正在执行的任务,返回等待执行的任务列表
		
		List<Runnable> shutdownNow();
		
		//判断线程池是否关闭
		boolean isShutdown();
		
		//线程池是否终止
		boolean isTerminated();
		
		boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
		// 提交任务,任务是一个实现了Callable的类,该类中返回任务结果,该方法返回Future,可以通过Future获得任务结果
		<T> Future<T> submit(Callable<T> task);
		<T> Future<T> submit(Runnable task, T result);
		
		//提交任务,任务是实现了Runnable的类,该类中的run方法是void因此无法返回结果,该方法仍然会返回Future,但是通过Future得不到结果
		Future<?> submit(Runnable task);
		
		// 提交任务集合,返回Future集合
		<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) ;
		
		// 提交任务集合,返回其中一个任务的结果
		<T> T invokeAny(Collection<? extends Callable<T>> tasks)
}

核心方法

shutdown()/shutdownNow()

使用完ExecutorService后,应该关闭它,使线程不能持续运行。例如:当主线执行结束,如果存在激活状态的ExecutorService,程序将仍然会保持运行,ExecutorService中激活的线程会阻止JVM关闭。

为了终止ExecutorService中的线程,需要调用shutdown()方法。ExecutorService不会立即关闭,但是它也不会接受新的任务,直到它里面的所有线程都执行完毕,ExecutorService才会关闭。所有提交到ExecutorService中的任务会在调用shutdown()方法之前被执行。

如果想立即关闭ExecutorService,可以调用shutdownNow()方法。会尝试立即停止所有正在执行的任务,并且忽略所有提交的但未被处理的任务。对于正在执行的任务是不能确定的,也许它们停止了,也许它们执行直到结束
execute(Runnable)

         class A implements ExecutorService {
            @Override
            public void execute(Runnable command) {

            }
        }

execute(Runnable) 方法接受Runable对象的实例,并异步执行,但不能获得Runnable执行的结果。从Java1.5开始可以通过Callable和Future,在任务执行完毕之后得到任务执行结果。

submit(Callable)

submit(Callable)方法与submit(Runnable)方法相似,除了接收的参数有所不同。Callable实例非常类似于Runnable,不同的是Callable.call()方法可以返回一个结果,Runnable.run()方法不能返回一个结果

        //Callable的声明
        public interface Callable<V> {
            V call() throws Exception;
        }
        
        Future future = executorService.submit(new Callable<String>(){
            public String call()  {
                return "Callable Result";
            }
        });
        System.out.println("future.get() = " + future.get());

submit(Runnable)

submit(Runnable) 方法也可以接收一个Runnable接口的具体实现,并返回一个Future对象。Future对象可以用来检测Runable是否执行完成。

Future future = executorService.submit(new Runnable() {
	public void run() {
		System.out.println("Asynchronous task");
	}
});

future.get(); //如果任务正常完成则该方法返回null,但不能获得Runnable的执行结果

ThreadPoolExecutor及参数

在java.util.concurrent.ThreadPoolExecutor包下提供了ThreadPoolExecutor类,它是是ExecutorService接口的实现,可以通过该类来创建线程池,这个类中有4个重载的构造方法,最核心的构造方法是有7个形参的。


线程池核心参数

corePoolSize:线程池中核心线程的数量

maximumPoolSize: 线程池维护线程的最大数量,是核心线程数量和非核心线程数量之和, 只有当核心线程都被用完并且缓冲队列满后,才会开始申超过请核心线程数的线程,默认值为Integer.MAX_VALUE

keepAliveTime  :非核心线程空闲的生存时间, 即超出核心线程数外的线程在空闲时候的最大存活时间,默认为60秒。

unit  :keepAliveTime的生存时间单位

BlockingQueue:缓冲队列,当没有空闲的线程时,新的任务会加入到workQueue中排队等待

threadFactory:线程工厂,用于创建线程

RejectedExecutionHandler:拒绝策略,当没有线程可以被使用时的处理策略(拒绝任务),默认策略为abortPolicy

corePoolSize:核心线程数量

1.线程池刚创建时,线程数量为0,当每次执行execute添加新的任务时会在线程池创建一个新的线程,直到线程数量达到corePoolSize为止。

2:核心线程会一直存活,即使没有任务需要执行,当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理

3:设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

maximumPoolSize:最大线程数

1:当池中的线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务

2:当池中的线程数=maximumPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

keepAliveTime:线程空闲时间

1:当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize

2:如果allowCoreThreadTimeout=true,则会直到线程数量=0

BlockingQueue:阻塞队列

当线程池正在运行的线程数量已经达到corePoolSize,那么再通过execute添加新的任务则会被加
workQueue队列中,在队列中排队等待执行,而不会立即执行。

SynchronousQueue:直接提交队列

ArrayBlockingQueue:有界队列,可以指定容量

LinkedBlockingDeque:无界队列

PriorityBlockingQueue:优先任务队列,可以根据任务优先级顺序执行任务

RejectedExecutionHandler:拒绝策略

1:当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务

2:当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务

当拒绝处理任务时线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是
AbortPolicy,在ThreadPoolExecutor类有如下几个内部实现类来处理这类情况

callerRunsPolicy:用于被拒绝任务的处理程序,直接在execute方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。(由调用线程处理该任务)

abortPolicy:直接抛出java.util.concurrent.RejectedExecutionException异常。(丢弃任务并抛出异常)

discardOldestPolicy:当线程池中的数量等于最大线程数时、抛弃线程池中最后一个要执行的任务,并执行新传入的任务。(丢弃队列最前面的任务,然后重新尝试执行任务)

discardPolicy:当线程池中的数量等于最大线程数时,不做任何动作。(丢弃任务,但是不抛出异常)

使用测试

public class ThreadPoolExecutorTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

        //向线程池提交100任务
        for (int i = 0; i < 100; i++) {
            threadPool.execute(new MyTask(i));
        }

        //执行任务并且获取返回值
        Future<String> future = threadPool.submit(new MyCallable(-1));
        // future.get(5,TimeUnit.SECONDS)
        String result = future.get();
        System.out.println(result);

        //批量执行任务并返回任务的结果集合
        List<MyCallable> taskList = new ArrayList<>(100);
        for (int i = 0; i < 100; i++) {
            taskList.add(new MyCallable(i));
        }
        List<Future<String>> futureList = threadPool.invokeAll(taskList);
        for (Future<String> futures : futureList) {
            try {
                String results = futures.get(5, TimeUnit.SECONDS);
                System.out.println(results);
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyTask implements Runnable {
    private int count;

    public MyTask(int count) {
        this.count = count;
    }

    @Override
    public void run() {
        System.out.println("execute task " + count + " : " + Thread.currentThread().getName());
    }
}

class MyCallable implements Callable<String> {
    private int count;

    public MyCallable(int count) {
        this.count = count;
    }

    @Override
    public String call() {
        System.out.println("execute  callable task " + " : " + Thread.currentThread().getName());
        return "返回结果: " + count;
    }
}

线程池其他相关

线程池的状态

线程池中有5个状态

RUNNING:创建线程池之后的状态是RUNNING

SHUTDOWN:该状态下,线程池就不会接收新任务,但会处理阻塞队列剩余任务,相对温和。

STOP:该状态下会中断正在执行的任务,并抛弃阻塞队列任务,相对暴力。

TIDYING:任务全部执行完毕,活动线程为 0 即将进入终止

TERMINATED:线程池终止

线程池的关闭

shutdown()

该方法执行后,线程池状态变为 SHUTDOWN,不会接收新任务,但是会执行完已提交的任务,此方法不会阻塞调用线程的执行。

shutdownNow()

该方法执行后,线程池状态变为 STOP,不会接收新任务,会将队列中的任务返回,并用 interrupt 的方式中断正在执行的任务。

Executors线程池工厂

通过Executors(jdk1.5并发包)提供四种线程池的创建

newCachedThreadPool

源码:

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

构造一个带缓冲功能的线程池,配置corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,
keepAliveTime=60s,以及一个无容量的阻塞队列SynchronousQueue,因此任务提交之后,将会创建新的线程执行;线程空闲超过60s将会销毁。

简单的说就是创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

测试:

    public static void main(String[] args) {
        //创建可缓存线程池
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

        //执行execute,表示创建了线程,类似start
        for (int i = 0; i < 10; i++) {
            int index = i;
            newCachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "----" + index);
                }
            });
            //验证线程复用
//			try {
//				Thread.sleep(2);
//			} catch (InterruptedException e) {
//				e.printStackTrace();
//			}
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 关闭线程池
            newCachedThreadPool.shutdown();
        }
    }

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

//thread-2复用线程了
pool-1-thread-1----0
pool-1-thread-4----3
pool-1-thread-2----1
pool-1-thread-3----2
pool-1-thread-2----9
pool-1-thread-7----6
pool-1-thread-8----7
pool-1-thread-5----4
pool-1-thread-6----5
pool-1-thread-9----8

newFixedThreadPool

源码:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

创建一个固定大小的线程池,也可以指定同时运行的线程数量从而控制线程最大并发数,超出的线程会在队列中等待。

测试:

 public static void main(String[] args) {
    	//定长线程池的大小最好根据系统资源进行设置
		//int processors = Runtime.getRuntime().availableProcessors();
		//ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(processors);

        //创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
		ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 9; i++) {
            int index = i;
            newFixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    if (index % 3 == 0) {
                        System.out.println("--------同一时刻最多执行3个线程,其余线程等待----------");
                    }
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "----" + index);
                }
            });
        }
    }
--------同一时刻最多执行3个线程,其余线程等待----------
pool-1-thread-1----0
pool-1-thread-3----2
pool-1-thread-2----1
--------同一时刻最多执行3个线程,其余线程等待----------
pool-1-thread-2----4
pool-1-thread-1----3
pool-1-thread-3----5
--------同一时刻最多执行3个线程,其余线程等待----------
pool-1-thread-3----8
pool-1-thread-1----7
pool-1-thread-2----6

newScheduledThreadPool

源码:

 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

   public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }	

创建一个定长线程池,支持定时及周期性任务执行。

测试:

	public static void main(String[] args) {
		//线程池大小
		ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
		//创建一个定长线程池,支持定时及周期性任务执行。
		for (int i = 1; i < 7; i++) {
			int finalI = i;
			newScheduledThreadPool.schedule(new Runnable() {
				@Override
				public void run() {
					System.out.println(Thread.currentThread().getName() + "-------" + finalI + "秒钟之后执行......");
				}
			}, i, TimeUnit.SECONDS);
		}
	}
pool-1-thread-1-------1秒钟之后执行......
pool-1-thread-3-------2秒钟之后执行......
pool-1-thread-2-------3秒钟之后执行......
pool-1-thread-1-------4秒钟之后执行......
pool-1-thread-3-------5秒钟之后执行......
pool-1-thread-2-------6秒钟之后执行......

ScheduledExecutorService 可以帮助开发者实现间隔执行的效果

schedule方法

该方法的作用是让任务按照指定的时间延时执行

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);

public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
Runnable / Callable<V> 可以传入这两个类型的任务
long delay  时间数量
TimeUnit unit 时间单位

scheduleAtFixedRate方法

该方法的作用是按照指定的时间延时执行,并且每隔一段时间再继续执行

倘若在执行任务的时候,耗时超过了间隔时间,则任务执行结束之后直接再次执行,而不是再等待间隔时间执行。

 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period, TimeUnit unit);
Runnable command  执行的任务
long initialDelay     延时的时间数量
long period         间隔的时间数量
TimeUnit unit       时间单位
    public static void main(String[] args) {
        //获取对象
        ScheduledExecutorService s = Executors.newScheduledThreadPool(3);
        //延时2秒后开始执行任务,每间隔3秒再执行任务
        s.scheduleAtFixedRate(() -> {
            System.out.println(System.currentTimeMillis());
            //模拟耗时操作
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 2, 3, TimeUnit.SECONDS);

    }

scheduleWithFixedDelay方法

该方法的作用是按照指定的时间延时执行,并且每隔一段时间再继续执行

在执行任务的时候,无论耗时多久,任务执行结束之后都会等待间隔时间之后再继续下次任务。

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);
Runnable command  执行的任务
long initialDelay     延时的时间数量
long period         间隔的时间数量
TimeUnit unit       时间单位

newSingleThreadExecutor

源码:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

corePoolSize和maximumPoolSize都为1,即创建一个固定大小是1的线程池,workQueue是new
LinkedBlockingQueue < Runnable >()是一种无界阻塞队列,队列的大小是Integer.MAX_VALUE,可以认为是队列的大小不限制。

所以,通过该方法创建的线程池是一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行,当有多个任务同时提交时,那也要一个一个排队执行。

测试:

	public static void main(String[] args) {
		//创建一个单线程化的线程池,只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(优先级)执行
		ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
		for (int i = 0; i < 10; i++) {
			int index = i;
			newSingleThreadExecutor.execute(new Runnable() {
				@Override
				public void run() {
					System.out.println(Thread.currentThread().getName() + "---" + index);
				}
			});
		}
	}
pool-1-thread-1---0
pool-1-thread-1---1
pool-1-thread-1---2
pool-1-thread-1---3
pool-1-thread-1---4
pool-1-thread-1---5
pool-1-thread-1---6
pool-1-thread-1---7
pool-1-thread-1---8
pool-1-thread-1---9

线程池使用注意事项

阿里巴巴编程规约明确指出:

线程池不允许使用Executors去创建,而是推荐通过ThreadPoolExecutor的方式自主创建,让人更加明确线程池的运行规则,规避资源耗尽的风险。

线程池核心数的选取:

I/O密集型:

CPU使用率较低,程序中会存在大量I/O操作占据时间,导致线程空余时间出来,线程个数为CPU核数的两倍。当其中的线程在IO操作的时候,其他线程可以继续用CPU,提高了CPU的利用率

CPU密集型:

CPU使用率较高(复杂运算,逻辑处理),线程数一般为CPU核数的线程。 线程个数为CPU核数时,线程可以并行执行,不存在线程切换开销,提高了CPU的利用率的同时也减少了切换线程导致的性能损耗

ThreadPoolExecutor的使用

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPool = null;
        try {
            //创建线程池对象
            threadPool = new ThreadPoolExecutor(2, 3, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(2));

            //创建任务
            Runnable r = () -> System.out.println(Thread.currentThread().getName());

            //将任务提交给线程池
            for (int i = 0; i < 4; i++) {
                threadPool.execute(r);
            }
        } finally {
            //关闭线程池
            if (threadPool != null) {
                threadPool.shutdown();
                if (!threadPool.awaitTermination(1, TimeUnit.MINUTES)) {
                    //等待1分钟,如果线程池没有关闭则会执行进来
                    threadPool.shutdownNow();
                }
            }
        }
    }