Java 并发

目录

线程

多线程原理

来体现一下多线程程序的执行流程。
代码如下:
自定义线程类:

public class MyThread extends Thread{
    /*    
     * 利用继承中的特点     
     *   将线程名称传递  进行设置    
     */    
	public MyThread(String name){    
		super(name);        
	}    
    /*    
     * 重写 run 方法    
     *  定义线程要执行的代码    
     */    
	public void run(){           
		for (int i = 0; i < 20; i++) {
    		//getName()方法 来自父亲
			System.out.println(getName()+i);         }        
	}    
}

测试类:

public class Demo {
    public static void main(String[] args) {
        System.out.println("这里是main线程"); 
        MyThread mt = new MyThread("小强");            
     	mt.start();//开启了一个新的线程    
     	for (int i = 0; i < 20; i++) {    
			System.out.println("旺财:"+i);           
		}        
	}    
}

流程图:
线程流程图
程序启动运行 main 时候,java 虚拟机启动一个进程,主线程 main 在 main() 调用时候被创建。随着调用 mt 的对象的
start 方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。
多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
栈内存原理图
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

多线程的常用方法

方法名 描述
sleep() 强迫一个线程睡眠N毫秒
isAlive() 判断一个线程是否存活。
join() 等待线程终止。
activeCount() 程序中活跃的线程数。
enumerate() 枚举程序中的线程。
currentThread() 得到当前线程。
isDaemon() 一个线程是否为守护线程。
setDaemon() 设置一个线程为守护线程。
setName() 为线程设置一个名称。
wait() 强迫一个线程等待。
notify() 通知一个线程继续运行。
setPriority() 设置一个线程的优先级。

Thread 类

在上一天内容中我们已经可以完成最基本的线程开启,那么在我们完成操作过程中用到了 java.lang.Thread 类,
API 中该类中定义了有关线程的一些方法,具体如下:
构造方法:

  • public Thread():分配一个新的线程对象。
  • public Thread(String name):分配一个指定名字的新的线程对象。
  • public Thread(Runnable target):分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。
    常用方法:
  • public String getName():获取当前线程名称。
  • public void start():导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public void run():此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread():返回对当前正在执行的线程对象的引用。
    翻阅 API 后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现 Runnable 接口方式,方式一我
    们上一天已经完成,接下来讲解方式二实现的方式。

创建线程四种方式

继承 Thread 类

public class MyThread extends Thread {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
	}
}
Thread t1 = new MyThread();
t1.start();

实现 Runnable 接口

只需要重写 run 方法即可。
步骤如下:

  1. 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体。
  2. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
  3. 调用线程对象的 start() 方法来启动线程。
    代码如下:
public class MyRunnable implements Runnable{
    @Override    
    public void run() {    
        for (int i = 0; i < 20; i++) { 		  System.out.println(Thread.currentThread().getName()+" "+i);            
        }        
    }    
}
public class Demo {
    public static void main(String[] args) {
        //创建自定义类对象  线程任务对象
        MyRunnable mr = new MyRunnable();
        //创建线程对象
        Thread t = new Thread(mr, "小强");
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("旺财 " + i);
        }
    }
}

通过实现 Runnable 接口,使得该类有了多线程类的特征。run() 方法是多线程程序的一个执行目标。所有的多线程代码都在 run 方法里面。Thread 类实际上也是实现了 Runnable 接口的类。
在启动的多线程的时候,需要先通过 Thread 类的构造方法 Thread(Runnable target) 构造出对象,然后调用 Thread 对象的 start() 方法来运行多线程代码。
实际上所有的多线程代码都是通过运行 Thread 的 start() 方法来运行的。因此,不管是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的,熟悉 Thread 类的 API 是进行多线程编程的基础。

tips: Runnable 对象仅仅作为 Thread 对象的 target,Runnable 实现类里包含的 run() 方法仅作为线程执行体。
而实际的线程对象依然是 Thread 实例,只是该 Thread 线程负责执行其 target 的 run() 方法。

实现 Callable 接口

public class MyCallable implements Callable<Integer> {
	@Override
	public Integer call() {
		System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
		return 1;
	}
}
// 使用 FutureTask 包装
FutureTask<Integer> futureTask = new FutureTask<>(useCall); 
// 包装为 Thread
Thread thread = new Thread(futureTask); 
thread.start();

ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<Integer> future = threadPool.submit(new MyCallable());

FutureTask

FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是 Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

使用匿名内部类方式

使用匿名内部类的方式实现 Runnable 接口,重新 Runnable 接口中的 run 方法

public class CreateRunnable {
	public static void main(String[] args) {
	//创建多线程创建开始
	Thread thread = new Thread(new Runnable() {
		public void run() {
			for (int i = 0; i < 10; i++) {
				System.out.println("i:" + i);
			}
		}
	});
	thread.start();
	}
}

Thread 和 Runnable 的区别

如果一个类继承 Thread,则不适合资源共享。但是如果实现了 Runable 接口的话,则很容易的实现资源共享。
总结:
实现 Runnable 接口比继承 Thread 类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免 java 中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现 Runable 或 Callable 类线程,不能直接放入继承 Thread 的类。

扩充:在 java 中,每次程序运行至少启动2个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用 java 命令执行一个类的时候,实际上都会启动一个 JVM,每一个 JVM 其实在就是在操作系统中启动了一个进程。

Runnable 和 Callable 的区别

相同点:

  • 都是接口
  • 都可以编写多线程程序
  • 都采用 Thread.start()启动线程
    主要区别:
  • Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果
  • Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息 注:Callalbe接口支持返回执行结果,需要调用 FutureTask.get() 得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

线程的 run() 和 start() 有什么区别?

  • 每个线程都是通过某个特定 Thread 对象所对应的方法 run() 来完成其操作的,run() 方法称为线程体。通过调用 Thread 类的 start() 方法来启动一个线程。
  • start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
  • start() 方法来启动一个线程,真正实现了多线程运行。调用 start() 方法无需等待 run 方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此 Thread 类调用方法 run() 来完成其运行状态, run() 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
  • run() 方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用 run(),其实就相当于是调用了一个普通函数而已,直接待用 run()方法必须等待 run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用 start()方法而不是 run()方法。
  • 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

线程安全

线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
我们通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共 100 个(本场电影只能卖 100 张票)。
我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这 100 张票)
需要窗口,采用线程对象来模拟;需要票,Runnable 接口子类来模拟
模拟票:

public class Ticket implements Runnable {
    private int ticket = 100;
    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作
        //窗口 永远开启
        while (true) {
            if (ticket > 0) {//有票 可以卖
                //出票操作
                //使用sleep模拟一下出票时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto‐generated catch block
                    e.printStackTrace();
                }
                //获取当前线程对象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在卖:" + ticket‐‐);
            }
        }
    }
}

测试类:

public class Demo { 
    public static void main(String[] args) {   
    //创建线程任务对象        
    Ticket ticket = new Ticket();        
    //创建三个窗口对象        
    Thread t1 = new Thread(ticket, "窗口1");      
    Thread t2 = new Thread(ticket, "窗口2");       
    Thread t3 = new Thread(ticket, "窗口3");        
    //同时卖票        
    t1.start();        
    t2.start();        
    t3.start();        
    }    
}

结果中有一部分这样现象:

窗口3正在卖:6
窗口1正在卖:5
窗口2正在卖:5
窗口3正在卖:4
窗口1正在卖:3
窗口2正在卖:2
窗口3正在卖:1
窗口2正在卖:0
窗口1正在卖:-1

发现程序出现了两个问题:

  1. 相同的票数,比如 5 这张票被卖了两回。
  2. 不存在的票,比如 0 票与 -1 票,是不存在的。
    这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
根据案例简述:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码
去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU
资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
那么怎么去使用呢?有三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

同步代码块

  • 同步代码块synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
    格式:
synchronized(同步锁){
     需要同步操作的代码
}

同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

使用同步代码块解决代码:

public class Ticket implements Runnable{
private int ticket = 100;    
        Object lock = new Object();    
        /*    
        * 执行卖票操作    
        */    
    @Override    
    public void run() {    
            //每个窗口卖票的操作         
    //窗口 永远开启         
        while(true){        
            synchronized (lock) {            
                if(ticket>0){//有票 可以卖                
                //出票操作                    
                //使用sleep模拟一下出票时间                     
                    try {                    
                    Thread.sleep(50);                        
                    } catch (InterruptedException e) { 
                    // TODO Auto‐generated catch block 
                    e.printStackTrace();                        
                    }                    
                    //获取当前线程对象的名字                     
					String name = Thread.currentThread().getName(); 
                    System.out.println(name+"正在卖:"+ticket‐‐); 
                }                
            }
        }
    }
}

当使用了同步代码块后,上述的线程的安全问题,解决了。

同步方法

  • 同步方法: 使用 synchronized 修饰的方法,就叫做同步方法,保证 A 线程执行该方法的时候,其他线程只能在方法外等着。
    格式:
public synchronized void method(){
   可能会产生线程安全问题的代码 
}

同步锁是谁?
对于非 static 方法,同步锁就是 this。
对于 static 方法,我们使用当前方法所在类的字节码对象(类名.class)。

使用同步方法代码如下:

public class Ticket implements Runnable{
private int ticket = 100;    
/*    
 * 执行卖票操作    
 */    
@Override    
public void run() {    
//每个窗口卖票的操作         
//窗口 永远开启         
while(true){        
	sellTicket();            
	}        
}    
/*    
 * 锁对象 是 谁调用这个方法 就是谁     
 *   隐含 锁对象 就是  this    
 *        
 */    
public synchronized void sellTicket(){    
        if(ticket>0){//有票 可以卖  
            //出票操作
            //使用sleep模拟一下出票时间
            try {
               Thread.sleep(100);  
            } catch (InterruptedException e) {
               // TODO Auto‐generated catch block  
               e.printStackTrace();
            }
            //获取当前线程对象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket‐‐);
        }
	}    
}

Lock 锁

java.util.concurrent.locks.Lock机制提供了比 synchronized 代码块和 synchronized 方法更广泛的锁定操作,同步代码块/同步方法具有的功能 Lock 都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock():加同步锁。
  • public void unlock():释放同步锁。
    使用如下:
public class Ticket implements Runnable{
	private int ticket = 100;    
	Lock lock = new ReentrantLock();    
	/*    
	 * 执行卖票操作    
	 */    
	@Override    
	public void run() {    
		//每个窗口卖票的操作         
		//窗口 永远开启         
		while(true){        
			lock.lock();            
			if(ticket>0){//有票 可以卖            
				//出票操作                 
				//使用sleep模拟一下出票时间                 
				try {                
				Thread.sleep(50);                    
				} catch (InterruptedException e) { 
				// TODO Auto‐generated catch block 
				e.printStackTrace();                    
				}                
			//获取当前线程对象的名字                 
			String name = Thread.currentThread().getName(); 
			System.out.println(name+"正在卖:"+ticket‐‐);
			}            
			lock.unlock();            
		}        
	}    
}

线程状态

线程状态概述

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:
这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

[线程的状态.png]

  • 新建(new):新创建了一个线程对象。
  • 就绪(可运行状态)(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取 cpu 的使用权。
  • 运行(running):可运行状态(runnable)的线程获得了 cpu 时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  • 阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
    • 阻塞的情况分三种:
      • (一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waittingqueue)中,使本线程进入到等待阻塞状态;
      • (二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则 JVM 会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
      • (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  • 死亡(dead)(结束):线程 run()、main()方法执行结束,或者因异常退出了 run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

线程的调度策略

线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

  1. 线程体中调用了 yield 方法让出了对 cpu 的占用权利
    • yield()方法只是提出申请释放 CPU 资源,至于能否成功释放由 JVM 决定。
    • 调用了yield()方法后,线程依然处于RUNNABLE状态,线程不会进入堵塞状态。
    • 调用了yield()方法后,线程处于RUNNABLE状态时,线程就保留了随时被调度的权利。
    • yield 只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield 方法暂停之后,线程调度器又将其调度出来重新执行。)
  2. 线程体中调用了 sleep 方法使线程进入睡眠状态
  3. 线程由于 IO 操作受到阻塞
  4. 另外一个更高优先级线程出现
  5. 在支持时间片的系统中,该线程的时间片用完
  6. 线程执行同步代码块或同步方法时, sleep 和 yield 都不会释放锁
    • yield()方法调用后线程处于 RUNNABLE 状态,而 sleep()方法调用后线程处于 TIME_WAITING状态,所以 yield()方法调用后线程只是暂时的将调度权让给别人,但立刻可以回到竞争线程锁的状态;而 sleep()方法调用后线程处于阻塞状态。
  7. Object.wait() 方法,会释放锁资源以及 CPU 资源。Thread.sleep() 方法,不会释放锁资源,但是会释放 CPU 资源。

sleep() 和 yield() 的区别

  • (1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
  • (2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
  • (3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
  • (4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用 yield()方法来控制并发线程的执行。

停止一个正在运行的线程

  • 在 java 中有以下 3 种方法可以终止正在运行的线程:
    • 使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止。
    • 使用 stop 方法强行终止,但是不推荐这个方法,因为 stop 和 suspend 及 resume 一样都是过期作废的方法。
    • 使用 interrupt 方法中断线程
      • interrupt()不能中断在运行中的线程,它只能改变中断状态而已。

public class InterruptionInJava implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread testThread = new Thread(new InterruptionInJava(),"InterruptionInJava");
//start thread
testThread.start();
Thread.sleep(1000);
//interrupt thread
testThread.interrupt();
System.out.println("main end");
}
@Override
public void run() {
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("Yes,I am interruted,but I am still running");
}else{
System.out.println("not yet interrupted");
}
}
}
}
 结果显示,被中断后,仍旧运行,不停打印 Yes,I am interruted,but I am still running - 修改代码,在状态判断中如上,添加一个 return 就 okay 了。**但现实中,我们可能需要做的更通用,只要添加一个开关**。 java
public class InterruptionInJava implements Runnable{

private volatile static boolean on = false;

public static void main(String[] args) throws InterruptedException {  
    Thread testThread = new Thread(new InterruptionInJava(), "InterruptionInJava");
    //start thread  
    testThread.start();
    Thread.sleep(1000);
    // 添加一个开关
    InterruptionInJava.on = true;
	// 改变中断状态,如果不添加,被阻塞的线程将会无法被中断
    testThread.interrupt();
    System.out.println("main end");
}

@Override
public void run() {
    while(!on){
        try {
            Thread.sleep(10000000);
        } catch (InterruptedException e) {
            System.out.println("caught exception right now: "+e);  
        }
    }
}

}

## Java 中 interrupted 和 isInterrupted 方法的区别
- interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
	注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出 interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
- interrupted:是静态方法,查看当前中断信号是 true 还是 false 并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
- isInterrupted:是可以返回当前中断信号是 true 还是 false,与 interrupt 最大的差别
## Timed Waiting (计时等待)
Timed Waiting 在 API 中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。单独的去理解这句话,真是玄之又玄,其实我们在之前的操作中已经接触过这个状态了,在哪里呢?
在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在 run 方法中添加了 sleep 语句,这样就强制当前正在执行的线程休眠(**暂停执行**),以“减慢线程”。
其实当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等
待),那么我们通过一个案例加深对该状态的一个理解。
**实现一个计数器,计数到 100,在每个数字之间暂停 1 秒,每隔 10 个数字输出一个字符串**
代码:
```java
  public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("‐‐‐‐‐‐‐" + i);
            }
            System.out.print(i);
            try {
                Thread.sleep(1000);
               System.out.print("    线程睡眠1秒!\n");  
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        new MyThread().start();
    }
}

通过案例可以发现, sleep 方法的使用还是很简单的。我们需要记住下面几点:

  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
  2. 为了让其他线程有机会执行,可以将 Thread.sleep() 的调用放线程 run() 之内。这样才能保证该线程执行过程中会睡眠
  3. sleep 与锁无关,线程睡眠到期自动苏醒,并返回到 Runnable(可运行)状态。

小提示:sleep() 中指定的时间是线程不会运行的最短时间。因此,sleep() 方法不能保证该线程睡眠到期后就开始立刻执行。
Timed Waiting 线程状态图:
计时等待

BLOCKED (锁阻塞)

Blocked 状态在 API 中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程 A 与线程 B 代码中使用同一锁,如果线程 A 获取到锁,线程 A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked 锁阻塞状态。
这是由 Runnable 状态进入 Blocked 状态。除此 Waiting 以及 Time Waiting 状态也会在某种情况下进入阻塞状态,而这部分内容作为扩充知识点带领大家了解一下。
Blocked 线程状态图
锁阻塞

Waiting (无限等待)

Wating 状态在 API 中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
那么我们之前遇到过这种状态吗?答案是并没有,但并不妨碍我们进行一个简单深入的了解。我们通过一段代码来学习一下:

  public class WaitingTest {
    public static Object obj = new Object();
    public static void main(String[] args) {
        // 演示waiting
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    synchronized (obj){
                        try {
                            System.out.println( Thread.currentThread().getName() +"=== 获取到锁对象,调用wait方法,进入waiting状态,释放锁对象");
                            obj.wait();  //无限等待
                            //obj.wait(5000); //计时等待, 5秒 时间到,自动醒来
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println( Thread.currentThread().getName() + "=== 从waiting状
态醒来,获取到锁对象,继续执行了");
                    }
                }
            }
        },"等待线程").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
//                while (true){   //每隔3秒 唤醒一次
                    try {
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒钟");
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj){
                        System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 获取到锁对
象,调用notify方法,释放锁对象");
                        obj.notify();
                    }
                }
//            }
        },"唤醒线程").start();
    }
}

通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的 Object.notify() 方法或 Object.notifyAll() 方法。
其实 waiting 状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
当多个线程协作时,比如 A,B 线程,如果 A 线程在Runnable(可运行)状态中调用了wait()方法那么 A 线程就进入了 Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了 notify() 方法,那么就会将无限等待的 A 线程唤醒。注意是唤醒,如果获取到锁对象,那么 A 线程唤醒后就进入 Runnable(可运行)状态;如果没有获取锁对象,那么就进入到 Blocked(锁阻塞状态)。
Waiting 线程状态图
无限等待

补充

线程状态图

tips:
我们在翻 阅API 的时候会发现 Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,
比如 Waiting(无限等待) 状态中 wait 方法是空参的,而 timed waiting(计时等待) 中 wait 方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于 Timed Waiting 状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从 Timed Waiting 状态立刻唤醒。

等待唤醒机制

线程间通信

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
为什么要处理线程间通信:
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

等待唤醒机制

什么是等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll() 来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
等待唤醒中的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列(ready queue)中
  2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

  • 总结如下:
    • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
    • 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
      调用 wait 和 notify 方法需要注意的细节
  1. wait 方法与 notify 方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过 notify 唤醒使用同一个锁对象调用的 wait 方法后的线程。
  2. wait 方法与 notify 方法是属于 Object 类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了 Object 类的。
  3. wait 方法与 notify 方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这 2 个方法。

生产者与消费者问题

等待唤醒机制其实就是经典的“生产者与消费者”的问题。
就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:

包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。

代码演示:
包子资源类:

public class BaoZi {
     String  pier ;
     String  xianer ;
     boolean  flag = false ;//包子资源 是否存在  包子资源状态
}

吃货线程类:

public class ChiHuo extends Thread{
    private BaoZi bz;

    public ChiHuo(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }
    @Override
    public void run() {
        while(true){
            synchronized (bz){
                if(bz.flag == false){//没包子
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
                bz.flag = false;
                bz.notify();
            }
        }
    }
}

包子铺线程类:

public class BaoZiPu extends Thread {

    private BaoZi bz;

    public BaoZiPu(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        int count = 0;
        //造包子
        while(true){
            //同步
            synchronized (bz){
                if(bz.flag == true){//包子资源  存在
                    try {

                        bz.wait();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 没有包子  造包子
                System.out.println("包子铺开始做包子");
                if(count%2 == 0){
                    // 冰皮  五仁
                    bz.pier = "冰皮";
                    bz.xianer = "五仁";
                }else{
                    // 薄皮  牛肉大葱
                    bz.pier = "薄皮";
                    bz.xianer = "牛肉大葱";
                }
                count++;

                bz.flag=true;
                System.out.println("包子造好了:"+bz.pier+bz.xianer);
                System.out.println("吃货来吃吧");
                //唤醒等待线程 (吃货)
                bz.notify();
            }
        }
    }
}

测试类:

public class Demo {
    public static void main(String[] args) {
        //等待唤醒案例
        BaoZi bz = new BaoZi();

        ChiHuo ch = new ChiHuo("吃货",bz);
        BaoZiPu bzp = new BaoZiPu("包子铺",bz);

        ch.start();
        bzp.start();
    }
}

执行效果:

包子铺开始做包子
包子造好了:冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子
包子铺开始做包子
包子造好了:薄皮牛肉大葱
吃货来吃吧
吃货正在吃薄皮牛肉大葱包子
包子铺开始做包子
包子造好了:冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子

线程池

  • Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来许多好处。
    • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
    • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用

线程池优点

  • 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
  • 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。

线程池概念

  • 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
    由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:
    线程池原理
    合理利用线程池能够带来三个好处:
  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池的使用

  • Executors 框架实现的就是线程池的功能。
    Executors 工厂类中提供的 newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其实也只是 ThreadPoolExecutor 的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池,
    Executor 工厂类如何创建线程池图:
    [Executor工厂类如何创建线程.png]

线程池四种创建方式?

  • Java 通过 Executors(jdk1.5 并发包)提供四种线程池,分别为
    1. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
    4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

Executor 和 Executors 的区别

  • Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
  • Executor 接口对象能执行我们的线程任务。
  • ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
  • 使用 ThreadPoolExecutor 可以创建自定义线程池。

四种构建线程池

newCachedThreadPool

  • 特点:newCachedThreadPool 创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要增加时, 它可以灵活的添加新的线程,而不会对池的长度作任何限制
  • 缺点:他虽然可以无线的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值
  • 总结:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
  • 代码示例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestNewCachedThreadPool {
public static void main(String[] args) {
// 创建无限大小线程池,由 jvm 自动回收
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int temp = i;
newCachedThreadPool.execute(new Runnable() {
public void run() {
try {
Thread.sleep(100);
} catch (Exception e) {
} System.out.println(Thread.currentThread().getName() + ",i==" + temp);
}
});
}
}
}
```

newFixedThreadPool

  • 特点:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。
  • 缺点:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致 OOM(超出内存空间)
  • 总结:请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。如 Runtime.getRuntime().availableProcessors()
    Runtime.getRuntime().availableProcessors()方法是查看电脑 CPU 核心数量)
  • 代码示例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestNewFixedThreadPool {
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int temp = i;
newFixedThreadPool.execute(new Runnable() {
public void run() { System.out.println(Thread.currentThread().getName() + ",i==" + temp);
}
});
}
}
}
```

newScheduledThreadPool

  • 特点:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于 Timer(Timer 是 Java 的一个定时器类)
  • 缺点:由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。
  • 代码示例

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class TestNewScheduledThreadPool {
public static void main(String[] args) {
//定义线程池大小为3
ScheduledExecutorService newScheduledThreadPool =
Executors.newScheduledThreadPool(3);
for (int i = 0; i < 10; i++) {
final int temp = i;
newScheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("i:" + temp);
}
}, 3, TimeUnit.SECONDS);//这里表示延迟3秒执行。
}
}
}
```

newSingleThreadExecutor

  • 特点:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • 缺点:缺点的话,很明显,他是单线程的,高并发业务下有点无力
  • 总结:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它
  • 代码示例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestNewSingleThreadExecutor {
public static void main(String[] args) {
ExecutorService newSingleThreadExecutor =
Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " index:" + index);
try {
Thread.sleep(200);
} catch (Exception e) {
}
}
});
}
}
}
```

线程池状态

  • RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
  • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
  • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
  • TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
  • TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

submit() 和 execute() 方法有什么区别

  • 相同点:
    • 相同点就是都可以开启线程执行池中的任务。
  • 不同点:
    • 接收参数:execute()只能执行 Runnable 类型的任务。submit() 可以执行 Runnable 和 Callable 类型的任务。
    • 返回值:submit() 方法可以返回持有计算结果的 Future 对象,而 execute() 没有
    • 异常处理:submit() 方便 Exception 处理

ThreadPoolExecutor 饱和策略有哪些?

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException 来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

自定义线程线程池

  • 先看 ThreadPoolExecutor(线程池)这个类的构造参数
    [ThreadPoolExecutor构造参数图.png]
    构造参数参数介绍:
corePoolSize 核心线程数量
maximumPoolSize 最大线程数量
keepAliveTime 线程保持时间,N个时间单位
unit 时间单位(比如秒,分)
workQueue 阻塞队列
threadFactory 线程工厂
handler 线程池拒绝策略
  • 代码示例
import java.util.concurrent.ArrayBlockingQueue;  
import java.util.concurrent.ThreadPoolExecutor;  
import java.util.concurrent.TimeUnit;  
public class Test001 {  
    public static void main(String[] args) {  
        //创建线程池  
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L,  
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));  
        for (int i = 1; i <= 6; i++) {  
            TaskThred t1 = new TaskThred("任务" + i);  
            //executor.execute(t1);是执行线程方法  
            executor.execute(t1);  
        }  
        //executor.shutdown()不再接受新的任务,并且等待之前提交的任务都执行完再关闭,阻塞队列中的任务不会再执行。  
        executor.shutdown();  
    }  
}  
class TaskThred implements Runnable {  
    private String taskName;  
    public TaskThred(String taskName) {  
        this.taskName = taskName;  
    }  
    @Override  
    public void run() {  
        System.out.println(Thread.currentThread().getName() + taskName);  
    }  
}

线程池的执行原理

[线程池的执行原理.png]

  • 提交一个任务到线程池中,线程池的处理流程如下:
    1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
    2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
    3. 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

并发容器

  1. ConcurrentHashMap:并发版 HashMap
  2. CopyOnWriteArrayList:并发版 ArrayList
  3. CopyOnWriteArraySet:并发 Set
  4. ConcurrentLinkedQueue:并发队列 (基于链表)
  5. ConcurrentLinkedDeque:并发队列 (基于双向链表)
  6. ConcurrentSkipListMap:基于跳表的并发 Map
  7. ConcurrentSkipListSet:基于跳表的并发 Set
  8. ArrayBlockingQueue:阻塞队列 (基于数组)
  9. LinkedBlockingQueue:阻塞队列 (基于链表)
  10. LinkedBlockingDeque:阻塞队列 (基于双向链表)
  11. PriorityBlockingQueue:线程安全的优先队列
  12. SynchronousQueue:读写成对的队列
  13. LinkedTransferQueue:基于链表的数据交换队列
  14. DelayQueue:延时队列

ConcurrentHashMap 并发版 HashMap

最常见的并发容器之一,可以用作并发场景下的缓存。底层依然是哈希表,但在 JAVA 8 中有了不小的改变,而 JAVA 7 和 JAVA 8 都是用的比较多的版本,因此经常会将这两个版本的实现方式做一些比较(比如面试中)。
一个比较大的差异就是,JAVA 7 中采用分段锁来减少锁的竞争,JAVA 8 中放弃了分段锁,采用 CAS(一种乐观锁),同时为了防止哈希冲突严重时退化成链表(冲突时会在该位置生成一个链表,哈希值相同的对象就链在一起),会在链表长度达到阈值(8)后转换成红黑树(比起链表,树的查询效率更稳定)。

CopyOnWriteArrayList 并发版 ArrayList

并发版 ArrayList,底层结构也是数组,和 ArrayList 不同之处在于:当新增和删除元素时会创建一个新的数组,在新的数组中增加或者排除指定对象,最后用新增数组替换原来的数组。
适用场景:由于读操作不加锁,写(增、删、改)操作加锁,因此适用于读多写少的场景。
局限:由于读的时候不会加锁(读的效率高,就和普通 ArrayList 一样),读取的当前副本,因此可能读取到脏数据。如果介意,建议不用。

public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;
    // 读元素,不加锁,因此可能会读到旧数据
    public E get(int index) {
        return get(getArray(), index);
    }
    // 添加元素有锁
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();    // 修改时加锁,保证并发安全
        try {
            Object[] elements = getArray(); // 当前数组
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);    // 创建一个新数组,比老的打一个空间
            newElements[len] = e;   // 要添加的元素放进新数组
            setArray(newElements);  // 用新数组替换原来的数组
            return true;
        } finally {
            lock.unlock();  // 解锁
        }
    }
}

opyOnWriteArraySet 并发 Set

基于 CopyOnWriteArrayList 实现(内含一个 CopyOnWriteArrayList 成员变量),也就是说底层是一个数组,意味着每次 add 都要遍历整个集合才能知道是否存在,不存在时需要插入(加锁)。
适用场景:在 CopyOnWriteArrayList 适用场景下加一个,集合别太大(全部遍历伤不起)。

ConcurrentLinkedQueue 并发队列 (基于链表)

基于链表实现的并发队列,使用乐观锁 (CAS) 保证线程安全。因为数据结构是链表,所以理论上是没有队列大小限制的,也就是说添加数据一定能成功。

ConcurrentLinkedDeque 并发队列 (基于双向链表)

基于双向链表实现的并发队列,可以分别对头尾进行操作,因此除了先进先出 (FIFO),也可以先进后出(FILO),当然先进后出的话应该叫它栈了。

ConcurrentSkipListMap 基于跳表的并发 Map

SkipList 即跳表,跳表是一种空间换时间的数据结构,通过冗余数据,将链表一层一层索引,达到类似二分查找的效果

ConcurrentSkipListSet 基于跳表的并发 Set

类似 HashSet 和 HashMap 的关系,ConcurrentSkipListSet 里面就是一个 ConcurrentSkipListMap,就不细说了。

ArrayBlockingQueue 阻塞队列 (基于数组)

基于数组实现的可阻塞队列,构造时必须制定数组大小,往里面放东西时如果数组满了便会阻塞直到有位置(也支持直接返回和超时等待),通过一个锁 ReentrantLock 保证线程安全。

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    /*
    * 读写共用此锁,线程间通过下面两个 Condition 通信
    * 这两个 Condition 和 lock 有紧密联系(就是 lock 的方法生产的)
    * 类似 Object 的 wait/notify    * */    final ReentrantLock lock;
    // 队列不为空的信号,取数据需要关注
    private final Condition notEmpty;
    // 队列没满的信号,写数据需要关注
    private final Condition notFull;
    // 一直阻塞知道有东西可以拿出来
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }    // 在尾部插入一个元素,队列已满时等待指定时间,如果还是不能插入则返回
    public boolean offer(E e, long timeout, TimeUnit unit)
            throws InterruptedException{
        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();   // 锁住
        try {
            // 循环等待队列有空间
            while (count == items.length) {
                if (nanos <= 0)
                    return false; // 等待超时,返回
                // 暂时放出锁,等待一段时间(可能被提前唤醒并抢到锁,所以需要循环判断条件)
                // 这段时间可能其他线程取走元素,这样就有机会插入了
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e); // 插入一个元素
            return true;
        } finally {
            lock.unlock();  // 解锁
        }
    }
}

乍一看会有点疑惑,读和写都是同一个锁,那要是空的时候正好一个读线程来了不会一直阻塞吗?
答案就在 notEmpty、notFull 里,这两个出自 lock 的小东西让锁有了类似 synchronized + wait + notify 的功能。传送门 → 终于搞懂了 sleep/wait/notify/notifyAll

LinkedBlockingQueue 阻塞队列 (基于链表)

基于链表实现的阻塞队列,想比与不阻塞的 ConcurrentLinkedQueue,它多了一个容量限制,如果不设置默认为 int 最大值。

LinkedBlockingDeque 阻塞队列 (基于双向链表)

类似 LinkedBlockingQueue,但提供了双向链表特有的操作。

PriorityBlockingQueue 线程安全的优先队列

构造时可以传入一个比较器,可以看做放进去的元素会被排序,然后读取的时候按顺序消费。某些低优先级的元素可能长期无法被消费,因为不断有更高优先级的元素进来。

SynchronousQueue 数据同步交换的队列

一个虚假的队列,因为它实际上没有真正用于存储元素的空间,每个插入操作都必须有对应的取出操作,没取出时无法继续放入。

import java.util.concurrent.SynchronousQueue;
public class Main {
    public static void main(String[] args) {
        SynchronousQueue<Integer> queue = new SynchronousQueue<>();
        new Thread(()->{
            try{
                for(int i=0;;i++){
                    System.out.println("放入:" + i);
                    queue.put(i);
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try{
                while(true){
                    System.out.println("取出:" + queue.take());
                    Thread.sleep((long)(Math.random()*2000));
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }).start();
    }
}

运行结果:

取出:0
放入:0
取出:1
放入:1
放入:2
取出:2
取出:3
放入:3
取出:4
放入:4
...
...

可以看到,写入的线程没有任何 sleep,可以说是全力往队列放东西,而读取的线程又很不积极,读一个又 sleep 一会。输出的结果却是读写操作成对出现。
JAVA 中一个使用场景就是 Executors.newCachedThreadPool(),创建一个缓存线程池。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(
            0,  // 核心线程为0,没用的线程都被无情抛弃
            Integer.MAX_VALUE,  // 最大线程数,理论上是无限,还没到这个值机器资源就被掏空
            60L, TimeUnit.SECONDS,  // 闲置线程 60s 后销毁
            new SynchronousQueue<Runnable>());  // offer 时如果没有空闲线程取出任务,则会失败,线程池创建一个新的线程
}

LinkedTransferQueue 基于链表的数据交换队列

实现了接口 TransferQueue,通过 transfer 方法放入元素时,如果发现有线程在阻塞在取元素,会直接把这个元素给等待线程。如果没有人等着消费,那么会把这个元素放到队列尾部,并且此方法阻塞直到有人读取这个元素。和 SynchronousQueue 有点像,但比它更强大。

DelayQueue 延时队列

可以使放入队列的元素在指定的延时后才被消费者取出,元素需要实现 Delayed 接口。

并发队列

  • 消息队列很多人知道:消息队列是分布式系统中重要的组件,是系统与系统直接的通信
  • 并发队列:并发队列是多个线程以有次序共享数据的重要组件

并发队列和并发集合的区别:

  • 那就有可能要说了,我们并发集合不是也可以实现多线程之间的数据共享吗,其实也是有区别的。
  • 队列遵循“先进先出”的规则,可以想象成排队检票,队列一般用来解决大数据量采集处理和显示的。
    并发集合就是在多个线程中共享数据的

阻塞队列和非阻塞队列区别

  • 当队列阻塞队列为空的时,从队列中获取元素的操作将会被阻塞。
  • 或者当阻塞队列是满时,往队列里添加元素的操作会被阻塞。
  • 或者试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
  • 试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来

常用并发列队

  1. 非堵塞队列:
    1. ArrayDeque, (数组双端队列)
      ArrayDeque (非堵塞队列)是 JDK 容器中的一个双端队列实现,内部使用数组进行元素存储,不允许存储 null 值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队列、栈的绝佳选择,性能比 LinkedList 还要好。
    2. PriorityQueue, (优先级队列)
      PriorityQueue (非堵塞队列) 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象
    3. ConcurrentLinkedQueue, (基于链表的并发队列)
      ConcurrentLinkedQueue (非堵塞队列): 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能。ConcurrentLinkedQueue 的性能要好于 BlockingQueue 接口,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不允许 null 元素。
  2. 堵塞队列
    1. DelayQueue, (基于时间优先级的队列,延期阻塞队列)
      DelayQueue 是一个没有边界 BlockingQueue 实现,加入其中的元素必需实现 Delayed 接口。当生产者线程调用 put 之类的方法加入元素时,会触发 Delayed 接口中的 compareTo 方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。
    2. ArrayBlockingQueue, (基于数组的并发阻塞队列)
      ArrayBlockingQueue 是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue 是以先进先出的方式存储数据
    3. LinkedBlockingQueue, (基于链表的 FIFO 阻塞队列)
      LinkedBlockingQueue 阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为 Integer.MAX_VALUE 的容量 。它的内部实现是一个链表。
    4. LinkedBlockingDeque, (基于链表的 FIFO 双端阻塞队列)
      LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
      相比于其他阻塞队列,LinkedBlockingDeque 多了 addFirst、addLast、peekFirst、peekLas t等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以 last 结尾的方法,表示插入、获取获移除双端队列的最后一个元素。
      LinkedBlockingDeque 是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为 Integer.MAX_VALUE。
    5. PriorityBlockingQueue, (带优先级的无界阻塞队列)
      priorityBlockingQueue 是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现 comparable 接口。
    6. SynchronousQueue (并发同步阻塞队列)
      SynchronousQueue 是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。
      将这个类称为队列有点夸大其词。这更像是一个点。

并发队列的常用方法

不管是那种列队,是那个类,当是他们使用的方法都是差不多的

方法名 描述
add() 在不超出队列长度的情况下插入元素,可以立即执行,成功返回 true,如果队列满了就抛出异常。
offer() 在不超出队列长度的情况下插入元素的时候则可以立即在队列的尾部插入指定元素,成功时返回 true,如果此队列已满,则返回 false。
put() 插入元素的时候,如果队列满了就进行等待,直到队列可用。
take() 从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
poll(long timeout,TimeUnit unit) 在给定的时间里,从队列中获取值,如果没有取到会抛出异常。
remainingCapacity() 获取队列中剩余的空间。
remove(Object o) 从队列中移除指定的值。
contains(Object o) 判断队列中是否拥有该值。
drainTo(Collectionc) 将队列中值,全部移除,并发设置到给定的集合中。

并发工具类

常用的并发工具类有哪些?

  • CountDownLatch
    CountDownLatch 类位于 java.util.concurrent 包下,利用它可以实现类似计数器的功能。比如有一个任务 A,它要等待其他 3 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch 来实现这种功能了。
  • CyclicBarrier (回环栅栏) CyclicBarrier 它的作用就是会让所有线程都等待完成后才会继续下一步行动。
    CyclicBarrier 初始化时规定一个数目,然后计算调用了 CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。
    CyclicBarrier 初始时还可带一个 Runnable 的参数, 此 Runnable 任务在 CyclicBarrier 的数目达到后,所有其它线程被唤醒前被执行。
  • Semaphore (信号量) Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(允许自定义多少线程同时访问)。就这一点而言,单纯的 synchronized 关键字是实现不了的。
posted @ 2025-01-22 15:42  Thousand_Mesh  阅读(36)  评论(0)    收藏  举报