最近发现自己对JAVA多线程这一块的了解可以说是完全为0,于是开始对多线程基础进行了一些学习,今天主要来说一说JAVA多线程的一些基本用法吧
说到线程,那么我们得先知道线程是什么,为什么要用多线程,而提到线程,我们就还得了解另外两个与其密切相关的名词,程序与进程
作为一名程序员,那么对程序肯定不陌生,官方的解释是,程序是为了完成完成某项特定的任务,使用某种语言,编写的一组指令的集合。说白了程序也就是人想让机器做一件事,使用机器能读懂的语言,也就是计算机语言,机器看懂之后进行执行。
那什么是进程呢,我们在打开任务管理器时,应该会看到这个字眼,进程指的就是正在进行的程序
而线程,指的是在一个进程中,执行的一套功能流程,称为线程,类似的,在一个进程中执行的多套功能流程就叫做多线程
比如我们使用某某杀毒软件,打开其图形界面,这就是一个进程,而该软件有很多功能,比如杀毒、体检等等,不同的功能同时执行就是多线程
接下来我们看看JAVA的多线程吧,JAVA采用的是抢占式策略系统,指的是系统会分配给每个执行任务的线程很小的时间段,当该时间段用完后,系统会自动地剥夺其CPU使用权,交给其他线程执行。这样可以保证每个线程都能被使用到。而JVM本身就是多线程的,我们常用的main方法,就是一个线程,叫做主线程。
JAVA中提供了两种执行线程的方式,我们先看看第一种
首先我们声明一个类继承Thread类:
接着重写run()方法,在方法体中放入我们想要实现的功能,比如我想打印1到100之间的偶数,为了方便观察,我们使用Thread类中的getName()方法获取到当前的线程名:
/线程执行体
public void run(){
for (int i=0;i<100;i++){
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
这样我们就新建了一个线程,接下来我们创建该线程的实例:
PrimeThread primeClass=new PrimeThread();
再调用start()方法启动线程,默认会调用run()方法:
primeClass.start();
为了体现多线程,我们将该线程放在main方法中执行,因为main方法本身也是一个线程,同时我们再创建一个新的线程,为了区分,我们在main方法里编写一个循环,输出100至200之间的偶数,最终代码是这样:
public static void main(String[] args) {
PrimeThread primeClass=new PrimeThread();
primeClass.start();
PrimeThread primeClass2=new PrimeThread();
primeClass2.start();
for (int i=100;i<200;i++){
if (i%2!=0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
最终结果:
可以看到三个线程在交替运行自己的功能,这样一个简单的多线程就完成了,在这里有几个线程中常用的方法:
currentThread():获取当前线程
getName():获取线程名字
start():启动线程
刚才我们有提到,调用start()方法默认会调用run()方法,那么run与start方法究竟有何不同,我们将start改为run再执行,通过运行结果看看两者的区别:
多次运行后我们发现,调用run方法后,两个子线程不再执行,而是主线程在完成两个子线程的功能,这说明当前只有主线程启动了而两个子线程并没有启动,这也就是说明,当我们要启动一个线程时,要调用start方法。
接下来我们说一下创建执行线程的第二种方式:
首先我们声明一个类,实现runnable接口:
同样,我们实现第一种方式相同的功能,首先实现run()方法,再写入循环输出语句:
@Override
public void run() {
for (int i=0;i<100;i++){
if (i%2!=0){
System.out.println(Thread.currentThread().getName()+":"+i++);
}
}
接下来创建该类的实例:
HellloThread hellloThread=new HellloThread();
再创建Thread类的实例,将我们上面实现类的实例以参数的形式传入:
Thread t1=new Thread(hellloThread);
最后调用实例的start方法启动线程,最终代码:
HellloThread hellloThread=new HellloThread();
Thread t1=new Thread(hellloThread);
t1.start();
for (int i=100;i<200;i++){
if (i%2!=0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
而由于Runnable接口是一个函数式接口,如果了解过Lambda表达式,我们也可以使用该表达式简化代码:
Runnable runnable1=()->{
for (int i=0;i<100;i++){
if (i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
};
};
Thread t1=new Thread(runnable1);
t1.start();
for (int i=100;i<200;i++){
if (i%2!=0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
无需新建Runable接口的实现类以及重写run方法,直接使用Lambda表达式即可解决,但它也有局限性,一是lambda表达式只对函数式接口适用,二是表达式中的参数只能是final变量,比如我们要在线程中执行数字的运算就有点麻烦了
至此,两种创建启动线程的方法就写完了,那么这两种方法究竟有什么区别?我们通过一个功能进行比较,假设我们现在模拟火车站的购票窗口,100张票开三个窗口同时卖出。我们先使用继承Thread类的方式:
首先新建一个Window类继承Thread类:
声明一个全局变量100表示票数:
int tick=100;
重写run方法,方法体中放入售票方法:
@Override
public void run() {
while (tick>0){
System.out.println(Thread.currentThread().getName()+"完成售票,余票为"+--tick);
}
}
创建一个Test类,并且创建三个子线程,为了更便于观察,我们将线程名分别命名为窗口一、二、三,使用setName方法即可:
Window window1=new Window();
Window window2=new Window();
Window window3=new Window();
window1.setName("1号窗口");
window2.setName("2号窗口");
window3.setName("3号窗口");
window1.start();
window2.start();;
window3.start();
运行结果:
三个线程在交替进行,但从头看起,我们会发现一个问题:
三个窗口都出现了剩余99张票,这说明三个窗口各有100张票在卖,也就是300张,而我们的需求是3个窗口共享100张票,那么我们就要将全局变量用static修饰,这样才能对所有成员进行共享:
static int tick=100;//使用static修饰成员共享该属性
再次运行:
结果正常。
那么我们再使用实现Runable接口的方式完成该功能
同样我们写下一个实现类,实现run方法以及编写卖票功能代码:
public class Ticket implements Runnable{
int tick=100;
@Override
public void run() {
while (tick>0){
System.out.println(Thread.currentThread().getName()+"完成售票,余票为:"+--tick);
}
}
}
注意我们这里没有对票数进行static修饰
同样使用Test类,创建并且启动线程,我们可以直接在新建Thread实例时,通过构造器重写线程名:
Ticket ticket = new Ticket();
Thread t1=new Thread(ticket,"窗口1");
t1.start();
Thread t2=new Thread(ticket,"窗口2");
t2.start();
Thread t3=new Thread(ticket,"窗口3");
t3.start();
运行结果:
发现运行正常,没有出现窗口卖票重复的情况,仔细分析一下,我们就可以得出两种方式的区别:
第一种方式是通过新建多个实现类来达到多线程的功能,所以每个实现类的属性都是不同的,如果我们不加上static修饰,那么票数就是3个100。而第二种方法,我们只新建了一个实现类的实例,最终通过创建不同的Thread实例,但传入的是同一个实现类完成多线程,也就是我们共用了一个实现类完成多线程,所以不必使用static修饰总票数。那么从代码来看,很明显第二种方式较为简单,如果我们是5个窗口进行售票,使用第一种方式就要新建5个Window类,以及其他相关的方法也要进行5次,而使用第二种方式,不管窗口数有多少,我们都只用创建一个实现类的实例,再新建不同的Thread实例即可。也就是说,第二种实现Runnable接口的方式,更适合完成这种多个线程同时处理同一份资源的需求。