多线程
1. 多线程基础
-
对于程序、进程、线程的理解
- 程序:为完成某种目的,而使用一种语言编写的一组指令集合。
- 进程:一个运行中的程序。它是一个动态的过程,同时进程也是操作系统资源分配的最基本单位
- 线程:一个程序内部的执行路径,由进程细化而来,线程是操作系统调度和执行的基本单位。
-
多线程的创建一共有四种方式(更具体的内容参照4.线程的新增创建方式)
- 继承
Thread
类 - 实现
Runnable
接口:在实际开发中优先选择(相比于继承而言,实际上使用最对多的是直接创建线程池)- 对比:使用接口实现避免了Ja va单继承的局限性,并且多个线程可以共享一个接口实现类的对象,具有天然的数据共享性
- 实现
Callable
接口:JDK5.0新增 - 使用
线程池
- 继承
-
启动一个线程必须调用start()方法,不能调用run()方法
- 调用run()方法就相当于调用一个普通的方法。而只有调用start方法,才能启动一个线程,并使其处于可运行状态,这才能够被JVM调度并执行
-
start()只能调用一次,不能让已经start()的线程再次start(),它内部有一个变量专门记录是否是首次启动,如果不是会报错:非法线程状态(ILLegalThreadStateException)
-
线程优先级
- 高优先级的线程要抢占低优先级的线程CPU,但这并不是绝对了,只是高优先级的线程有更高的概率被优先执行。
-
线程分类
- 守护线程:不能单独存在,必须辅以用户线程才能存在,用户线程结束,守护线程结束。Java的垃圾回收就是一个典型的守护线程。
- 用户线程:如main方法
-
线程的几种状态
- NEW(新建):新创建的一个线程类,但未调用start()方法
- RUNNABLE(就绪\运行):当一个新建线程调用
start()
方法之后进入就绪
状态,进入线程队列等待CPU分配时间片,一旦得到CPU
时间片只有变进入运行状态
run()方法指定了线程的操作和功能 - BLOCKED(阻塞):需要等待同步锁的释放、join()等。
- WAITING(等待状态):表示该线程需要等待其他线程做出⼀些特定动作(通知或中断)、sleep()等
- TIMED_WAITING(超时等待):可以在指定的时间后⾃⾏返回⽽不是像 WAITING 那样⼀直等
待。 - TERMINATED(终止状态):表示线程已经运行完毕。
2. 线程同步
-
线程同步问题(线程安全):多个线程
修改
共享数据,都会出现线程安全问题(多个线程读共享数据不会出现问题)-
解决方法
-
同步代码块(一)
-
synchronized(同步监视器){ //需要被同步的代码块 } // 同步监视器:锁;多个线程必须共用同一把锁
-
-
同步方法(二)
-
synchronized void show(){ //非静态的同步方法,,同步监视器是`this` //需要被同步的代码 } // 然后在run方法中调用show方法 static synchronized void show(){ //静态的同步方法,同步监视器是:`当前类本身` //需要被同步的代码 }
-
-
-
-
单例模式
-
//懒汉式 // #懒汉式:要使用才创建,不使用就不创建。 // ##普通懒汉式:执行效率低 无论是否已经存在实例,在多线程的情况下都会发生阻塞。 public class Dog { private Dog() {} // 防止调用构造方法 private static Dog instance = null; public synchronized static Dog getInstance() { //无论是否已经实例化,这里都会被阻塞 if (instance == null) instance = new Dog(); return instance; } } // ##双重判断型单例模式 public class Dog { private Dog() {} private volatile static Dog instance = null; // 防止出现指令重排的问题 public static Dog getInstance() { // 确定是否需要阻塞 if (instance == null){ synchronized(Dog.class){ //这里在多线程的情况下会出现指令重排的问题,所以对共有资源instance使用关键字volatile修饰 if (instance == null) instance = new Dog(); } } return instance; } }
-
//饿汉式:不管使不使用都先创建 class Dog{ private Dog(){} private static Dog instance = new Bank(); public static Dog getInstance(){ return instance; }
-
-
死锁问题
-
产生的原因
互斥
:锁在同一时刻只能被一个线程使用不可剥夺
:其他线程无法抢夺已经被占有的锁请求与保持
:线程持有锁,不释放,并请求其他的锁。循环等待
:持有锁资源的线程相互等待。
-
-
解决方式:破坏上述任意一个条件即可(一般是采用专门的算法,或者原则)
- 资源不互斥(难以做到)
- 可剥夺
- 同时请求资源,要么同时持有所需资源,要么都不持有
- 为资源编号,线程只能从小到大依次请求资源,不能请求一个更小编号的资源
- 资源不互斥(难以做到)
- 线程同步-Lock(锁)问题
-
- 解决现场安全问题的方式三:Lock锁 — JDK5新增
- JUC的包下面由一个 ReentrantLock类实现了Lock锁,其本身有两个构造方法,其中一个可以实现公平锁,每一个进入的进程形成一种队列分方式,先进先出
- synchronized与Lock的异同
- 相同点:都可以解决线程安全问题
- 不同点:
- synchronized是一种隐私锁,在执行完相应的同步代码之后,自动释放同步监视器
- Lock是一种显示锁,需要手动的启动同步(
lock()
),也需要手动结束同步(unlock()
)。性能更好,JVM调度线程的时间开销小
-
// #线程安全:方式三 // Lock实现 ReentrantLock lock = new ReentrantLock(); ReentrantLock lock = new ReentrantLock(true);//实现公平锁,进来的线程组成一个队列。 // 在进程里面加锁 public void run(){ try{ // 加锁 lock.lock() //同步代码块 }finally{ // 解锁 lock.unlock() } }
3. 线程通信
- 线程通信有多种方式
- 第一种方式Object方法:
wait() 、notify()、notifyAll()
- 这三个方法必须使用在同步代码块或同步方法中,且必须是同步监视器
- sleep()与wait()方法的不同
- 相同点;两个方法都可以使当前线程进入阻塞状态
- 不同点:
- 声明位置不同:sleep()声明在Thread类中,wait()声明在Object类中
- 调用要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块中
- 如果使用在同步代码块或者方法中:sleep()不会释放锁,wait()会释放锁,wait()需要被notify()唤醒
- 第一种方式Object方法:
// 令当前线程挂起,并放弃cpu、同步资源并等待。
// 可以被notify()、notifyAll()方法唤醒
wait()
// 随机唤醒一个正在等待的线程, 如果有优先级,就唤醒优先级高的。
notify()
// 唤醒所有一个正在等待的线程
notifyAll()
- 经典的线程通信问题
- 生产者和消费者
4. 线程的新增创建方式
- 线程的创建方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口:JDK5.0新增
- 使用线程池
- 线程创建方式三:实现Callable接口
- 与Runable相比,Callable的功能更强大一些
- 相比run()方法,可以有返回值
- 方法可以抛出异常,run()方法只能在内部处理异常不能抛出
- 支持泛型的返回值
- 需要借助
FutureTask
类,比如获取返回结果等Future
接口- 可以对Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
- FutureTask是
Future
接口的唯一实现类 - FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,有可以作为Future得到Callable的返回值
- 与Runable相比,Callable的功能更强大一些
class MyThread implements Callable<T>{
// 线程需要执行的方法
@Override
public T call() throws Exception {
return null;
}
}
// #调用方式,需要使用到FutureTask
MyThread mythread = new MyThread();
FutureTask futureTask = new FutureTask(mythread);
// ##启动线程
new Thread(futureTask).start();
// ##获取线程返回内容返回内容,并处理异常(不需要就不调)
T reback = futureTask.get();
- 线程创建方式四:线程池【最常用】
- 优点
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理(在ThreadPoolExecutor实现类中)
- corePoolSize:核心池的大小
- maximumPollSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- 线程池相关API:接口
ExecutorService
,工具类Executors
- ExecutorService两个方法
- execute():执行任务,没有返回值
- submit():执行任务存在返回值,适合Callable
- Executors工具类,创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
- ExecutorService两个方法
- 优点
class MyThread implements Callable<String>{
@Override
public String call() throws Exception {
return Thread.currentThread().getName()+": test";
}
}
// #线程池
// ## 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// ##启动线程
Future<String> submit = service.submit(new MyThread());
Future<String> submit1 = service.submit(new MyThread());
String o = submit.get();
String o1 = submit1.get();
System.out.println(o);
System.out.println(o1);
// ##关闭线程池
service.shutdown();
// ###输出
pool-1-thread-1: test
pool-1-thread-2: test