java多线程学习复习一篇就够了
全文从多线程的实现方式、线程的状态、线程的方法、线程的同步、线程的通讯、等角度对多线程的基础知识进行总结
一、多线程的实现方式
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法
- 实现Callable接口,重写call方法
这是三种多线程的创建方式,此外我们创建多线程时可以使用多个Thread对象开启同一个Runnable接口实现类达到多线程的目的,也可以使用多个Thread对象开启多个Runnable实现类来达到多线程,这两种实现方法笔者思考了下,感觉都是有应用场景的,我们在购票系统中,投票的方法其实是多线程都需要调用的,我们就可以使用多个Thread开启同一个Runnable实现类的方式来实现多线程,只需要为卖票方法加锁即可。但若是有一个队列我们想要用多线程去处理队列里面的内容,此时就可以使用多个Thread开启多个Runnable实现类的方式来适用不同对象的不同处理方式,从而提高处理效率。
1.继承Thread类,重写run方法
这种实现方式是jdk1.0便已经存在了,实现也很简单,直接继承Thread,重写run方法,然后我们通过Thread对象的start方法启动即可。
示例代码如下:
public class Test1 extends Thread{
@Override
public void run() {
while(true){
System.out.println("线程1");
}
}
public static void main(String[] args) {
new Test1().start();
}
}
2.实现Runnable接口,重写run方法
实现Runnable接口,其实与第一种实现方式本质上还是一样的,因为Thread就是实现了Runnable接口。当使用实现Runnable接口的方式实现多线程时,应该重写run方法。启动线程时使用Thread的start方法,示例代码如下:
public class TestThread {
public static void main(String[] args) {
new Thread(() -> {
while(true)
System.out.println("Runnable多线程1");
}).start();
new Thread(() -> {
while(true)
System.out.println("Runnable多线程2");
}).start();
}
}
3.实现Callable接口,重写call方法
Callable接口是JDK1.5加入的,位置在java并发包里面,他的功能比较强大支持异常的抛出与线程执行结果的返回。这些都是前面两种实现方式所不具备的。前面两种能完成的实现Callable都可以完成,前面两种完不成的Callable也可以完成,所以没有理由不使用Callbale。示例代码如下:
public class TestThread {
public static void main(String[] args) throws Exception{
FutureTask<String> futureTask = new FutureTask<>(()->{
int i =0 ;
while(i<100)
System.out.println("Callable线程1在执行:"+i++);
return "线程1执行完了";
});
FutureTask<String> futureTask2 = new FutureTask<>(()->{
int i =0 ;
while(i<100)
System.out.println("Callable线程2在执行:"+i++);
return "线程2执行完了";
});
new Thread(futureTask).start();
new Thread(futureTask2).start();
System.out.println(futureTask.get());
System.out.println(futureTask2.get());
}
}
以上是三种多线程的实现方式,有人会把线程池也算成一种多线程的实现方式,笔者认为线程池并不能算是一种方式,他只是一种池化技术,这种技术在编程中有很多场景,底层还是使用的一样的技术而已。总结以上三种多线程的实现方式,
- 可以发现Thread是最low的一种,因为他是基于继承来实现的,而java中类时不支持多继承的(接口可以多继承),所以他会影响到线程类的扩展性
- 若是不需要线程的返回值建议使用Runnable方式,需要的话就只能选择Callable了
- 所有线程的创建方式都需要依赖Thread类的start方法,说明该方法才是创建线程的真正方法,该方法也是一个native方法。
- 因为所有线程的启动都需要依赖Thread,所以Thread中的方法可以说都是公用的,Thread也是很重要的
不过当下全是使用线程池来实现多线程技术,基本没有手动去创建线程的,所以线程池的使用和手动实现线程池才是应该注意的地方。
二、线程的状态
1.new状态
创建出Thread的对象后就是new的状态了。
2.就绪状态
Thread的start方法才是创建一个线程的根本,start方法调用native的实现去开启一个线程(可以发现无论哪种多线程的实现方式都需要Thread的start方法来开启线程),当这个start方法执行后线程就是new的状态了。
3.运行状态
运行状态就是线程在运行run方法或者call方法体中的内容了,线程在运行状态中可以进入到阻塞状态,也可以进入到就绪状态,比如yield就会让线程进入就绪状态,wait、sleep会让线程进入到阻塞状态。
4.阻塞状态
当现场争抢cpu时间片失败后就会进入到阻塞状态,阻塞状态中的线程需要等他其他线程释放cpu资源然后再一次和其他线程去争抢cpu的时间片,争抢成功则进入到运行状态,否则再次进入到阻塞状态。
5.死亡状态:线程如何正确的死亡
我们知道Thread已经提供了stop等方法用于停止一个线程或者说让一个线程死亡,但是stop方法是Dreprected的,是不推荐使用的,那我们该如何正确的停止一个线程呢?答案是在主线程中维护一个boolean变量,用这个变量控制线程的运行和死亡,比如如下代码:我们可以在适当的时候将flag置为false,这样线程也就结束了。此处有一点需要注意,volatile关键字修饰了flag,被volatile关键字修饰的变量一旦改变,会强制子线程中的该变量失效,子线程需要从主线程从新获取(可见性问题)。
public class TestThread {
volatile static Boolean flag = true;
public static void main(String[] args) throws Exception{
FutureTask<String> futureTask = new FutureTask<>(()->{
int i =0 ;
while(flag)
System.out.println("Callable线程1在执行:"+i++);
return "线程1执行完了";
});
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
我们都知道
三、Thread中常用的方法
1.setPriority():
设置线程优先级
需要注意的是线程的优先级高也不一定就肯定先执行,只是优先级高的线程先执行的概率更大而已,优先级从1-10,10为最高,不设置默认为5,到底谁先执行与操作系统有关系,主要看操作系统对cpu的调度,示例代码如下:
public class TestThread {
volatile static Boolean flag = true;
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(() -> {
System.out.println("优先级为1的线程");
});
Thread thread2 = new Thread(() -> {
System.out.println("优先级为3的线程");
});
Thread thread3 = new Thread(() -> {
System.out.println("优先级为5的线程");
});
Thread thread4 = new Thread(() -> {
System.out.println("优先级为7的线程");
});
Thread thread5 = new Thread(() -> {
System.out.println("优先级为10的线程");
});
thread1.setPriority(1);
thread2.setPriority(3);
thread3.setPriority(5);
thread4.setPriority(7);
thread5.setPriority(10);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
2.sleep():
让线程进入阻塞状态这个方法用过的人应该是非常多了,传入的是一个int型整数,代表毫秒。目的是让线程睡一会。该方法结束后线程进入就绪状态。值得注意的是每个线程都有一把锁,而sleep并不会释放这把锁。
3.join():
这是个插队的方法,他不是静态方法,需要使用Thread的对象来调用,当一个线程调用join方法时,该线程会强制抢占cpu资源,直到该线程执行完毕其他线程才会继续执行,示例代码如下:
public class TestThread {
volatile static Boolean flag = true;
public static void main(String[] args) throws Exception{
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("vip线程1:"+i);
}
});
for (int i = 0; i < 500; i++) {
System.out.println("主线程:"+i);
if(i==200)
thread.join();
}
thread.start();
}
}
4.yield():
释放cpu资源,让当前线程进入就绪状态,礼让操作,重新和其他线程竞争cpu资源。需要说的是礼让并不一定成功。笔者多次测试都礼让成功了,看来礼让还是有效果的。示例代码如下:
public class TestThread {
volatile static Boolean flag = true;
public static void main(String[] args) {
new Thread(() -> {
int i = 0;
while(i<100){
if(i==50)
Thread.yield();
System.out.println("线程1:"+i++);
}
}).start();
new Thread(() -> {
int i = 0;
while(i<100)
System.out