目录
多线程的实现方案二:实现Runnable接口(匿名内部类形式)
4.线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
多线程
什么是线程?
线程(thread)是一个程序内部的一条执行路径。
main方法的执行就是一条单独的执行路径。
程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
什么是多线程?
指从软硬件上实现多条执行流程的技术。
多线程用在哪里?
购票系统、上传和下载、消息通信、淘宝、京东....
创建线程(3种)
一、继承Thread类,并且重写run()方法。
Thread类中的run方法不是抽象方法,Thread类也不是抽象类
- 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
- 创建MyThread类的对象
- 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
首先,定义一个Ch01的类,再创建一个外部类MyThread,让它去继承Thread类。继承完之后发现没有报错,说明,他不是抽象类。反之,如果报错,说明它是抽象类,并且run方法是抽象方法。
//定义一个线程继承Thread类
class MyThread extends Thread {
//重写run方法
}
public class Ch01 {
}
然后,重写run方法。
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("子线程执行输出"+i);
}
}
}
启动线程
当MyThread继承了Thread类之后,它就是一个独立的线程。要让线程启动,调用线程的start方法。
思考:MyThread中无start方法,如何调用?
想要调方法,就得先创建对象。对象调方法。
public class Ch01 {
public static void main(String[] args) {
System.out.println(1);
MyThread myThread = new MyThread();
myThread.start();
System.out.println(3);
System.out.println(4);
}
}
测试结果
为什么结果输出的是run方法中的内容?
当调用start方法启动一个线程时,会执行重写的run方法的代码。调用的是start,执行的是run。
为什么不直接调run方法,而是调用start启动线程?
直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
只有调用start方法才是启动一个新的线程执行。
接下来,在主线程中再输出几个语句,运行看结果。
package com.jsoft.morning;
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("子线程执行输出"+i);
}
}
}
public class Ch01 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i = 0; i < 3; i++) {
System.out.println("子"+i);
}
}
}
运行测试
可以看出每次运行的结果都不一样。实际上是:主线程把子线程启动之后,主线程跑得很快,然后有可能子线程就抢到了控制台的打印,所以子线程也跑,然后主线程又抢到了..线程的优先级,概率问题!做不到百分百。
方法一的优缺点
- 优点:编码简单
- 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。
把主线程任务放在子线程之前。
如果放在主线程之前,主线程一直都是先跑完的,相当于一个单线程的效果
public class Ch01 {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
System.out.println("子"+i);
}
MyThread myThread = new MyThread();
}
}
测试结果
可以看出无论如何运行,主线程都是一直先跑的。
总结
方式一是如何实现多线程的?
- 继承Thread方法
- 重写run方法
- 创建线程对象
- 调用start()方法启动
方式二、实现Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理。
- 调用线程对象的start()方法启动线程
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程"+i);
}
}
}
public class Ch02 {
public static void main(String[] args) {
// System.out.println(1);
// 创建一个任务对象
Runnable target = new MyRunnable();
}
}
Thread t = new Thread(target);
t.start();
完整代码
package com.jsoft.morning;
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程"+i);
}
}
}
public class Ch02 {
public static void main(String[] args) {
// 创建一个任务对象
Runnable myThread2 = new MyRunnable();
Thread t = new Thread(myThread2);
//启动线程
t.start();
for (int i = 0; i < 4; i++) {
System.out.println("主线程"+i);
}
}
}
运行测试
方式二的优缺点
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
//继承
class MyRunnable extends Object implements Runnable { }
//实现接口
class MyRunnable implements Runnable,Cloneable { }
- 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
总结
第二种方法是如何创建线程的?
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理。
- 调用线程对象的start()方法启动线程
多线程的实现方案二:实现Runnable接口(匿名内部类形式)
- 可以创建Runnable的匿名内部类对象。
- 交给Thread处理。
- 调用线程对象的start()启动线程。
package com.jsoft.morning;
public class ThreadDemo2 {
public static void main(String[] args) {
//创建一个任务对象
Runnable target=new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程"+i);
}
}
};
//把任务对象交给Thread处理
Thread t=new Thread(target);
//启动线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程"+i);
}
}
}
简化代码(使用箭头函数(lambda表达式))
package com.jsoft.morning;
/**
* 使用箭头函数(lambda表达式)
*/
public class ThreadDemo2 {
public static void main(String[] args) {
//创建一个任务对象
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程" + i);
}
}
};
//把任务对象交给Thread处理
Thread t = new Thread(target);
//启动线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程" + i);
}
}
}
把target后面的部分去掉
new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程" + i);
}
}
}
放到 Thread t = new Thread(target),代替target。
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程" + i);
}
}
});
再把整个new,去掉,代替t.start();的t。
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程" + i);
}
}
}).start();
这个时候发现,new Runnable是灰色的,说明可以不写,替换,CTRL+左键查看Runnable源码发现,是个函数。
将run方法去掉
new Runnable() {
@Override
public void run
加上->
new Thread(()-> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程" + i);
}
}).start();
前两种线程创建方式都存在一个问题
- 他们重写的run方法均不能直接返回结果。
- 不适合需要返回线程执行结果的业务场景。
因此,JDK5.0提供了Callable和FutureTask来实现
方法三、利用Callable和FutureTask接口实现
1.得到任务对象
(1)定义类实现Callable接口,重写call方法,封装要做的事情。
(2)用FutureTask把Callable对象封装成线程任务对象。
2.把线程任务对象交给Thread处理。
3.调用Thread的start方法启动线程,执行任务
4.线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
package com.jsoft.morning;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* 实现Callable接口
*/
//定义一个任务类,实现Callable接口,应该声明线程任务执行完毕后结果的数据类型
class MyCallable implements Callable<String> {
//重写call方法
public String call() throws Exception {
System.out.println(2);
return "call方法的返回值";
}
}
public class Ch04 {
public static void main(String[] args) {
System.out.println(1);
// Callable-->FutureTask-->RunnableFuture-->Runnable-->Thread
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
System.out.println(3);
System.out.println(4);
}
}
方法名称 | 说明 |
public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask对象。 |
public V get() throws Exception | 获取线程执行call方法返回的结果。 |
方法三的优点和缺点
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂
总结
方式 | 优点 | 缺点 |
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂。不能返回线程执行的结果 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 | 编程相对复杂 |
Thread常用API
Java中提供两种类型的线程
用户线程
QQ,主程序就是用户线程。
守护程序线程
守护线程为用户线程提供服务,仅在用户线程运行时才需要。守护线程对于后台支持任务非常有用,比如:垃圾回收。
大多数JVM线程都是守护线程。
创建守护线程 ,任何线程继承创建它的线程守护进程状态。由于主线程是用户线程,因此在main方法内启动的任何线程默认都是守护线程。
public class Ch05 extends Thread {
@Override
public void run() {
super.run();
}
public static void main(String[] args) {
Ch05 ch05 = new Ch05();
// ch05就变成了守护线程
ch05.setDaemon(true);
ch05.start();
}
}
Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。
线程的休眠方法
方法名称 | 说明 |
public static void sleep(long time) | 让当前线程休眠指定的时间后再继续执行,单位为毫秒 |
public class Ch06 {
public static void sleep(int i) {
try {
// 线程休眠1秒
Thread.sleep(i);
System.out.println("哈哈哈...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
sleep(3000);
}
}
线程的生命周期(!重要!)
NEW:这个状态主要是线程未被start()调用执行
RUNNABLE:线程正在JVM中被执行,等待来自操作系统的调度
BLOCKED:阻塞。因为某些原因不能立即执行需要挂起等待。
WAITING:无限期等待。Object类。如果没有唤醒,则一直等待。
TIMED_WAITING:有限期等待,线程等待一个指定的时间
TERMINATED:终止线程的状态,线程已经执行完毕。
等待和阻塞两个概念有点像,阻塞因为外部原因,需要等待,
等待一般是主动调用方法,发起主动的等待。等待还可以传入参数确定等待时间。
CPU多核缓存结构
CPU缓存为了提高程序运行的性能,现在CPU在很多方面对程序进行优化。
处理速度:CPU>内存>硬盘
在CPU处理内存数据时,如果内存运行速度太慢,就会拖累CPU的速度。为了解决这样的问题,CPU设计了多级缓存策略。
CPU分为三级缓存:每个CPU都有L1,L2缓存,但是L3缓存是多核公用的。
CPU查找数据时,CPU -> L1 -> L2 -> L3 -> 内存 -> 硬盘
英特尔提出了一个协议MESI协议
1、修改态,此缓存被动过,内容与主内存中不同,为此缓存专有
2、专有态,此缓存与主内存一致,但是其他CPU中没有
3、共享态,此缓存与主内存一致,其他的缓存也有
4、无效态,此缓存无效,需要从主内存中重新读取
可见性
thread线程一直在高速读取缓存中的isOver,不能感知主线程已经把isOVer改成了true
这就是线程的可见性的问题。
怎么解决?
volatile能够强制改变变量的读写直接在内存中操作。
public class Ch03 {
private volatile static boolean isOver = false;
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(!isOver){
}
System.out.println(number);
}
});
thread.start();
Thread.sleep(1000);
number = 50;
// 已经改了,应该能退出循环了
isOver = true;
}
}
线程安全
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
指令重排
是指编译器或CPU为了优化程序的执行性能而对指令进行重新排序的一种手段。
volatile关键字的作用
volatile 的主要作用有两点:
保证变量的内存可见性
禁止指令重排序
指令3不能被排到1和2前面, 但是1和2之间没有依赖关系,编辑器就可以重排1和2。 不会对程序的执行顺序产生干扰。
public class Ch01 {
{
int a = 1; // 1
int b = 2; // 2
int c = a + b; // 3
}
public static void main(String[] args) {
int [] nums = new int[]{1,2,3,4,5};
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
}
}
线程的可见性
public class VolatileExample {
/**
* main 方法作为一个主线程
*/
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 开启线程
myThread.start();
// 主线程执行
for (; ; ) {
if (myThread.isFlag()) {
System.out.println("主线程访问到 flag 变量");
}
}
}
}
/**
* 子线程类
*/
class MyThread extends Thread {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改变量值
flag = true;
System.out.println("flag = " + flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
线程争抢
解决线程争抢最好的解决方法:加锁。