👨🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:首期文章
📚订阅专栏:多线程&JUC
希望文章对你们有所帮助
JUC的学习也是需要一些计算机、操作系统的知识的,也算是比较重要的吧,其实自己也是接触了不少的,包括之前做Redis的项目的时候也是老摸这些玩意。但是还没有非常系统的学过,为了即将到来的面试,JUC抓紧速成一波。
多线程的实现和常用成员方法
- 实现多线程
- 继承Thread类
- 实现Runnable接口
- 利用Callable和Future接口
- 总结
- 多线程的常用成员方法
- 线程优先级
- 守护线程
- 礼让线程
- 插入线程
实现多线程
继承Thread类
将类声明为Thread类的子类,该子类应重写Thread类的new方法,接下来就可以分配并启动该子类的实例,开启一个线程。
这里实现一个简单的Demo:
1、创建Thread类的子类:
public class MyThread extends Thread{
@Override
public void run() {
//线程要执行的代码
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "Hello World!");
}
}
}
2、创建对象开启实例,观察结果:
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//给线程起名字,方便观察运行结果
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
最终运行结果显示线程1和线程2是并行执行,而不是串行的了。
实现Runnable接口
先自己定义一个类,这个类实现了Runnable接口,并重写run方法。接着在测试类中创建这个类的对象,再创建一个Thread类的对象,并开启线程。
1、定义MyRun类并实现Runnable接口,由于这个类没有继承Thread类,所以不能使用getName方法,但是Thread类本身具有静态方法currentThread
可以获取当前在执行任务的线程的对象,因此可以通过这个对象来执行getName方法:
public class MyRun implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//获取当前线程的对象
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "Hello World");
}
}
}
2、测试类中创建该类的对象、Thread类的对象,并开启线程:
public class ThreadDemo {
public static void main(String[] args) {
//创建MyRun的对象,表示多线程要执行的任务
MyRun mr = new MyRun();
//创建线程对象,并setName
Thread t1 = new Thread(mr);
t1.setName("线程1");
Thread t2 = new Thread(mr);
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
利用Callable和Future接口
第三种实现方式实际上是对前面两种方法的补充。
前面两种实现方式都重写了run,但是其没有返回值,我们就无法获取多线程运行的结果。
第三种实现方式的特点就是可以获取多线程运行结果
。
其实现的流程:
1、创建一个MyCallable类实现Callable接口
2、重写call方法,方法是有返回值的,表示多线程运行的结果
3、编写测试类:
(1)创建MyCallable对象表示多线程要执行的任务
(2)创建Future对象,该对象可以管理多线程运行的结果,由于Future是一个接口无法创建对象,所以要创建其实现类FutureTask的对象
(3)创建Thread对象,再开启线程
1、MyCallable类:
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//求1-100之间的和
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
2、测试类:
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建MyRun的对象,表示多线程要执行的任务
MyCallable mc = new MyCallable();
//创建FutureTask对象
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建线程对象
Thread t = new Thread(ft);
//开启线程
t.start();
Integer result = ft.get();
System.out.println(result);
}
}
总结
1、继承Thread:
(1)优点:变成简单,可以直接使用Thread类中的方法
(2)缺点:可拓展性差,不能再继承其他的类
2、实现Runnable接口:
(1)优点:拓展性强,实现该接口的同时还能继承其他的类
(2)编程相对复杂,不能直接使用Thread类中的方法
3、实现Callable接口:
(1)除了Runnable接口的优点,还可以观察线程运行的结果
(2)编程相对复杂,不能直接使用Thread类中的方法
多线程的常用成员方法
下面是多线程常用的一些成员方法,其实很多也都是见过的:
方法名称 | 说明 |
---|---|
String getName() | 返回线程名称 |
void setName(String name) | 设置线程名,也可以使用构造方法 |
static Thread currentThread() | 获取当前线程对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
setPriority(int newPriority) | 设置线程优先级 |
final int getDaemon(boolean on) | 设置为守护线程 |
public static void yield() | 礼让线程 |
public static void join() | 插入进程 |
对于前4种方法,有一些注意点和细节:
1、若没有给线程设置名字,使用getName也是会有默认的名字的,格式为Thread-X(X为序号,从0开始),可以自行去底层看一下
2、除了可以用setName可以给线程设置名字,也可以使用构造函数MyThread t = new MyThread("线程");
,但是需要保证MyThread里面的构造函数内部使用super关键字,从而重写父类的构造方法。
3、如果我们没有启动线程,执行Thread.currntThread(),也可以获取到线程,这个线程的名称即为main,即JVM虚拟机启动以后,会自动启动多条线程,其中有一条就是main线程,作用是调用main方法,并执行里面的代码
4、sleep():
(1)哪条线程执行到这个方法,哪条线程就睡眠
(2)方法的参数就表示睡眠的时间,单位为毫秒
(3)当时间到了以后,线程会自动被唤醒,继续执行下面的代码
线程优先级
首先需要了解一下操作系统中的系统调度,调度分为抢占式调度和非抢占式调度,在java虚拟机中采用的是抢占式调度
,因此可以给线程设置优先级,优先级越大,抢占到CPU、被执行的概率就越大。
线程的优先级可以设置为1-10,默认为5。
注意:
1、可以使用getPriority来获取线程的优先级
2、优先级越高只能代表该线程被执行的概率越大,而不代表一定会被执行
守护线程
守护线程这个名称听起来不是很好理解,可以想象成是一个备胎线程,当其他的非守护线程执行完毕后,守护线程其实根本没有执行的必要了,会陆续的结束。将线程设置为守护线程调用t.setDaemon(true)
即可。
这其实还是有应用场景的,比如QQ中的聊天和发送文件可以视为2个线程,当我们把聊天的线程关闭了以后,线程2就没有执行的必要了,就会自动终止。
礼让线程
我们之前设置了两个线程来执行相同的任务,可以发现两个线程确实是交替执行的,但是经常会出现线程1连续输出了好多内容这种情况,出现的原因就是因为线程的执行是有概率性的,而礼让线程表示将当前CPU的执行权出让:
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName());
//出让当前CPU的执行权
Thread.yield();
}
}
}
但这里也只能尽量让结果均衡,但是还是可能会出现那种情况,所以这种方法用的少。
插入线程
观察下面的代码:
MyThread t = new MyThread();
t.setName("土豆");
t.start();
for(int i = 0; i < 10 ; ++i) {
System.out.println("main线程" + i);
}
最后执行会先执行main线程,毕竟执行的就是main函数。
而使用join方法就可以将其插入到main线程之前,使得其先被执行:
t.join();//把t线程插入到当前线程之前,在这里当前线程就是main线程
这个方法用的也不多,只需要了解一下就好了。