一. 多线程的基本概念
我们之前学习的程序在没有跳转语句的情况下,都是由上至下沿着一条路径依 次执行。现在想要设计一个程序,可以同时有多条执行路径同时执行。比如, 一边游戏,一边 qq 聊天,一边听歌,怎么设计?要解决上述问题,需要使用多进程或者多线程来解决。
1.1 程序 进程 和线程
程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。一个进程可以启动多个线程,线程是进程中的一个执行场景。JVM是一个进程,通过这个JVM进程可以启动多个java线程。
线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。线程必然不是越多越好,线程切换也是要开销的,当你增加一个线程的时候,增加的额外开销要小于该线程能够消除的阻塞时间,这才叫物有所值。Linux自从2.6内核开始,就会把不同的线程交给不同的核心去处理。Windows也从NT.4.0开始支持这一特性。
进程与线程的详细区分:
进程是操作系统调度和分配资源的最小单位(亦是系统运行程序的基 本单位),系统在运行时会为每个进程分配不同的内存区域,进程和进程之间的内存独立,进程之间的数据交换和通信的成本很高。线程是 CPU 调度和执行的最小单位,线程和线程之间栈内存独立,一个线程一个栈,但是线程之间堆内存和方法区内存是共享的,它们从同一个堆中分配对 象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
1.2 概述java 程序的执行原理:
java 命令执行会启动 JVM,JVM 的启动表示启动一个应用程序,即启动了一个进程。 该进程会自动启动一个“主线程”,然后主线程负责调用某个类的main方法。所以main方法的执行是在主线程中执行的。然后通过main方法代码的执行可以启动其他的“分支线程”。 所以,main 方法结束程序不一定结束,因为其他的分支线程有可能还在执行。
1.3 并行、并发、多线程、多进程
并行(parallel):指两个或多个线程在同一时刻发生(同时发生)。比如:多个人同时做不同的事。强调的是同时执行多个任务
并发(concurrency):指两个或多个事件在同一个时间段内发生。即在一段时间内,有多条指令/线程在单个 CPU 核心上快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。强调的是在时间上交替执行多个任务。并发就是宏观上是一起进行着的,微观上是交替的,并行则在微观上也是严格重叠的,还有一种说法是并行包含了并发。并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度地并发运行。并且,在进行并发编程时,如果希望通过多线程执行任务让程序运行的更快,将会面临非常多的挑战,比如线程的上下文切换问题、死锁等。
多进程是实现并行的一个有效手段。它可以充分发挥多个cpu核心数的作用,将多个任务分配到不同的cpu核心上,从而实现同一时刻,处理多个任务。它很适合计算密集型任务。当然多进程也可以实现并发,但由于进程之间只共享文件表,没有共享用户地址空间。进程之间的调度开销比较大,而且进程通信必须采用显式的IPC机制。常见的就是管道。因此多进程并发耗资源,耗时,通信不方便,较少采用。
为什么出现多线程技术?因为IO操作不需要占用CPU,CPU空闲下来的时间,为啥我们不好好利用起来呢?所有多线程孕育而生!多线程是一种并发程序设计的技术。 通过在单个进程中创建多个线程,这些线程可以并发的执行不同的任务。多线程的用处在于,做某个耗时的操作时,需要等待返回结果,这时用多线程可以提高程序并发程度,让其他不耗时的任务先完成。(io耗时远大于线程的“上下文切换”)如果一个不需要任何等待并且顺序执行能够完成的任务,用多线程简直是浪费。
1.4 单核和多核CPU
单核 CPU在一个时间单元内只能执行一个线程的任务。例如,可以把 CPU 看成是医院,其核心数看作医院的医生数量,则单核CPU可形象的表示在一定时间内该医院只能给一个病人诊断(核心有限)。这时候想要提升系统性能,只有两个办法,要么提升该核心的性能(让医生看病快点,惨绝人寰),要么多加几个核心数量(多整几个医生),即为多核的 CPU。显然后者更合适。
多核儿就是系统同时可以运行多个线程,多核的效率是单核的倍数吗?譬如 4 核 A53 的 cpu,性能是单核 A53 的 4 倍吗?理论上是,但是实际不可能。譬如,4 核 CPU 对应的内存、cache、寄存 器并没有同步扩充 4 倍。这就好像医院一样,1 个医生换 4 个医生,但是做 B 超检查 的还是一台机器,性能瓶颈就从医生转到 B 超检查了。另一个是多核 CPU 之间的协调管理损耗。譬如多个核心同时运行两个相关的任务, 需要考虑任务同步,这也需要消耗额外性能。好比公司工作,一个人的时候至少不用开会浪费时间,自己跟自己商量就行了。两个人就要开会同步工作,协调分配,所以工作效率绝对不可能达到 2 倍。
不管是单核还是多核,都可以实现多线程机制。在单cpu的情况下多线程表现为并发,而多核心的情况除了并发外,至少还说明可能拥有并行的能力,具体有无情况视当时的实际状态由操作系统的调度决定。
1.5 多线程在单核和多核CPU上的执行效率问题的讨论
在多任务环境下,多核CPU实现多线程的性能通常比单核CPU实现多线程更高。多核CPU可以同时并发执行多个线程,而且可以并行执行不同的任务,从而提高了系统的处理能力和响应速度。而单核CPU在多线程中,由于只有一个物理核心,只能通过轮换方式来分配处理器资源给不同的线程,线程间的调度和上下文切换也需要一定的时间和开销,这会带来一定的性能损失。
但是,在某些情况下,单核CPU实现多线程可能会比多核CPU实现多线程更有优势。例如,在涉及到复杂io访问或计算的单任务环境下,单核CPU实现多线程可以更好地控制程序的执行流程和资源的占用,避免多核CPU上线程间的竞争和复杂的调度问题。此外,在一些特定的应用领域,如嵌入式系统和实时操作系统等,单核CPU实现多线程也可以满足系统的性能和响应需求。因此,在具体的应用场景下,需要根据实际的需求和性能要求来选择使用单核CPU还是多核CPU实现多线程。
1.6 守护线程
有一种线程,它是在后台运行的,它的任务是为其他线程提供服务的,这种线 程被称为“守护线程”。JVM 的垃圾回收线程就是典型的守护线程。
守护线程有个特点,就是如果所有非守护线程都死亡,那么守护线程自动死亡。形象理解:兔死狗烹,鸟尽弓藏
调用 setDaemon(true)方法可将指定线程设置为守护线程。必须在线程启动之前设置,否则会报 IllegalThreadStateException 异常。 调用 isDaemon()可以判断线程是否是守护线程。设置为守护线程后,当主线程结束后,守护线程并没有把所有的数据输出完就结束了
1.7 协程及其和线程的区别
我们知道操作系统在线程等待IO的时候,会阻塞当前线程,切换到其它线程,这样在当前线程等待IO的过程中,其它线程可以继续执行。当系统线程较少的时候没有什么问题,但是当线程数量非常多的时候,却产生了问题。一是系统线程会占用非常多的内存空间,二是过多的线程切换会占用大量的系统时间。
协程刚好可以解决上述2个问题(后面线程池也一定程度上解决了上述问题)。协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。
在遇到同时处理10000个读取数据库问题,我们只需要启动100个线程,每个线程上运行100个协程,这样不仅减少了线程切换开销,而且优雅的解决了上述任务。
协程对计算密集型的任务也没有太大的好处,计算密集型的任务本身不需要大量的线程切换。协程只有在等待IO的过程中才能重复利用线程,上面我们已经讲过了,线程在等待IO的过程中会陷入阻塞状态,意识到问题没有?假设协程运行在线程之上,并且协程调用了一个阻塞IO操作,这时候会发生什么?实际上操作系统并不知道协程的存在,它只知道线程,因此在协程调用阻塞IO操作的时候,操作系统会让线程进入阻塞状态,当前的协程和其它绑定在该线程之上的协程都会陷入阻塞而得不到调度,这往往是不能接受的。因此在协程中不能调用导致线程阻塞的操作。也就是说,协程只有和异步IO结合起来,才能发挥最大的威力。
线程和协程是两种不同的并发编程实现方式,它们之间的主要区别在于以下几个方面:
- 调度方式:线程由操作系统进行直接或间件调度,而协程由程序员在代码中显式地切换执行。
- 开销:在线程的上下文切换过程中,需要保存和恢复线程的各种状态,开销比较大。而协程的上下文切换仅仅是保存和恢复栈的内容,开销很小。
- 资源占用:线程需要占用操作系统的资源,包括线程栈、状态等,生成线程的开销比较大。而协程没有自己的状态和栈,使用的是主程序的栈,占用的资源比较小。
- 实现方式:线程的实现需要依赖于操作系统提供的原语,而协程的实现可以使用语言本身提供的语法和相关库。
- 同步机制:Java中线程是抢占式的,需要使用锁等同步机制来防止多线程同时访问共享数据。而协程是异步的,可以在不同的协程之间进行切换,不需要使用锁等同步机制。
- 状态保留:线程没有保存上一次调用时的状态,每次过程重入时相当于重新开始执行。而协程可以保留上一次调用时的状态,每次过程重入时可以继续上一次的执行。
二.线程的创建与启动
Java 语言的 使用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。每个线程都是通过某个特定 Thread 对象的 run()方法来完成操作的,因此把 run()方法体称为线程执行体。通过该 Thread 对象的 start()方法来启动这个线程,而非直接调用 run()。要想实现多线程,必须在主线程中创建新的线程对象。一个线程对象只能调用一次 start()方法启动,如果重复调用了,则将抛出 以上的异常“IllegalThreadStateException”。毋庸置疑,run()方法和start()方法是多线程中最基础和最重要的方法之一。
2.1 通过继承 Thread 类来创建并启动多线程
2.1.1 步骤
1.定义 Thread 类的子类,并重写该类的 run()方法,该 run()方法的方法体就代表了线程需要完成的任务
2. 创建 Thread 子类的实例,即创建了线程对象
3. 调用线程对象的 start()方法来启动该线程
来个小对比:start()方法的执行是在JVM中开辟新的栈空间,会就绪一个新的线程。一旦jvm线程调度器决定该线程应该运行,它就会开始运行,并在调用
run()
方法的过程中执行线程的代码。由此可见run()方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的 CPU 调度决定。如果自己手动调用 run()方法,那么就只是普通方法,没有启动多线程模式,而是在当前线程运行。
2.1.2 代码实现
建议手敲一遍下面代码,熟悉一下,别没有idea了Thread都不会写好吗!
主线程和子线程的执行顺序是不确定的,并且可能会交错执行。这是因为线程的调度是由操作系统来控制的,而操作系统会根据线程的优先级、CPU的负载情况等因素来决定线程的执行顺序。通过下面代码验证一下吧
//自定义线程类
public class MyThread extends Thread{
//定义指定线程名称的构造方法
public MyThread(String name){
//调用父类的 String 参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程的执行逻辑
*/
@Override
public void run(){
for (int i = 0; i <10 ; i++) {
System.out.println(getName()+"正在执行!"+i);
}
}
}
public class Main {
public static void main(String[] args) {
//必须传一个字符串,因为自定义类中没有显示的定义空参
MyThread mt1=new MyThread("子线程1");
//开启线程
mt1.start();
MyThread mt2=new MyThread("子线程2");
mt2.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程"+i);
}
}
为什么以上程序每次都是主线程执行完才执行子线程呢?子线程倒是在交替执行的。文心一言给的解释是:在您的代码片段中,可能存在一种特殊情况,即主线程执行完循环后,子线程才开始执行。这是因为主线程在启动子线程后立即开始执行循环,而子线程需要一定的时间来创建和准备执行。如果主线程的循环执行速度非常快,那么子线程可能来不及开始执行,主线程就已经完成了循环。
可能会有疑问,主线程的优先级是不是默认比子线程高?
在Java中,线程的优先级是由Thread类的优先级属性决定的,它是一个整数值,范围从1到10。默认情况下,所有线程的优先级都是5。线程的优先级只是影响线程调度的一个因素,并不保证线程的执行顺序。实际上,线程调度是由操作系统来控制的,而操作系统的线程调度算法不仅考虑线程的优先级,还会考虑CPU的负载情况、线程的状态等因素。
2.2 通过实现runnable接口来创建并启动多线程
Java 有单继承的限制,当我们无法继承 Thread 类时,那么该如何做呢?在核心类库中提供了 Runnable 接口,我们可以实现 Runnable 接口,重写 run()方法, 然后再通过 Thread 类的对象代理启动和执行我们的线程体 run()方法。下面直接用代码来展示步骤:
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 TestMyRunnable {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//mr.start()是错误的,任务对象没有start方法。
/**创建线程对象
public Thread(Runnable target,String name) 构造器, Runnable target:这是一个实现了
Runnable 接口的对象。 当你创建一个新的线程时,你需要提供一个 Runnable 对象,这个对象定义了新线程要执行的任务。String name:这是新线程的名字。
*/
Thread t = new Thread(mr, "长江");
t.start();
//让主线程睡一会儿,时间我是精心试了的,小于五毫秒黄河先执行,
//大于10毫秒,黄河后执行,猜测创建线程耗时在5-10毫秒间
try {
Thread.sleep(8);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 20; i++) {
System.out.println("黄河 " + i);
}
}
}
Thread 类实际上也是实现了 Runnable 接口的类。尽管 Thread
类实现了 Runnable
接口,但我们通常不会将 Thread
对象作为任务传递给其他线程来执行。相反,我们会创建一个实现了 Runnable
接口的自定义类,并将其实例化对象作为任务对象传递给其他线程来执行。这样可以更好地将任务逻辑与线程管理逻辑分离,提高代码的可读性和可维护性。以上代码就是证明!
下面我们用匿名内部类改造一下上述代码:
因为考虑到子类或实现类是一次性的,那么我们“费尽心机”的给它取名字,就显得多余。我们完全可以使用匿名内部类的方式来实现,避免给类命名的问题。使用匿名内部类的对象直接调用方法:
interface A{ void a(); } public class Test{ public static void main(String[] args){ new A(){ @Override public void a() { System.out.println("aaaa"); } }.a(); } }
//通过父类或父接口的变量多态引用匿名内部类的对象 interface A{ void a(); } public class Test{ public static void main(String[] args){ A obj = new A(){ @Override public void a() { System.out.println("aaaa"); } }; obj.a(); } }
//匿名内部类的对象作为实参 interface A{ void method(); } public class Test{ public static void test(A a){ a.method(); } public static void main(String[] args){ test(new A(){ @Override public void method() { System.out.println("aaaa"); } } ); } }
//以下代码用匿名内部类创建了两个新的线程并启动它们。每个线程都打印出0到9之间的数字
public class Main {
public static void main(String[] args) {
new Thread("新的线程!"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":正在执行!"+i);
}
}
}.start();
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" + i);
}
}
}).start();
}
}
/**
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。通常用于主线程和 Runnable 实现类
*/
2.3 JDK5.0 新增线程创建方式——实现 Callable 接口
在Java中,
Runnable
接口的run()
方法,没有返回值(即void
类型)并且不接受任何参数。与使用 Runnable 相比, Callable 功能更强大些 ,相比 run()方法,对应的call()方法可以有返回值 ,可以抛出异常(run方法所在自定义线程类作为子类不能比父类抛出更多的异常,而父类未抛出异常,故子类无法抛出异常,出现异常只能try-catch解决) , 支持泛型的返回值(需要借助 FutureTask 类,获取返回结果。缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。
// 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 futureTask = new FutureTask(numThread);
// 5. 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()
//创建并启动一个新的线程,该线程将异步执行NumThread类的call()方法,并允许主线程在将来的某个时间点获取该方法的结果
new Thread(futureTask).start();
// 接收返回值
try {
// 6. 获取 Callable 中 call 方法的返回值
// get() 返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call() 的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.4 JDK5.0 新增线程创建方式——使用线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束 了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。 那么有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池 中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
如何得到线程池对象呢?
2.4.1. 使用ExecutorService接口的的实现类ThreadPoolExecutor来创建线程池对象
以上参数要记吗?参数顺序要记吗?来个小技巧
new ThreadPoolExecutor(); 然后按ctrl加单击,进入进源码看顺序
public class ThreadPool { public static void main(String[] args) throws ExecutionException, InterruptedException { /**int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,一个接口,不能直接new对象 可以用匿名累不类,或者推荐的Executors.defaultThreadFactory() ThreadFactory threadFactory, RejectedExecutionHandler handler) AbortPolicy是RejectedExecutionHandler接口的实现类 */ ExecutorService p=new ThreadPoolExecutor(3,5,8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()); //线程池对象的使用,处理Runnable任务,使用execute(Runnable command) Runnable target=new MyRunnable(); p.execute(target);//自动创建新线程, 自动执行Runnable任务 p.execute(target); p.execute(target); p.execute(target); //线程池默认一直开启 //p.shutdown();任务执行完毕后再执行 //p.shutdownNow()立即执 行 //处理Callable任务,使用Future<?> submit(Runnable task) Callable target1=new MyCallable(); Future future = p.submit(target1);//返回未来任务对象,调用其get方法获取线程返回结果 System.out.println(future.get()); } }
任务队列的类型:除了基于数组的ArrayBlockingQueue,还有基于链表实现的LinkedBlockingDeque,前者需要指定任务队列的数量。
什么时候创建临时线程:当核心线程没有空闲,任务队列也满了,再来新的任务,则会创建临时线程。 核心线程配置:计算密集型任务,核心线程数=cpu核心数+1; io密集型任务,线程数=cpu核心1数*2 。
拒绝策略什么时候执行:核心线程、临时线程都在忙,任务队列也满了,又来新任务
2.4.2 使用Executors(线程池工具类)调用静态方法返回不同特点的线程池对象
根据阿里巴巴开发手册,以上方式不建议使用:
并且该方式的底层仍然是方式1,灵活性却不够,不利于对程序的掌控
三.线程的调度与控制
线程调度是指系统为线程分配CPU执行时间片的策略方式,主要调度方式有:协同式线程调度、分时调度和抢占式线程调度。
协同式线程调度:某一线程执行完了之后,会主动通知系统切换到另外一个线程上执行(可以想象下类似排队一样的场景)。
分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式线程调度:优先级高的线程获取 CPU 的时间片相对多一些,如果线程的优先级相同, 那么会随机选择一个
在Java中,虽然底层操作系统的调度机制会影响到JVM线程的调度,但Java线程的创建、启动、暂停、恢复和结束等操作都是由JVM管理的。JVM通过线程调度器来协调和管理不同线程的执行,包括分配CPU时间片给各个线程,并调度它们在处理器上执行,使得多个线程能够并发执行。JVM的线程调度模式采用了抢占式模式,即根据线程的优先级别来获取CPU的使用权。
3.1 线程优先级
Java 线程优先级使用 1 ~ 10 的整数表示:
最低优先级 1:
Thread.MIN_PRIORITY
最高优先级 10:
Thread.MAX_PRIORITY
普通优先级 5:
Thread.NORM_PRIORITY
class ThreadTest{
public static void main(String[] args) {
Runnable r1 = new Processor();
Thread t1 = new Thread(r1, "t1");
//设置线程的优先级,线程启动后不能再次设置优先级
//必须在启动前设置优先级
//设置最高优先级
t1.setPriority(Thread.MAX_PRIORITY);
//启动线程
t1.start();
//取得线程名称
//System.out.println(t1.getName());
//获取线程的优先级
System.out.println(Thread.currentThread().getPriority());
Thread t2 = new Thread(r1, "t2");
//设置最低优先级
t2.setPriority(Thread.MIN_PRIORITY);
t2.start();
System.out.println(Thread.currentThread().getName());
}
}
class Processor implements Runnable {
public void run() {
for (int i=0; i<100; i++) {
System.out.println(Thread.currentThread().getName() + "," + i);
}
}
}
3.2 Thread.sleep
一个线程遇到 sleep(n) 的时候,就会睡眠,进入到阻塞状态, 放弃 CPU,腾出 cpu 时间片,给其他线程用,但是不会释放资源锁。A线程中出现此代码,A线程休眠,B线程中出现此代码,B线程休眠。当睡眠时间到达了,线程会进入可运行状态,然而它并不一定会立即获得CPU的执行权。线程调度器会根据调度算法来决定哪个线程应该获得CPU的执行权。因此,即使一个线程已经处于可运行状态,它仍然需要等待调度器的调度才能再次执行。如果线程在睡眠状态被中断了,将会抛出IterruptedException。
很多人一看到sleep(0)就认为线程睡眠0秒那不是毫无意义吗?其实sleep(0)并不是阻塞0秒的意思,Thread.sleep(0)表示当前的线程暂时放弃CPU的执行时间让给其他的线程或进程使用CPU资源,而自身线程马上进入就绪状态而不是阻塞等待状态。这样既可以使得系统可以做到适当切换执行的线程,也不影响当前线程的竞争从而可以提示系统执行的效率。常用于在轮询或者忙等待中。Thread.Sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争”。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。
sleep配循环,特定时间做特定的事情,有点儿定时器的味儿了。
来到面试题:
t线程不睡,主线程睡!
3.3 其他方法
Thread.yield:让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调度器又将其调度出来重新执行。感觉和sleep(0)有点类似不。
t.join():当前线程可以调用另一个线程的 join 方法(t就是这里的另一个线程的实例对象),调用后当前线程会被阻塞不再执行,直到被调用的线程执行完毕,当前线程才会执行
public class ThreadTest { public static void main(String[] args) { Runnable r1 = new Processor(); Thread t1 = new Thread(r1, "t1"); t1.start(); try { t1.join(); }catch(InterruptedException e) { e.printStackTrace(); } System.out.println("------main end-------"); } } class Processor implements Runnable { public void run() { for (int i=0; i<10; i++) { System.out.println(Thread.currentThread().getName() + "," + i); } } }
t.interrupt():如果我们的线程正在睡眠,可以采用 interrupt 进行中断,不是中断线程的执行,是中断它的睡眠!开发中可能不知道要让某个线程睡眠多久,取决于当前工作进度,当需要它休眠中断时,调用t.interrupt()中断该线程睡眠,在A线程中调用t线程实例对象中断t线程的睡眠,原理是让t线程的sleep报错,然后捕捉到后继续运行。
isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
getName() :获取当前线程名称。
setName(String name):设置该线程名称。
currentThread() :返回对当前正在执行的线程对象的引用。通常用于主线程和 Runnable 实现类(run方法中),为静态方法。怎么理解当前正在执行的线程呢?哪个线程调用了start方法,那currentThread() 方法返回的对象就表示哪个线程。
如何正确的关闭线程呢?不要用t.stop()方法,直接停止会导致操作未完成,数据丢失等情况发生,正确的操作是:
if中是执行逻辑,else中是关闭资源等操作
四. 线程的生命周期
CPU 需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行 (Running)、阻塞(Blocked)、死亡(Dead)
新版本:注意,cpu时间片不是锁
程序只能对新建状态的线程调用 start(),并且只能调用一次,如果对 非新建状态的线程,如已启动的线程或已死亡的线程调用 start()都会 报错 IllegalThreadStateException 异常。
新建:此时它和其他 Java 对象一样,仅仅由 JVM 为其分配了内存,并初始化了实例变量的值。此时的线程对象并没有任何线程的动态特征,程序也不会执行它的线程体 run()。
运行:JVM 会为其创建方法调用栈和程序计数器,当然,处于这个状态中的线程并没有开始运行,只是表示已具备了运行的条件,随时可以被调度。至 于什么时候被调度,取决于 JVM 里线程调度器的调度。
阻塞:当在运行过程中的线程遇到如下情况时,会让出 CPU 并临时中止自己的执行,进入阻塞状态: • 线程调用了 sleep()方法,主动放弃所占用的 CPU 资源
• 线程试图获取一个锁,但该锁正被其他线程持有;
• 线程执行过程中,锁调用了 wait(),让它等待某个通知(notify)
• 线程执行过程中,锁调用了 wait(time)
• 线程执行过程中,遇到了其他线程对象的加塞(join)
• 线程被调用 suspend 方法被挂起(已过时,因为容易发生死锁)
死亡:线程会以以下三种方式之一结束,结束后的线程就处于死亡状态:
• run()方法执行完成,线程正常结束
• 线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)
• 直接调用该线程的 stop()来结束该线程(已过时)
WAITING(无限等待),阻塞的一种:
通过 Object 类的 wait 进入 WAITING 状态的要有 Object 的 notify/notifyAll 唤醒;
通过 Condition 的 await 进入 WAITING 状态的要有 Condition 的 signal 方法唤醒;
通过 LockSupport 类的 park —— LockSupport 类的 unpark方法唤醒;
通过 Thread 类的 join ——只有调用 join 方法的线程对象结束才能当前线程恢复;
五. 线程的安全问题及解决
5.1线程安全问题举例
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条 记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
下面演示一个取钱案例:
需求:
小明和小红是一对夫妻,他们有一个共同的账户,余额是 10 万元,现在模拟 2 人同时去取钱 10 万,看是否会出现线程安全问题分析:
①:需要提供一个账户类,接着再测试类中创建一个账户对象代表2个人的共享账户。
②:需要定义一个线程类(用于创建两个取钱线程,分别代表小明和小红取钱)。
③:在测试类中创建2个线程,传入同一个账户对象给2个线程处理。
④:启动2个线程,同时去同一个账户对象中取钱10万。
//创建一个共享的账户类 public class Account { private String cardId; // 卡号 private double money; // 余额。 public Account() { } public Account(String cardId, double money) { this.cardId = cardId; this.money = money; } // 小明 小红同时过来的 public void drawMoney(double money) { // 先搞清楚是谁来取钱? String name = Thread.currentThread().getName(); // 1、判断余额是否足够 if(this.money >= money){ System.out.println(name + "来取钱" + money + "成功!"); this.money -= money; System.out.println(name + "来取钱后,余额剩余:" + this.money); }else { System.out.println(name + "来取钱:余额不足~"); } } public String getCardId() { return cardId; } public void setCardId(String cardId) { this.cardId = cardId; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } } //取钱的线程类 public class DrawThread extends Thread{ private Account acc; public DrawThread(Account acc, String name){ super(name); this.acc = acc; } @Override public void run() { // 取钱(小明,小红) acc.drawMoney(100000); } } //测试类 public class ThreadTest { public static void main(String[] args) { // 1、创建一个账户对象,代表两个人的共享账户。 Account acc = new Account("ICBC-110", 100000); // 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万。 new DrawThread(acc, "小明").start(); // 小明 new DrawThread(acc, "小红").start(); // 小红 } }
运行程序,你会发现两个人都取了10万块钱,余额为-10完了
在举个更高阶的买票的例子:
class TicketWindow extends Thread { //private int ticket = 100;如果ticket在此,卖出多少章票,为什么? /*发现卖出 300 张票。 不同的实例对象的实例变量是独立的。*/ private static int ticket = 100;//如果ticket在此,用static修饰,卖出多少章票,为什么? /*。 发现卖出近 100 张票。 但是有重复票问题。*/ public void run() { //int ticket = 100; 如果ticket在此,卖出多少章票,为什么? /*发现卖出 300 张票。 问题:局部变量是每次调用方法都是独立的,那么每个线程的 run()的 ticket 是 独立的,不是共享数据。*/ while (ticket > 0) { System.out.println(getName() + "卖出一张票,票号:" + ticket); ticket--; } } } public class Main { public static void main(String[] args) { TicketWindow w1 = new TicketWindow(); TicketWindow w2 = new TicketWindow(); TicketWindow w3 = new TicketWindow(); w1.setName("窗口 1"); w2.setName("窗口 2"); w3.setName("窗口 3"); w1.start(); w2.start(); w3.start(); } } //如果改继承Thread为实现runnable接口呢,ticket是非静态的成员变量,发现它不是卖300张,而是 //有重票,略大于100,为什么?因为任务对象被多个线程对象所共享!
5.2 线程同步解决方案
同步代码块解决:
synchronized(同步锁) {
访问共享资源的核心代码
}对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。在以上代码中,核心代码指的是:
public class Account { .......... public void drawMoney(double money) { // 先搞清楚是谁来取钱? String name = Thread.currentThread().getName(); // 1、判断余额是否足够 // this正好代表共享资源! synchronized (this) { if(this.money >= money){ System.out.println(name + "来取钱" + money + "成功!"); this.money -= money; System.out.println(name + "来取钱后,余额剩余:" + this.money); }else { System.out.println(name + "来取钱:余额不足~"); } } } ...............//想想如果取钱方法不在Account类中,在DrawThread中,该用什么做锁呢? }
同步代码块的同步锁对象有什么要求?“唯一”是关键,对于实例方法建议使用共享资源this作为锁对象。上述例子共享资源就是账户acc,当前对象(Account类的实例)this正好指代acc,对于静态方法建议使用字节码(类名.class)对象作为锁对象。锁对象随便选择,会影响其他无关线程执行。例如实例方法不用this做锁对象,用类名.class,则锁的范围太大,那小红取钱的过程中,不仅会锁小红的家人小明,还会锁住别人家正常取钱的账户。所以针对实例方法,用当前共享资源(即当前账户实例acc,保证其他非共享资源即其他账户实例acc1,acc2正常)作为锁很合理。对应静态方法,访问该方法的所有线程可以看作“一家人”,锁的范围要比this大
同步方法解决:
public synchronized void drawMoney(double money) { // 先搞清楚是谁来取钱? String name = Thread.currentThread().getName(); // 1、判断余额是否足够 if(this.money >= money){ System.out.println(name + "来取钱" + money + "成功!"); this.money -= money; System.out.println(name + "来取钱后,余额剩余:" + this.money); }else { System.out.println(name + "来取钱:余额不足~"); } }
java 同步方法也是有锁对象,只不过这个锁对象没有显示的写出来而已。 对于实例方法,锁对象其实是this(也就是方法的调用者) 对于静态方法,锁对象时类的字节码对象(类名.class)。范围上:同步代码块锁的范围更小,同步方法锁的范围更大。可读性:同步方法更好。
Lock锁解决:Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
//创建一个共享的账户类 public class Account { private String cardId; // 卡号 private double money; // 余额。 private final Lock lk = new ReentrantLock();//final表示禁止二次赋值,每个账户都要一个 //锁对象,故要作为成员变量。 public Account() { } public Account(String cardId, double money) { this.cardId = cardId; this.money = money; } // 小明 小红同时过来的 public void drawMoney(double money) { // 先搞清楚是谁来取钱? String name = Thread.currentThread().getName(); try { lk.lock(); // 加锁 // 1、判断余额是否足够 if(this.money >= money){ System.out.println(name + "来取钱" + money + "成功!"); this.money -= money; System.out.println(name + "来取钱后,余额剩余:" + this.money); }else { System.out.println(name + "来取钱:余额不足~"); } } catch (Exception e) { e.printStackTrace(); } finally { lk.unlock(); // 解锁 } } }
做个题:有100份礼品,小红,小明两人同时发送,当剩下的礼品小于10份的时候则不再送出,利用多线程模拟该过程并将线程的名称打印出来。
5.3 死锁与乐观锁简介
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要 的同步资源,就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线 程处于阻塞状态,无法继续。
面试官:你能解释清楚什么是死锁,我就录取你! 面试者:你录取 我,我就告诉你什么是死锁! …. 恭喜你,面试通过了
如果我既要线程安全,又要同时访问共享资源,阁下该如何应对呢?答案是使用乐观锁,之前一上来就加锁,比较悲观,我们称之为悲观锁,线程虽安全,性能较差,乐观锁一开始不加锁,默认线程安全,等到线程要出现安全问题时使用一些巧妙的算法进行控制,实现线程安全的基础上提升性能。
关于死锁、乐观锁,cas机制、原子类等代码级剖析,以及单例模式中懒汉式的线程安全问题等,我们等到juc并发编程时会“再谈同步”!好好唠嗑唠嗑儿。
六. 线程通信
当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,以相互协调,避免无效的资源挣抢。线程通信的常见模式:是生产者与消费者模型。
- 生产者线程负责生成数据
- 消费者线程负责消费生产者生成的数据
- 注意:生产者生产完数据后应该让自己等待,通知其他消费者消费;消费者消费完数据之后应该让自己等待,同时通知生产者生成。
方法名称
说明
void wait()
让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或
notifyAll()方法
void notify()
唤醒正在等待的单个线程
void notifyAll()
唤醒正在等待的所有线程
这三个方法是object类中的方法,不是线程类所独有的,每个对象都有此方法,应该使用当前同步锁对象进行调用。
public class Desk {
private List<String> list = new ArrayList<>();
// 放1个包子的方法
// 厨师1 厨师2 厨师3
public synchronized void put() {
try {
String name = Thread.currentThread().getName();
// 判断是否有包子。
if(list.size() == 0){
list.add(name + "做的肉包子");
System.out.println(name + "做了一个肉包子~~");
Thread.sleep(2000);
// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
}else {
// 有包子了,不做了。
// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 吃货1 吃货2
public synchronized void get() {
try {
String name = Thread.currentThread().getName();
if(list.size() == 1){
// 有包子,吃了
System.out.println(name + "吃了:" + list.get(0));
list.clear();
Thread.sleep(1000);
this.notifyAll();
this.wait();
}else {
// 没有包子
this.notifyAll();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
//2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。
Desk desk = new Desk();
// 创建3个生产者线程(3个厨师)
new Thread(() -> { //runnable接口是函数式接口,其匿名内部类可简化为lambda表达式
while (true) {
desk.put();
}
}, "厨师1").start();
new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师2").start();
new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师3").start();
// 创建2个消费者线程(2个吃货)
new Thread(() -> {
while (true) {
desk.get();
}
}, "吃货1").start();
new Thread(() -> {
while (true) {
desk.get();
}
}, "吃货2").start();
}
}
desk没有实现runnable接口?那desk是线程类吗?那谁实现了runnable接口呢?是ThreadTest类的匿名内部类实现的!当然,desk实现runnable接口也没问题
public class Desk implements Runnable {
// ... 其他代码 ...
@Override
public void run() {
while (true) {
// 这里可以添加你的线程逻辑
put();
// 或者
// get();
}
}
}