JUC 多线程

冷暖自知 提交于 2020-01-12 14:56:43

JUC

​ 在Java 5.0 提供了 java.util.concurrent (简称 JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。

一、多线程回顾

线程和进程

​ 程序是完成特定任务、用某种语言编写的一段代码,程序是静态的,程序运行后变为一个进程,进程是动态的。一个进程内可以有多个线程同时执行。进程是所有线程的集合,每一个线程是进程中的一条执行路径。

多线程的使用优势

​ 提高应用程序的响应;提高 CPU 的利用率;改善冗长、复杂的程序结构;使用线程可以耗时任务放到后台去处理。

多线程的创建方式

方式一:继承 Thread 类

  1. 创建一个继承 Thread 类的子类
  2. 重写 Thread 类的 run() --> 将此线程执行的操作声明在 run() 中
  3. 创建 Thread 类的子类的对象
  4. 通过此对象调用 start():① 启动当前线程 ② 调用当前线程的 run()

方式二:实现 Runnable 接口

  1. 创建一个实现 Runnable 接口的类
  2. 实现类去实现 Runnable 中的抽象方法:run()
  3. 创建实现类的对象
  4. 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
  5. 通过 Thread 类的对象调用 start()
// 当线程只需要创建一次时,可以使用匿名内部类的方式创建
new Thread(new Runnable() {
			@Override
			public void run() {
			// 逻辑代码
			}
		}).start();;

方式三:实现 Callable 接口

1.创建一个实现 Callable 的实现类
2.实现 call 方法,将此线程需要执行的操作声明在 call() 中
3.创建 Callable 接口实现类的对象
4.将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的对象
5.将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()
6.获取 Callable 中 call 方法的返回值,get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call() 的返回值。

// 匿名内部类
Callable<Object> callable = new Callable<Object>() {

	@Override
	public Object call() throws Exception {
		// 逻辑代码
		return "该值可以被FutureTask.get()拿到";
	}
};

FutureTask<Object> futureTask = new FutureTask<>(callable);

new Thread(futureTask, "线程A").start();

    try {
        Object object = futureTask.get();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

​ Callable 接口 比 Runnable 强大之处:

	1. call() 方法相比 run() 方法有返回值。
	2. call() 方法可以将异常抛出(throws)。
	3. 支持泛型的返回值。
	4. Callable 接口一般用于配合 ExecutorService 使用。

方式四:使用线程池

​ 并发情况下,大量的线程创建和销毁对资源的消耗很大,因此创建即用即取的线程池,可以避免频繁创建和销毁,方便线程管理,实现重复利用,提高性能。

线程池的常用属性:

​ corePoolSize:核心池的大小,默认情况下核心线程一直存活,即使处于闲置状态也不会受存keepAliveTime限制。

​ maximumPoolSize:最大线程数,超过这个数的线程将被阻塞。

​ keepAliveTime:线程没有任务时最多保持多长时间后会终止。

步骤:

  1. 创建线程池并指定线程数量。

    ​ ThreadPoolExecutor 是线程池的真正实现,它通过构造方法的一系列参数,来构成不同配置的线程池

  2. 执行指定的线程的操作。需要提供实现 Runnable 接口或 Callable 接口实现类的对象

  3. 关闭线程池

ExecutorService pool = Executors.newFixedThreadPool(10);	// 指定池中的线程数量

ThreadPoolExecutor service = (ThreadPoolExecutor) pool;	

// 设置线程池的属性
service.setCorePoolSize(5);	// 核心池的大小,默认情况下核心线程会一直存活。
service.setKeepAliveTime(2, TimeUnit.SECONDS);	// 设置闲置线程 2 秒后关闭。
service.setMaximumPoolSize(20);		// 再次设置最大线程数,如果小于构造器中的值,则该值优先。

service.execute(new Thread(new Runnable())); // 适合用于实现 Runnable 接口的类
service.submit(new THread(new Callable<V>()));// 适合用于实现 Callable 接口的类

service.shutdown();

线程的常用方法

Thread 中常用 API 解释
start() 启动线程
currentThread() 获取当前线程对象
getID() 获取当前线程ID
getName() 获取当前线程名称,Thread-编号 该编号从0开始
setName() 设置当前线程的名字
sleep(long mill) 休眠线程
stop() 停止线程
yield() 释放 cpu 的操作
join() 加塞,谁调 join() 谁先执行,执行完该线程后,其他线程才能执行。
isAlive() 判断线程是否还活着

线程的优先级

线程的优先级控制:

MAX_PRIORITY(10)

MIN _PRIORITY (1)

NORM_PRIORITY (5)

常用方法:

getPriority() **:**返回线程优先级

setPriority(int newPriority) **:**设置线程的优先级

​ 子线程创建时继承父线程的优先级。

线程的生命周期

在这里插入图片描述

线程的状态

在这里插入图片描述

线程的分类

​ Java 中的线程分为两类:一种是守护线程(后台线程),一种是用户线程(前台线程)。它们唯一的区别是判断JVM何时离开。

​ 守护线程是用来服务用户线程的,通过在 start() 方法前调用 thread.setDaemon(true) 可以把一个用户线程变成一个守护线程。当主线程不存在或主线程停止时,守护线程也会停止。

	Java垃圾回收就是一个典型的守护线程。若 JVM 中都是守护线程,当前 JVM 将退出。

线程的停止

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

​ 最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。

​ 2.使用 interrupt() 方法中断线程

​ 线程处于阻塞状态:

​ 如使用了 sleep ,同步锁的 wait , socket 中的 receiver , accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt() 方法时,会抛出 **InterruptException **异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。

​ 线程未处于阻塞状态:

​ 使用 **isInterrupted() **判断线程的中断标志来退出循环。当使用 interrupt() 方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。

​ 3.使用 stop 方法强行终止线程(该方法已被废弃,使用它是极端不安全的)

二、线程安全问题

​ 当多个线程同时共享同一个全局变量或静态变量,记性写的操作时,可能会发生数据冲突,造成线程安全问题。

同步机制

方式一:同步代码块

​ 同步监视器可以是任意类的对象

​ 实现 Runnable 的方式同步监视器可以用 this;继承 Thread 的方式同步监视器不能使用 this

​ 继承 Thread 的方式在实现线程安全的问题上,共享数据必须 static 修饰保证只有一份,同步监视器不能使用 this,因为可能会创建多个对象。

synchronized (同步监视器/) {
	// 操作共享数据的代码
}

方式二:同步方法

​ 在实现 Runnable 的方式中,同步方法不用加 static,默认的同步监视器是 this

​ 在继承 Thread 的方式中,同步方法必须加 static,共享数据也是 static,默认的同步监视器就是 (当前类.class)

权限修饰符  synchronized 返回值类型   方法名(形参列表){
 	// 操作共享数据的代码
}

常见问题

① 什么是静态同步函数,它使用什么锁?

​ 使用 static 和 synchronized 关键字同时修饰的函数为静态同步函数。静态同步函数使用的锁是该函数所属字节码文件对象,即类.class。

② 同步代码块与同步函数区别?

​ 同步代码使用自定义锁(明锁),同步函数使用this锁。

③什么是多线程中的死锁?

​ 死锁指不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

使用 Lock 解决线程安全问题

​ 从 JDK5.0 开始,Java提供了 java.util.concurrent.locks.Lock 接口,通过显式定义同步锁对象来实现同步。

​ ReentrantLock 重入锁,是实现 Lock 接口的一个类,也是在实际编程中使用频率很高的一个锁。

​ 相比 synchronized 的由系统自动获取锁和释放锁,Lock 需要手动实现加锁和释放锁,因此也更加灵活。

class Thread implements Runnable{
	private ReentrantLock lock =new ReentrantLock(true);	// true 代表开启公平模式
	@Override
	public void run() {
			try {
				lock.lock();
				//操作资源
			} finally {
				lock.unlock();
			}
		}
}

三、线程的通信

线程阻塞与唤醒

wait()	// 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify()	// 一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级高的那个。
notifyAll()		// 一旦执行此方法,就会唤醒所有被wait的线程。
/*
说明:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
*/

【面试题】sleep() 和 wait() 的异同?

相同点:sleep() 和 wait() 都可以使得当前的线程进入阻塞状态。
不同点:
1.两个方法声明的位置不同:Thread 类中声明 sleep() , Object类中声明 wait()。
2.调用的要求不同:sleep() 可以在当前线程的任何场景下调用。 wait() 必须使用在同步代码块或同步方法中。
3.关于是否释放锁:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait() 会释放锁。

使用 Lock 后的线程通信

​ Condition 的功能类似于在传统的线程技术中的,Object.wait() 和 Object.notify() 的功能。

​ Condition.await() 类似于 wait。

​ Condition.signal() 类似于 notify。

class Thread implements Runnable{
    private ReentrantLock lock = new ReentrantLock(true);	// true 代表开启公平模式
    private Condition condition = lock.newCondition();	// 创建 condition 对象
    @Override
    public void run() {
			try {
                lock.lock();
                conditoin.signal();		// 唤醒线程
                //操作资源
                try {
                    condition.await();	// 阻塞线程
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
			} finally {
				lock.unlock();
			}
		}
}

四、JUC 工具类

ReentrantReadWriteLock

​ JAVA 的并发包提供了读写锁 ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。

CountDownLatch

​ CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞。其它线程调用countDown 方法会将计数器减 1 (调用 countDown 方法的线程不会阻塞),当计数器的值变为0时,因 await 方法阻塞的线程会被唤醒,继续执行。

CyclicBarrier

​ CyclicBarrier 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过 CyclicBarrier 的 await() 方法。

Semaphore

​ acquire(获取) 当一个线程调用 acquire 操作时,获取信号量(信号量减1),如果当前信号量为0,则会一直等到有线程释放信号量,或超时。

​ release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。

​ 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

锁的划分

​ ① 特质上分:共享锁和排他锁

​ ② 用途上分: 读锁和写锁

​ ③ 数据库: 表锁和行锁

​ ④ 世界观划分: 悲观锁(真锁)和乐观锁(假锁)

才会继续干活。线程进入屏障通过 CyclicBarrier 的 await() 方法。

Semaphore

​ acquire(获取) 当一个线程调用 acquire 操作时,获取信号量(信号量减1),如果当前信号量为0,则会一直等到有线程释放信号量,或超时。

​ release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。

​ 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

锁的划分

​ ① 特质上分:共享锁和排他锁

​ ② 用途上分: 读锁和写锁

​ ③ 数据库: 表锁和行锁

​ ④ 世界观划分: 悲观锁(真锁)和乐观锁(假锁)

​ ⑤ 是否明显: 显式锁和隐式锁

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!