Java多线程
一、线程介绍及相关概念
1、程序、进程、线程:
- 程序program:
静态
,为了完成特定任务,用某种语言编写的一组指令的集合,即一段静态的代码
,静态的对象。 - 进程process:
动态
,与操作系统有关,程序的一次执行过程·,正在内存中运行的应用程序。- 进程作为
操作系统调度和分配资源的最小单位
(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
- 进程作为
- 线程(thread):与CPU有关,进程的进一步细化,是程序内部的
一条执行路径
。是CPU调度和执行的最小单位
,在一个进程执行过程中CPU会来回切换执行不同的线程。- Java是支持多线程的:即一个进程同一时间若
并行
执行多个线程。- 多线程的优点:提高应用程序的响应;CPU利用率;改善程序结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
- 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象,便于线程之间通信,但多个线程操作
共享的系统资源
可能就会带来安全的隐患
- 内存泄漏
- 死锁
- 线程不安全——数据混乱、错误或者丢失;而安全则是多线程情况下数据的正确性和一致性。解决:同步机制!!!
- 各个进程独立,各个线程不是,同一进程中的线程会相互影响,线程开销小,但不利于资源的管理和保护。
- 线程分类:IO密集型、CPU密集型
- Java是支持多线程的:即一个进程同一时间若
举例:运行360杀毒软件(一个进程),可以同时执行木马查杀、系统修复、电脑清理(多个线程)。不同进程之间不共享内存,因为数据交换和通信成本很高。进程之间通信举例:支付宝与饿了么之间。(socket)
如果想让多个线程共同完成一个任务,那么他们之间要可以通信(通过共享的系统资源:JVM的堆、方法区;电脑中的文件等等)
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
- 程序计数器是线程私有:其作用是①字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制(顺序、选择、循环、异常处理);②多线程时,记录当前线程执行的位置(下一条指令的地址),当线程切换回来继续执行。【注意执行native方法记录undefined地址】。因此为了线程切换后能恢复到正确的执行位置
- 虚拟机栈私有: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、
常量池
引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 - 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
- 堆和方法区是所有线程共享的资源:堆主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、
常量
、静态变量、即时编译器编译后的代码等数据。
2、单核CPU&多核CPU&并行&并发&同步&异步
- 单核CPU:在一个时间单元内,只能执行一个线程的任务。
- 多核CPU:多几个CPU,但是效率未必就是单核的倍数,因为①多个核心的其他共用资源限制;②多核CPU之间的协调管理损耗
- 并行(parallel):指两个或多个事件在
同一时刻
发生(同时发生)。指在同一时刻,有多条指令
在多个CPU
上同时
执行。 - 并发(concurrency):指两个或多个事件在
同一个时间段内
发生。即在一段时间内,有多条指令
在单个CPU
上快速轮换、交替
执行,使得在宏观上具有多个进程同时执行的效果。——因此我们需要并发编程,JUC
【即创建多个线程的任务同时执行】 - 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
- 异步:调用在发出之后,不用等待返回结果,该调用直接返回。
总结:单核CPU对于多个线程任务只能并发,多核CPU则这些并发执行的程序便可以分配到多个CPU上,实现多任务并行执行,利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。
3、“并发时”,CPU调度线程的方式:
- 分时调度:所有线程
轮流使用
CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。 - 抢占式调度:让
优先级高
的线程以较大的概率
优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
二、创建和启动线程
创建多线程的方式:继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。
1、多线程:Java语言的JVM允许程序运行多个线程,使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
2、Thread类的特性:Thread对象的run()定义线程需要完成的任务,start()启动线程。必须在主线程中创建新的线程对象,才能实现多线程
3、Thread类也是实现Runnable接口
- Thread(Runnable target):无论是Runnable实现类的对象还是Thread子类对象,使用这个构造器本质都是调用target.run()
- Thread():创建Thread子类对象,如new MyThread();
- 无论是Thread类还是Thread子类都可以创建线程【Thread子类需要重载相关的构造器】,比如new MyThread(Runnable target)
2.1 Thread类的常用结构
1、构造器
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
2、常用方法系列1
- public void run() :此线程要执行的任务在此处定义代码。
- public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
- public String getName() :获取当前线程名称。
- public void setName(String name):设置该线程名称。
- public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在Thread子类中就是this,通常用于主线程和Runnable实现类。如:Thread.currentThread().setName(“主线程”);
- public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
- public static void yield():yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
3、常用方法系列2
- public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
- void join() :等待该线程终止。
void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。
void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。 - public final void stop():
已过时
,不建议使用。强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。 - void suspend() / void resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()。
已过时
,不建议使用。
4、常用方法系列3:每个线程都有优先级,分时调度策略:同级先到先服务;抢占式策略:让优先级高的线程以较大的概率优先使用CPU,如果优先级相同会随机选择一个。
- Thread类的三个优先级常量:
- MAX_PRIORITY(10):最高优先级
- MIN _PRIORITY (1):最低优先级
- NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
- public final int getPriority() :返回线程优先级
- public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。
5、守护线程相关操作:在后台运行的,为其他线程提供服务的线程。如JVM垃圾回收线程。特点:其他非守护线程死亡,它会自动死亡。
- setDaemon(true):将指定线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException异常。
- isDaemon():判断线程是否是守护线程。
2.2 创建线程法1:继承Thread类(分配线程对象)
1、实现步骤:创建并启动多线程
- 定义线程需要完成的任务:定义Thread类的子类,并重写该类的run()方法——线程执行体。public void run()
- 创建线程对象:创建Thread子类的实例
- 启动线程:调用线程对象的start()方法来启动该线程。在主线程中创建新的线程对象!
定义线程代码,注意点如下
- run()方法由JVM调用,由操作系统的CPU调度决定,如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
- 启动多线程,必须调用start方法。一个线程对象只能调用一次start()方法启动,否则将抛出异常“
IllegalThreadStateException
”。- 为了让多个Thread子类对象可以共享一个资源对象,可以将这个对象作为构造器参数传入,全程只创建这个对象传入多个子类对象中
package com.atguigu.thread;
//自定义线程类
public class MyThread extends Thread {
//private Person p;//比如这是需要共享的对象,可以定义static,可以作为构造器参数传入
//定义指定线程名称的构造方法:默认时Thread-0依次增长!
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
public MyThread(Person p) {
this.p = p;
}
public MyThread(Person p, String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
package com.atguigu.thread;
public class TestMyThread {
public static void main(String[] args) {
//创建自定义线程对象1
MyThread mt1 = new MyThread("子线程1");
//开启子线程1
mt1.start();
//创建自定义线程对象2
MyThread mt2 = new MyThread("子线程2");
//开启子线程2
mt2.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
执行步骤:主线程中存在两个线程,CPU会来回切换调度执行不同的线程——并行执行多线程
!
2.3 创建线程法2:实现Runnable接口(创建线程的目标对象)
1、实现步骤:Java有单继承的限制,可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行线程体run()方法
- 创建一个实现Runnable接口的类
- 实现接口中的run()–>将此线程要执行的操作,声明在此方法体中
- 创建当前实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例。即Thread t = new Thread(mr, “长江”);
- Thread类的实例调用start():1.启动线程 2.调用当前线程的run()【Thread源码中又target.run()】
总结:如果使用这一个runnable接口实现类的实例来创建多个线程,那么这多个线程共享这一个实例(这个实例在堆中,是所有线程共享的),而单继承Thread类不一样,这个需要创建多个线程对象,但是可以使用静态变量来实现多个对象共享。
原理:多个线程共享一个实例:因为只创建了这一个实例在堆中
Thread这部分的源码:Thread类也是实现Runnable接口的
public class Thread implements Runnable {
/* What will be run. */
private Runnable target;
public Thread(Runnable target){
this( group: null, target, name: "Thread-" + nextThreadNum(),stackSize:0);
}
}
实现Runnable接口,使用Thread的target参数创建Thread线程。
通过Runnable接口的实现类的实例创建两个线程Thread,从始至终之创建了一个实例,因此这两个线程共享一个,该实例时new的因此在栈中,栈中都是共享的,多线程之间通过共享资源(JVM中共享是方法区、堆)来通信。
public class MyRunnable implements Runnable {
int num = 0;//实现类的属性,如果只创建一个对象供多个线程使用,就是多个线程之间共享的
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
public class TestMyRunnable {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象1
Thread t1 = new Thread(mr, "长江");
t1.start();
//创建线程对象2
Thread t2 = new Thread(mr, "大海");//t1、t2两个线程共享同一个实例
t2.start();
for (int i = 0; i < 20; i++) {
System.out.println("黄河 " + i);
}
}
}
使用匿名内部类实现Thread类、或者Runnable接口
//继承Thread类
new Thread("新的线程!"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}.start();
//实现Runnable接口
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" + i);
}
}
}).start();
2、实现Runnable接口VS继承Thread类方式
- 共同点:① 启动线程,使用的都是Thread类中定义的start()② 创建的线程对象,都是Thread类或其子类的实例。
- 不同点:一个是类的继承,一个是接口的实现。
- 建议:建议使用实现Runnable接口的方式。
- Runnable方式的好处:① 实现的方式,避免的类的单继承的局限性 ② 更适合处理有共享数据的问题(只创建一个Runnable实现类的实例),实现多线程之间通信。③ 实现了代码和数据的分离。
联系:public class Thread implements Runnable
- Runnable方式的好处:① 实现的方式,避免的类的单继承的局限性 ② 更适合处理有共享数据的问题(只创建一个Runnable实现类的实例),实现多线程之间通信。③ 实现了代码和数据的分离。
2.4 创建线程法3:实现Callable接口JDK5.0新特性
1、Callable对于Runnable方式的好处
- call():与run()方法对比可以有返回值,更灵活
- call():可以使用throws的方式处理异常(抛出)
- 支持泛型的返回值:Callable使用了泛型参数,可以指明具体的call()的返回值类型,更灵活。(需要借助FutureTask类,获取返回结果)
缺点:如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态的。
Future接口介绍:
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutureTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
2、具体实现方式:比Runnable接口的实现类对经历了FutureTask构造器中构造
创建Callable实现类并重写call()方法→创建Callable接口实现类的对象→将这个对象作为参数传递到FutureTask构造器中
,创建FutureTask的对象→将FutureTask的对象作为参数传递到Thread类的构造器中
,创建Thread对象,并调用start()
Callable接口创建线程的方式
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
// 接收返回值
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();//这里是主线程调用的,这是静态方法,和sleep()一样,在哪个线程中调用就是哪个
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.4 创建线程法4:使用线程池
真正生产环境中使用的创建线程的方式,多个线程可以同时执行一个任务,也可以不同的线程执行各自的任务,也可以一个线程执行多个任务。注意:线程池中最好不要放耗时任务,例如网络请求、文件读写,这些可以采用异步请求操作来处理,伊庇民阻塞线程池中得线程。
前面线程只能处理特定的任务,线程池中的线程可以处理不同的任务–由线程池统一管理其生命周期。
步骤:创建线程(Runnable-Thread继承它、Callable)worker --> 新建线程池(如ThreadPoolExecutor)–> 使用线程池的方法execute(Runnable worker )表示提交一个任务到线程池中去(该方法执行的逻辑就是线程池实现原理)
补充知识点:
1、线程池创建线程的两种方式:
- 通过ThreadPoolExecutor构造函数【推荐】
- 通过Executor框架的
工具类Executors:可以创建多种类型的线程池
:- FixedThreadPool:固定线程数量的线程池。核心线程数=最大线程数。当新任务过来时,如果线程池中有空闲线程立即执行,如果没有,新的任务暂存在任务队列(阻塞队列)–LinkedBlockingQueue无界队列,容量为Integer.MAX_VALUE。
- SingleThreadExecutor:只有一个线程的线程池。核心线程数=最大线程数=1。若>1个任务被提交到该线程池,那么该任务会被保存在阻塞队列中–LinkedBlockingQueue无界队列。
- CachedThreadPool: 可根据实际情况调整线程数量的线程池。最大线程数是Integer.MAX_VALUE,可以理解为线程数可以无限扩展。使用的是SynchronousQueue同步队列(作用就是没有空闲线程时新建线程处理任务)。但是若有空闲线程可以复用,会优先使用可以复用的线程。若果没有空闲线程,且又有新任务提交,就会创建新的线程来处理任务。
- ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池。使用的阻塞队列是DelayedWorkQueue延迟阻塞队列。
2、推荐第一种方式:更加明确线程池的运行规则,避免资源耗尽风险。必须手动通过它的构造函数来声明。也提供了一些检测线程池运行状态–获取线程池当前得线程数/活跃数、已经执行完成得线程数、正在排队中得线程数。
第二种方式:会有OOM风险。使用工具类Executors创建的内置线程池:
- FixedThreadPool、SingleThreadExecutor使用无界队列,可能会堆积大量的任务请求,导致OOM。
- SingleThreadExecutor使用同步队列,会创建大量的线程,导致OOM
- ScheduledThreadPool使用无界的延迟阻塞队列,任务队列最大长度为Integer.MAX_VALUE,
注:规避OOM风险–使用有界队列,控制线程创建数量。
3、提交任务到线程池中的方法
注:使用ThreadPoolExecutor创建的线程池可以使用execute(Runnable 线程)方法提交任务;工具类Executor框架的工具类Executors可以使用execute()、submit()两种方式。
Executors工具类可以将Runnable对象转换成Callable对象。
- execute(Runnable):提交无需返回值的任务,无法判断是否被线程池成功执行
- submit(Runnable/Callable):提交需要返回值的任务,线程池会返回一个Future类型的对象,通过它可以判断任务是否执行成功。其中的get()方法获取返回值–会阻塞当前线程直到任务完成。
4、线程池关闭操作:shutdown() VS shutdownNow();判定
- shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
- shutdownNow() :关闭线程池,线程池的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
- isShutDown 当调用 shutdown() 方法后返回为 true。
- isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true几种常见的内置线程池
5、如何设计?既保证任务不丢丢弃且在服务器有余力时及时处理?–持久化思路
- 设计一张表间任务存储到MySQL数据库中:实现RejectedExecutionHandler接口自定义拒绝策略(将线程池暂时无法处理(此时阻塞队列已满)的任务入库)–> 继承BlockingQueue实现一个混合式阻塞队列,重写take()方法,取任务时优先从数据库中读取最早的任务,数据库中无任务时再从 ArrayBlockingQueue中去取任务。
- Redis缓存任务
- 将任务提交到消息中心
ThreadPoolExecutor 类中提供了四种构造函数,下面为参数最全的一种。其余构造函数在此基础上产生–给给定某些默认参数的构造方法,例如默认制定拒绝策略是什么。
几种参数解释:注意–任务队列存放任务,线程池中运行线程,空闲线程处理队列中的任务
- corePoolSize:任务队列未达到最大值时,最大可以同时运行的线程数量。
- maximumPoolSize:任务队列的任务达到最大容量时,当前可以同时运行线程数量变为最大线程数
- workQueue:如果核心线程数已满,新任务会被存放到队列中。
【阻塞队列
如下:】
- LinkedBlockingQueue(无界队列):队列大小为Integer.MAX_VALUE,任务队列永远不会被放满。理解为任务队列可以无限扩展。
- SynchronousQueue(同步队列):没有容量,不存储元素,保证对于提交的任务,如果有空闲线程,则处理;否则新建一个线程来处理任务。可以理解为线程数可以无限扩展。
- DelayedWorkQueue(延迟阻塞队列):按照延迟的时间长短对任务进行排序,内部采用堆结构,保证每次出队的任务都是当前队列中执行时间最靠前的。
- keepAliveTime:线程数量大于corePoolSize时,如果还没有新任务提交,核心线程外的线程不会被立即销毁,而是会等待。
- unit:keepAliveTime的单位
- threadFactory:executor创建新线程用,线程工厂,用来创建线程
- handler:
拒绝策略
:同时运行的线程达到最大线程数量且队列已经被放满任务时,定义再过来新任务的处理方式。
- AbortPolicy(默认):抛出RejectedExecutionException异常拒绝新任务的处理
- CallerRunsPolicy:调用自己的线程运行任务,在调用execute(线程)方法的线程中运行run被拒绝的任务,如果执行程序关闭则丢弃该任务。–> 缺点:降低对于新任务提交速度(如主线程运行run被拒绝的任务,会导致主线程阻塞,影响程序正常运行)
- DiscardPolicy:不处理新任务直接丢弃。
- DiscardOldestPolicy:丢弃最早的未处理的任务请求。
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
long keepAliveTime,
TimeUnit unit,//时间单位
//任务队列,用来储存等待执行任务的队列
BlockingQueue<Runnable> workQueue,
//线程工厂,用来创建线程,一般默认即可
ThreadFactory threadFactory,
//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
RejectedExecutionHandler handler
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程池创建示例–使用ThreadPoolExecutor创建
import java.util.Date;
/**
* 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
* @author shuang.kou
*/
public class MyRunnable implements Runnable {
private String command;
public MyRunnable(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
Runnable worker = new MyRunnable("" + i);
//执行Runnable
executor.execute(worker);
}
//终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
Future异步思想:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 Future 类获取到耗时任务的执行结果。
使用方式:把线程池放进来。
private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
CompletableFuture.runAsync(() -> {
//...
}, executor);
Executor框架的使用示意图
线程池实现原理
1、为什么需要线程池?使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务
并发的线程数量很多,且每个线程执行很短时间的任务就结束了,频繁创建线程会大大降低系统的效率(因为频繁创建线程和销毁线程需要时间)
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
2、好处:
- 提高执行效率:因为线程已经提前创建好了
- 提高资源的复用率:因为执行完的线程并未销毁,而是可以执行其他的任务。
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
3、线程池相关API
- JDK5.0之前,我们必须手动自定义线程池。从JDK5.0开始,Java内置线程池相关的API。在java.util.concurrent包下提供了线程池相关API:
ExecutorService
和Executors
。 ExecutorService
:真正的线程池接口。常见子类ThreadPoolExecutorvoid execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行Runnable<T> Future<T> submit(Callable<T> task)
:执行任务,有返回值,一般又来执行Callablevoid shutdown()
:关闭连接池
Executors
:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池Executors.newFixedThreadPool(int nThreads)
; 创建一个可重用固定线程数的线程池Executors.newSingleThreadExecutor()
:创建一个只有一个线程的线程池Executors.newScheduledThreadPool(int corePoolSize)
:创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
线程池创建线程
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread2 implements Callable {
@Override
public Object call() throws Exception {
int evenSum = 0;//记录偶数的和
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
evenSum += i;
}
}
return evenSum;
}
}
public class ThreadPoolTest {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// //设置线程池的属性
// System.out.println(service.getClass());//ThreadPoolExecutor
service1.setMaximumPoolSize(50); //设置线程池中线程数的上限
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
try {
Future future = service.submit(new NumberThread2());//适合使用于Callable
System.out.println("总和为:" + future.get());
} catch (Exception e) {
e.printStackTrace();
}
//3.关闭连接池
service.shutdown();
}
}
三、多线程的生命周期
JDK1.5之前:新建、就绪、运行、阻塞、死亡
JDK1.5及之后:新建、可运行(准备、运行)、锁阻塞、计时等待、无限等待、死亡
对比:将阻塞细分为不同的情况
1、多线程生命周期在java.lang.Thread.State的枚举类中定义:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
锁、sleep(线程本身主动放弃)、wait(线程通信-共享中对象调用的)
加塞join、
2、不同的生命周期说明:阻塞状态分为三种:BLOCKED、WAITING、TIMED_WAITING
NEW(新建)
:线程刚被创建,但是并未启动。还没调用start方法。RUNNABLE(可运行)
:(准备/运行)对于Java对象来说,只能标记为可运行,至于什么时候运行,不是JVM来控制的了,是OS来进行调度的,而且时间非常短暂,因此对于Java对象的状态来说,无法区分。Teminated(被终止)
:表明此线程已经结束生命周期,终止运行。BLOCKED(锁阻塞)
:一个正在阻塞、等待一个监视器锁(锁对象)的线程。只有获得锁对象的线程才能有执行机会。- 比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
TIMED_WAITING(计时等待)
:一个正在限时等待另一个线程执行一个(唤醒notify)动作的线程处于这一状态。- 当前线程执行过程中遇到Thread类的
sleep
或join
,Object类的wait
,LockSupport类的park
方法,并且在调用这些方法时,设置了时间
,那么当前线程会进入TIMED_WAITING,直到时间到,或被中断。
- 当前线程执行过程中遇到Thread类的
WAITING(无限等待)
:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。- 当前线程执行过程中遇到遇到Object类的
wait
,Thread类的join
,LockSupport类的park
方法,并且在调用这些方法时,没有指定时间
,那么当前线程会进入WAITING状态,直到被唤醒。- 通过Object类的wait进入WAITING状态的要有Object的notify/notifyAll唤醒;
- 通过Condition的await进入WAITING状态的要有Condition的signal方法唤醒;
- 通过LockSupport类的park方法进入WAITING状态的要有LockSupport类的unpark方法唤醒
- 通过Thread类的join进入WAITING状态,只有调用join方法的线程对象结束才能让当前线程恢复;
- 当前线程执行过程中遇到遇到Object类的
说明:当从WAITING或TIMED_WAITING恢复到Runnable状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入BLOCKED状态。
四、线程安全问题及解决
4.1 线程安全问题介绍(资源)
JVM中多个线程共享的是:方法区:常量、静态变量等;堆:new对象。即多线程在访问这些共享的资源时就会出现问题。
1、线程不安全的原因:
- 情形1:局部变量不能共享:run()中的执行代码定义的局部变量
- 情形2:不同对象的实例变量不能共享:Thread子类定义的实例变量
- 情形3:静态变量是共享的:Thread子类/Runnable实现类中定义的static变量(类属性)
- 情形4:同一个对象的实例变量共享:Runnable实现类如果只创建一个实例对象,通过Thread(Runnable obj)创建多个线程的目标对象。
- 情形5:抽取资源类,共享同一个资源对象:将共享的资源封装成一个对象
总结:Thread子类在创建多线程时,必须是创建多个
Thread子类的线程对象才能实现;Runnable实现类创建多线程时只需创建一个
实例对象,然后调用Thread类创建线程的目标对象即可实现。
实现方式:①抽取资源类,在主线程中定义内部类(继承Thread/实现Runnable接口);②单独创建Thread子类/Runnable实现接口,在主线程中重复使用。
卖票问题:100张票,由三个窗口同时开始售卖:
- 法1使用Runnable实现类的方式(共享一个Runnbale实现类对象,多线程之间可以通信);
存在的问题:这个会出现数据混乱的问题,多线程共享MyRunnable实例对象(包括其属性ticket),假如线程"窗口1"修改ticket资源还没来得及返回堆中,该资源又被线程"窗口2"读取修改,最后会导致这两个线程卖了同一张票!- 法2继承Thread类:创建多个线程对象,每个对象都有自己独立的资源,他们之间没办法通信。
解决:必须保证一个线程a在操作ticket(共享资源)的过程中,其它线程必须等待,直到线程a操作ticket结束以后,其它线程才可以进来继续操作ticket。
②单独创建Thread子类/Runnable实现接口,在主线程中重复使用。
public class OverrideTest {
public static void main(String[] args){
MyRunnable1 myrun = new MyRunnable1();
Thread t1 = new Thread(myrun,"窗口1");
Thread t2 = new Thread(myrun,"窗口2");
Thread t3 = new Thread(myrun,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
class MyRunnable1 implements Runnable{
//static int ticket = 10;//情形3:静态变量由多个对象共享,适合Thread子类创建线程对象
int ticket = 10;//情形2、4:当只创建一个对象放在堆中,是多个线程共享的属性
@Override
public void run(){
//放在方法内是局部变量,在栈中,不是多线程之间共享的区域!如果创建三个线程,那么就是每个线程都有一个独立的100
//int ticket = 100;//情形1:局部变量不能共享
while(true){
try {//被阻塞。
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);//ticket.ticket是情形5
ticket--;
}else{
break;//跳出循环
}
}
}
}
①抽取资源类,在主线程中定义内部类(继承Thread/实现Runnable接口)
//1、编写资源类
class Ticket {
private int ticket = 100;
public void sale() {
if (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
ticket--;
} else {
throw new RuntimeException("没有票了");
}
}
public int getTicket() {
return ticket;
}
}
//在主线程中定义Thread内部类:因为资源已经被封装,无需写重复代码!
public class SaleTicketDemo5 {
public static void main(String[] args) {
//2、创建资源对象
Ticket ticket = new Ticket();
//3、启动多个线程操作资源类的对象
Thread t1 = new Thread("窗口一") {
public void run() {
while (true) {
ticket.sale();
}
}
};
Thread t2 = new Thread("窗口二") {
public void run() {
while (true) {
ticket.sale();
}
}
};
Thread t3 = new Thread(new Runnable() {
public void run() {
ticket.sale();
}
}, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
4.2 解决方法:同步机制(同步代码块/同步方法)sychronized
补充:
- Java对象在堆中的数据部分:对象头、实例变量、空白的填充,其中对象头包含:Mark Word(和当前对象有关的GC、锁标记等信息)、指向类的指针(记录由哪个类创建的)、数组长度(只有数组才有)
- 创建对象的过程:①分配堆内存空间②内存空间初始化③指向堆内存中对象的首地址。在不影响执行顺序的情况下是可以进行指令重排的比如①③②,这也会导致一系列问题,可以用volite(禁止指令重排)、synchronized、lock解决
1、Java是如何解决线程安全的问题?同步机制的原理
相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”(同步锁)。哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的ID,这样其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新获得/占用”同步锁“对象。
为什么可以给对象加锁?
因为Java对象在堆中的对象头会记录和当前对象有关的锁信息。
重点关注两个事:共享数据及操作共享数据的代码;同步监视器(保证唯一性)
2、具体实现方式:共享的资源、同步操作的代码(操作共享资源),同步锁必须唯一可以是任意一个对象(也可以用共享资源对象)
- 同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。
- 需要被同步代码块:即为操作共享数据的代码。同一时刻只能有一个线程操作这些代码,其它线程必须等待。
- 同步锁,也称同步监视器,哪个线程获取哪个执行,多个线程必须共用一个,可以是任何一个类的对象,必须是唯一的才能锁住。
- 共享数据:即多个线程多需要操作的数据。比如:ticket
- 注意点:同步锁可以是:实现Runnable接口方法中,this;继承Thread类中,“类名.Class”
- 同步方法:如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。
3、何时释放锁?在当前线程的同步方法、同步代码块中
- 执行结束
- 遇到break、return终止了该代码块、该方法的继续执行。
- 出现了未处理的Error或Exception,导致当前线程异常结束。
- 执行了锁对象的wait()方法,当前线程被挂起,并释放锁。
4、不会释放锁的操作
- 调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
- 其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。
- 应尽量避免使用suspend()和resume()这样的过时来控制线程。
- 注意:锁肯定都是加在重写的run()方法中的,同步方法要在run()中调用
同步机制的两种实现方式:同步代码块、同步方法。
//法1:同步代码块
synchronized(同步锁){//这个同步锁必须是唯一的,可以是this当前类的对象
//需要同步操作的代码
}
//法2:同步方法:锁对象——非静态方法是当前类的对象,静态方法是一个类的Class对象(在内存中只有一个)
public synchronized void method(){
//可能会产生线程安全问题的代码
}
实现三个窗口同时售卖火车票
法1:同步代码块:①Runnable实现类,this;②使用Thread子类把同步锁改为"类名.class"再创建线程对象即可!
法2:同步方法:①Runnable实现类,定义非静态方法加锁;②使用Thread子类,定义为静态方法加锁!
public class OverrideTest {
public static void main(String[] args){
MyRunnable1 myrun = new MyRunnable1();
Thread t1 = new Thread(myrun,"窗口1");
Thread t2 = new Thread(myrun,"窗口2");
Thread t3 = new Thread(myrun,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
//法1:同步代码块
class MyRunnable1 implements Runnable{
static int ticket = 100;//当只创建一个对象放在堆中,是多个线程共享的属性
Object obj = new Object();
@Override
public void run(){
while(true){//直接锁这里,肯定不行,会导致,只有一个窗口卖票
synchronized (this){//同步监视器:唯一性。如果是Thread子类使用"类名.class"作为同步锁
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);
ticket--;
}else{
break;//跳出循环
}
}
}
}
}
//法2:同步方法
class MyRunnable1 implements Runnable{
static int ticket = 100;//当只创建一个对象放在堆中,是多个线程共享的属性
@Override
public void run(){
while(ticket>0){
saleOneTicket();
}
}
//锁对象是this,这里就是TicketSaleRunnable对象,因为上面3个线程使用同一个TicketSaleRunnable对象,所以可以
public synchronized void saleOneTicket(){
if(ticket>0){//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);
ticket--;
}
}
}
练习题:银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
【提示】
- 1,明确哪些代码是多线程运行代码,须写入run()方法
- 2,明确什么是共享数据。
- 3,明确多线程运行代码中哪些语句是操作共享数据的。
法1:继承Thread子类:采用同步代码块,抽取共享资源,封装共享操作代码
//继承Thread子类中的重写run()方法部分,创建线程对象方式Customer cus1 = new Customer(account,"甲");
class Customer extends Thread{
Private Account account;//使用一个实例创建Thread多个对象,唯一的共享资源
int sum = 3000;//创建三个线程,因此每个线程一份
public Customer(Account account, String name){
super(name);
this.account = account;
}
@Override
public void run(){
while(sum > 0){
synchronized (Customer.class){//这里是唯一的
account.deposit(1000);
sum -= 1000;
}
}
}
}
//封装共享资源和共享操作代码
class Account{
private double balance;//余额
public void deposit(double amt){
if(amt>0){
this.balance += amt;
System.out.println(Thread.currentThread().getName()+"存钱"+amt+",余额为:"+ balance);
}
}
}
法2继承Thread子类:采用同步方法
//重写的run方法部分
@Override
public void run(){
for(int i = 0; i < 3; i++){//放心地用,i是局部变量,每个线程私有
account.deposit(1000);
}
}
//修改同步方法
class Account{
private double balance;//余额
public synchronized void deposit(double amt){//这里是唯一的,默认为Account.class
if(amt>0){
this.balance += amt;
System.out.println(Thread.currentThread().getName()+"存钱"+amt+",余额为:"+ balance);
}
}
}
4.3 解决方法:同步机制Lock
1、定义:JDK5.0新特性,保证线程的安全,也称同步锁。通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口:控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。线程安全控制中常用ReentrantLock,可以显式加锁、释放锁。
- ReentrantLock类:实现了 Lock 接口,拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。还提供了在激烈争用情况下更佳的性能。
步骤1,创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为static final
步骤2.执行lock()方法,锁定对共享资源的调用
步骤3.unlock()的调用,释放对共享数据的锁定
Lock语法格式
注意:如果同步代码有异常,要将unlock()写入finally语句块。
class A{
//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例
private static final ReentrantLock lock = new ReenTrantLock();
public void m(){
//2. 调动lock(),实现需共享的代码的锁定
lock.lock();
try{
//保证线程安全的代码;
}
finally{//放在这里面确保它一定会执行!
//3. 调用unlock(),释放共享代码的锁定
lock.unlock();
}
}
}
2、Lock与synchronized对比
synchronized:隐式锁,出了作用域、遇到异常自动解锁。无论是同步代码块还是同步方法中,只有出了作用域才会释放锁。【调用监视器的wait()方法也可以释放,等notify()后在从这里执行】
Lock:显示锁,通过手动开启,手动关闭锁,释放对同步监视器的调用,更灵活。Lock有多种获取锁的方式(如从sleep线程中抢到锁),另外一个不行
Lock作为接口提供了多种实现类,适合更复杂的场景。JVM调用较少时间调度线程,性能更好,有很好的扩展性。
Lock可以对读锁不加锁,但是synchronized不行,
建议:Lock→同步代码块→同步方法
4.4 同步机制的应用(懒汉式单例模式-线程安全)
单例模式
饿汉式VS懒汉式:懒汉式可以理解为是饿汉式的一种延迟new对象的操作,其中懒汉式是线程不安全的,饿汉式是线程安全的!
懒汉式存在线程安全问题,需要同步机制来处理
枚举类的本质还是饿汉式,只不过饿汉式提供了方法调用,而枚举类是直接调用该对象。
法三:双重锁检验:为了避免指令重排需要将instance声明为volatile,再使用synchronized加同步锁。
//内部类方式
public class LazySingle {
private LazySingle(){}
public static LazySingle getInstance(){
return Inner.INSTANCE;
}
private static class Inner{
static final LazySingle INSTANCE = new LazySingle();
}
}
//枚举类方式:本质是饿汉式
public enum HungryOne{
INSTANCE;//这一句就相当于声明为class类的下面的两句
//public static final HungryOne INSTANCE = new HungryOne();
//private HungryOne(){};
}
//饿汉式
public class Singleton{
private Singleton(){}//私有化构造器
private static Singleton instance = new Singleton();//对象是否声明为final 都可以
public static Singleton getInstance(){
return instance;
}
}
//懒汉式
public class Singleton{
private Singleton(){}//私有化构造器
private static Singleton instance = null;//new延迟
public static Singleton getInstance(){
if(instance == null){
return new Singleton();//延迟到这里就可能出现线程安全问题
}
return instance;
}
}
//法一:同步方法:解决懒汉式线程安全问题
public class Singleton{
private Singleton(){}//私有化构造器
private static Singleton instance = null;//new延迟
public static synchronized Singleton getInstance(){//同步锁是唯一的:Singleton.class
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
//法二:同步代码块:解决懒汉式线程安全问题、及其优化点
public class Singleton{
private Singleton(){}//私有化构造器
private static Singleton instance = null;//new延迟
public static Singleton getInstance(){//同步锁是唯一的:Singleton.class
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
return instance;//可以放在同步代码块外也行
}
}
}
//法三:同步代码块的优化点:由于指令重排的问题会导致在new对象的过程中,线程1创建了对象但还没初始化完成,线程2判断发现不为null,直接出去了,就有问题!解决办法:volatile关键字
public class Singleton{
private Singleton(){}//私有化构造器
private static volatile Singleton instance = null;//new延迟,其中volatile 避免指令重排
public static Singleton getInstance(){//同步锁是唯一的:Singleton.class
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
4.5 同步机制引发的问题:死锁
1、死锁定义:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
发生在多线程并发时,对对方共享资源的争夺
2、死锁原因及解决方案
秀发死锁的原因 | 解决死锁 |
---|---|
占用互斥条件 | 无法被破坏,因为线程需要通过互斥解决安全问题。 |
占用且等待 | 一次性申请所有所需资源,就不存在等待的问题 |
不可抢夺(不可抢占) | 占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。 |
循环等待 | 将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。 |
五、线程的通信
5.1 线程通信机制
1、线程间通信的理解:多线程共同完成同一个任务,那么多线程之间需要一些通信机制,协调它们的工作,以此实现多线程共同操作一份数据。
2、实现方式:声明在0bject类中的方法。notify()适用于交替工作
- wait():线程进入等待状态。同时,会释放对同步监视器的调用。线程状态进入 WAITING 或 TIMED_WAITING
- notify():唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
注意:以上是配合synchronized使用,而Lock需要配合Condition实现线程间的通信
- wait方法与notify方法必须要由
同一个锁对象(同步监视器)调用
。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。 - wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在
同步代码块
或者是同步函数
中使用。因为:必须要通过锁对象
调用这2个方法。否则会报java.lang.IllegalMonitorStateException异常。
被通知的线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
5.2 场景题:交替打印&生产者消费者
问题1:两个线程交替打印1-10
public class PrintNum {
public static void main(String[] args){
PrintNumObj pri = new PrintNumObj();
Thread th1 = new Thread(pri,"线程1");
Thread th2 = new Thread(pri,"线程2");
th1.start();
th2.start();
}
}
class PrintNumObj implements Runnable{
int i = 1;
@Override
public void run() {
while(true){
synchronized (this){
//this可以省略,由同步监视器调用
this.notify();//唤醒被wait()的线程中优先级最高的那一个线程
if(i<=10){
System.out.println(Thread.currentThread().getName()+"打印:"+i);
i++;
}else{
break;
}
try {
//由同步监视器调用
this.wait();//线程进入等待状态
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
问题2:生产者和消费者问题:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。【店员可以理解为操作共享数据】
分析:多线程、共享数据、有线程安全问题【同步机制】、线程通信
- 解决的问题:
线程安全问题:共享的数据缓冲区,产生的安全问题→同步解决
线程的协调工作问题:生产者缓冲区满时wait(),等到下次消费者消耗缓冲区数据时notify()正在等待的线程恢复到就绪状态,开始向缓冲区添加数据。同理,消费者在缓冲区空时wait(),进入阻塞状态,等到生产者添加数据时notify()正在等待的消费者恢复到就绪状态。- 原理:生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
生产者消费者:采用Thread子类,
但是如果采用Runnable的话如何区分不同的线程对象从而进行不同的消费/生产操作?
public class PrintNum {
public static void main(String[] args){
Clerk clerk = new Clerk();
Thread th1 = new Customer(clerk);
Thread th2 = new Producer(clerk);
th1.start();
th2.start();
}
}
//封装共享资源对象,及对共享资源操作的业务代码
class Clerk{
int productNum=0;
public synchronized void addProductNum(){
if(productNum >= 20){//共享内存区域满了
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
this.notifyAll();
productNum++;
System.out.println(Thread.currentThread().getName()+"生产了了第"+productNum+"个产品");
}
}
public synchronized void minusProductNum(){
if(productNum <= 0){//共享内存区域空了
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
this.notifyAll();
System.out.println(Thread.currentThread().getName()+"消费了第"+productNum+"个产品");
productNum--;
}
}
}
//消费者线程类
class Customer extends Thread{
Clerk clerk;//体现共享区域
public Customer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run(){
for (int i = 0; i < 20; i++) {//这里加个限制,就执行二十次,免得太多了
clerk.minusProductNum();
}
}
}
//生产者线程类
class Producer extends Thread{
Clerk clerk;//体现共享区域
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run(){
for (int i = 0; i < 20; i++) {//这里加个限制,就执行二十次,免得太多了
clerk.addProductNum();
}
}
}
六、企业真题
1、什么是程序、进程、线程?
- 程序(program):为完成特定任务,用某种语言编写的
一组指令的集合
。即指一段静态的代码。 - 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。程序是静态的,进程是动态的。进程作为
操作系统调度和分配资源的最小单位
。 - 线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。线程作为
CPU调度和执行的最小单位
2、多线程使用场景
- 手机app应用的图片的下载
- 迅雷的下载
- Tomcat服务器上web应用,多个客户端发起请求,Tomcat针对多个请求开辟多个线程处理
3、如何在Java中出实现多线程?
Thread类、Runnable接口都是重写run()方法
Callable接口是实现call()方法
线程池创建,关于线程池的优点
- 提高了程序执行的效率。(因为线程已经提前创建好了)
- 提高了资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
- 可以设置相关的参数,对线程池中的线程的使用进行管理
4、Thread类中的start()和run()有什么区别?
start():① 开启线程 ② 调用线程的run()
5、Java中Runnable和Callable有什么不同?
Callable对于Runnable方式的好处
- call():与run()方法对比可以有返回值,更灵活
- call():可以使用throws的方式处理异常(抛出)
- 支持泛型的返回值:Callable使用了泛型参数,可以指明具体的call()的返回值类型,更灵活。(需要借助FutureTask类,获取返回结果)
缺点:如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态的。
6、sleep() 和 yield()区别?(神*泰岳)
sleep():一旦调用,就进入“阻塞”(或TIMED_WAITING状态)
yield():释放cpu的执行权,处在RUNNABLE的状态
7、线程的生命周期?线程的基本状态以及状态之间的关系?
7、stop()和suspend()方法为何不推荐使用?
stop():一旦执行,线程就结束了,导致run()有未执行结束的代码。stop()会导致释放同步监视器,导致线程安全问题。
suspend():与resume()搭配使用,导致死锁。
8、Java 线程优先级是怎么定义的?
三个常量。[1,10]
9、你如何理解线程安全的?线程安全问题是如何造成的?
多线程并发在访问这些共享的资源时就会出现问题。
10、多线程共用一个数据变量需要注意什么?
线程安全问题
11、多线程保证线程安全一般有几种方式?
- 同步机制:synchronized修饰同步方法、同步代码块
- Lock接口
两种对比:synchronized和ReentrantLock有什么不同
synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用。
Lock是通过两个方法控制需要被同步的代码,更灵活一些。
Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。
类似问题:
> 如何解决其线程安全问题,并且说明为什么这样子去解决?(北京联合**)
> 请说出你所知道的线程同步的方法。(天*伟业)
> 哪些方法实现线程安全?(阿*)
> 同步有几种实现方法,都是什么? (锐*企业管理咨询)
> 你在实际编码过程中如何避免线程安全问题?(*软国际)
> 如何让线程同步?(*手)
> 多线程下有什么同步措施(阿*校招)
> 同步有几种实现方法,都是什么?(海*科)
12、synchronized加在静态方法和普通方法区别
同步监视器不同。静态:当前类本身 非静态:this
13、当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?
需要看其他方法是否使用synchronized修饰,同步监视器的this是否是同一个。
只有当使用了synchronized,且this是同一个的情况下,就不能访问了。
14、线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?
同步一定阻塞;阻塞不一定同步。
15、什么是死锁,产生死锁的原因及必要条件
(1)如何看待死锁?
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
我们编写程序时,要避免出现死锁。
(2)诱发死锁的原因?
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待
以上4个条件,同时出现就会触发死锁。
(3)如何避免死锁?
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
16、Java中notify()和notifyAll()有什么区别
notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同,则
随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
17、为什么wait()和notify()方法要在同步块中调用
因为调用者必须是同步监视器。
18、多线程:生产者,消费者代码(同步、wait、notifly编程)
思路:封装共享资源clerk:产品数量productNum、对产品的add、minus
创建线程生产者、消费者,其中条件分别有共享空间满/为空时需wait(),执行后还需要notify()
19、wait()和sleep()有什么区别?调用这两个函数后,线程状态分别作何改变?
相同点:一旦执行,当前线程都会进入阻塞状态
不同点:
- 声明的位置:wait():声明在Object类中sleep():声明在Thread类中,静态的
- 使用的场景不同:wait():只能使用在同步代码块或同步方法中sleep():可以在任何需要使用的场景
- 使用在同步代码块或同步方法中:wait():一旦执行,会释放同步监视器sleep():一旦执行,不会释放同步监视器
- 结束阻塞的方式:wait(): 到达指定时间自动结束阻塞 或 通过被notify唤醒,结束阻塞sleep(): 到达指定时间自动结束阻塞
20、手写一个单例模式(Singleton),还要安全的.&手写一个懒汉式的单例模式&解决其线程安全问题,并且说明为什么这样子去解决
饿汉式;安全的懒汉式;内部类;