Java高并发板块(2)_多线程实现类

1. 进程与线程

1.1 串行

多个任务依次执行,上一个任务没有完成的时候,不能执行后续任务。

1.2 并发

多个任务依次执行,但是在执行的过程当中可以替换运行。
在某一个时刻还是一个任务在执行,但在一个时间段是多个任务在执行。
在这里插入图片描述

1.3 并行

多个任务同时执行,没有先后顺序。
最终执行结果跟耗时最长的任务有关。
在这里插入图片描述

1.4 多进程与多线程

相信各位看官们看了上述概念后,大概能明白进程与线程的关系,无非就是:进程是线程的容器。
但跟并发和并行有啥子关系哟。还请各位看官们别急,看笔者娓娓分解。
一个进程(领导-数据调度)中可以包含多个线程(职员-干活)。

为了能够充分压榨CPU,人们捣鼓出了多进程(任务分配管理者),利用CPU的分时机制,每个进程都能抢占到一定的CPU时间,而CPU的处理速度是极快的,于是乎就给咱反应不过来的大脑造成了许多进程任务“同时”执行的假象,也就是咱上面提到的并发。

后面人们发现,进程的启动和销毁需要浪费大量的系统性能,导致程序执行性能下降。
为了提升并发处理的操作性能,进一步压榨CPU,人们又搞出了个多线程(干活的员工)

因为线程足够“小”,所以启动速度和并发处理效果非常好,只要再加上够好的硬件设备,抗住千万级的并发访问也不是啥问题,想想双十一凌晨几千万的访问量,没把阿里服务器搞垮,咱多线程那可是功不可没的。

至于并行,由于多个任务在同一时刻执行,所以依赖于多处理器系统,但大部分情况下有多个任务执行时间差距不大的并发处理就够用了,镭射炮自有镭射炮的用处。

2. 创建多线程

2.1 继承Thread类

Thread核心方法有两个:
run()(这个run()是对资源描述的run()方法,暂且认为是Thread的方法,Runnable处再解释)
start()(线程运行的方法)
其定义代码如下:

public void run(){}
public synchronized void start(){……}

(synchronized同步先不用管,后续篇章会讲解)
代码如下:

class MyThread extends Thread{
	private String name;
	public MyThread(String name) {
		this.name = name;
	}
	@Override
	public void run() {
		for(int x = 0; x < 3; x++) {
			System.out.println(this.name + "执行" + x);
		}
	}
}
public class JavaThread{
	public static void main(String[] args) {
		new MyThread("线程一").start();
		new MyThread("线程二").start();
		new MyThread("线程三").start();
	}
}

以下是执行结果:

线程三执行0
线程一执行0
线程二执行0
线程一执行1
线程三执行1
线程一执行2
线程二执行1
线程三执行2
线程二执行2

各位看官们也看到了,三个线程的执行都是看CPU心情,爱先执行谁就先执行谁。
也就是随机执行(先不考虑优先级的问题)。
但是相信有的看官也有疑惑:用start()方法启动最后不还是调用run()方法么,为啥还要用start()方法启动呢?
看官别急,咱这就来好好探讨探讨这个问题。
咱把start()方法换成run()方法,来看看运行结果:

线程一执行0
线程一执行1
线程一执行2
线程二执行0
线程二执行1
线程二执行2
线程三执行0
线程三执行1
线程三执行2

这就奇了怪了,咋就变成了顺序执行了呢?
看官们别急,咱先打开API文档,找到Thread中的start()方法,发现里面有个start0()方法,这方法可不得了。

咱知道Java为了保证其可移植性,就弄了个虚拟机(JVM)出来,程序都是在JVM上先运行的。
但是多线程的操作是需要调用系统资源的,咋办呢?于是就有了start0()方法啦。
start0()方法由JVM实现,然后调用本地系统函数进行资源调度操作。
所以说start()方法起到了启动多线程的作用。

还有咱先前提到的多线程并发操作带来的优势——效率高,总得拿出点证据来是吧,咱这就来试验试验到底有没有这回事。
为了让效果更明显,咱把“x<3”改为“x<1000”,再在多线程启动操作前后获取时间戳,最后做差,代码如下:

……
long start = System.currentTimeMillis();
……
long end = System.currentTimeMillis();
System.out.println(end - start + "ms");

现在start方法启动后运行结果为1ms

再来看看没有用多线程,也就是用run()方法启动,结果如下:
32ms(平均值)

所以说啊,利用多线程进行并发处理的确是可以提高效率的。
看完Thread后咱再来看看Runnable。

2.2 实现Runnable接口

咱先打开API文档,找到Runnable,其定义代码如下:

@FunctionalInterface
public interface Runnable{
	public void run();
}

诶,看到这里相信各位看官们都会纳闷了,咋Runnable里也冒出了个run()方法呢?让咱打开API文档,找到Thread,其定义代码如下:

public class Thread extends Object implements Runnable{……}

哦,原来Thread是Runnable的子类啊,敢情Thread里面的run()方法原来是Runnable的呀。
这时眼尖的看官就发现问题了,这Runnable里面咋没有start()方法呢,那多线程可该怎么启动?看官别急,咱再打开文档,找到Thread,发现里面有个构造方法可有趣了,其定义代码如下:

public Thread(Runnable target){……}

原来这Thread还能接收Runnable的子类啊,于是乎这Runnable的子类被这么一包装,也能调用start()方法来启动多线程了。
说到这里,干脆把另外几个方法也给拎出来,其定义代码如下:

public Thread(Runnable target, String name) {……}

有了这个构造方法以后,咱就可以不用多此一举设定一个“name”属性了,直接利用这个构造方法在传入Runnable子类的时候就可以给线程命名啦;

public final String getName()

有了这个方法后,就可以取得线程名称啦:

public static Thread currentThread(){……} 

有了这个方法之后就可以取得线程对象啦。
清楚了以上关系和方法后,咱再来看Runnable是怎样实现多线程的,代码如下:

class MyThread implements Runnable{
	@Override
	public void run() {
		for(int x = 0; x <3; x++) {
			System.out.println(Thread.currentThread().getName() + "执行" + x);
		}
	}
}
public class JavaRunnable{
	public static void main(String[] args) {
		new Thread(new MyThread(), "线程一").start();
		new Thread(new MyThread(), "线程二").start();
		new Thread(new MyThread(), "线程三").start();
	}
}

以下是随机抽取的一次运行结果:

线程一执行0
线程二执行0
线程二执行1
线程二执行2
线程三执行0
线程一执行1
线程三执行1
线程一执行2
线程三执行2

2.3 实现Callable接口

咱还是先打开API文档看看Callable,其定义如下:

@FunctionalInterface
public interface Callable<V>{
	public V call() throws Exceptioin;
}

(泛型V的类型就是返回值的类型)
一看这代码就有意思了,start()方法没有也就算了,连run()方法都没有,那还弄啥多线程哦。
但咱也别过早下结论,先来看看API文档里是怎么描述的。

咱先找到RunnableFuture接口,发现其一个父接口是Runnable,这没什么;
咱再找到FutureTask类,发现其实现了RunnableFuture接口,这也没什么。
关键是FutureTask类里面有个构造方法不得了了,代码如下:

public FutureTask(Callable<V> callable){……}

清楚了以上关系和方法后,咱一起来看看Callable是如何实现多线程的,代码如下:

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyThread implements Callable<String>{
	@Override
	public String call() throws Exception {
		for(int x = 0; x<3; x++) {
			System.out.println(Thread.currentThread().getName() + "执行" + x);
		}
		return Thread.currentThread().getName() + "执行完毕";
	}
}
public class JavaCallable{
	public static void main(String[] args) throws Exception{
		FutureTask<String> taskA = new FutureTask<String>(new MyThread());
		new Thread(taskA, "线程一").start();
		new Thread(new FutureTask<String>(new MyThread()), "线程二").start();
		System.out.println(taskA.get());       //获取线程taskA的返回值
	}
}

随机抽取一次运行结果如下:

线程二执行0
线程一执行0
线程二执行1
线程二执行2
线程一执行1
线程一执行2
线程一执行完毕

3. 总结

看完以上三种实现多线程的方法后,相信看官们早就一肚子的疑问了,既然上面三种方式都可以实现多线程,那为什么要搞这么多花样呢?
实现方式这么多,实际开发中该怎么选取呢……各位看官别急,笔者这就为看官们解惑。

3.1 为什么有 这么多的实现方式呢

咱知道,Thread中的start()作为启动方法是必不可少的啦。
而Thread 和 Runnable的子类都可以利用run()方法进行资源描述,但run()方法可是有个致命缺陷的呢。
相信心细的看官早已发现,那就是——run()方法是没有返回值的

有时候就是需要线程中有返回值咋办呢?
于是Callable就诞生啦,利用FutureTask中的get()方法就能够获取线程的返回值。

但是Thread之后为什么还要弄个Runnable呢?
因为Thread类只能够单继承,而Runnable接口可以克服单继承的局限性。

3.2 实际开发如何选择

清楚了三种方式产生的实际背景后,就不难选择了。利用Runnable或Callable的子类进行资源的描述,然后利用Thread中的start()方法进行线程的启动。

以买票的为例,代码如下所示:
(暂时不管同步问题)

class MyThread implements Runnable{
	private int ticket = 5;
	@Override
	public void run() {
		for(int x = 0; x<1000; x++) {
			if(ticket > 0)
				System.out.println(Thread.currentThread().getName() + "买票,剩余票数:" + --this.ticket);
		}
	}
}
public class JavaDemo{
	public static void main(String[] args) {
		Runnable source = new MyThread();        //资源类
		new Thread(source, "线程一").start();
		new Thread(source, "线程二").start();
		new Thread(source, "线程三").start();
	}
}

随机抽取一次运行结果如所示:

线程一买票,剩余票数:3
线程三买票,剩余票数:2
线程三买票,剩余票数:0
线程二买票,剩余票数:4
线程一买票,剩余票数:1

这时有些天马行空的看官就会想了,能不能用Thread的子类来进行资源的描述呢?
从可行性来讲,当然是可以的啦。用Thread的子类来进行资源的描述也是可以创建多线程的,效果是一样的。

但笔者委实不推荐这种方式,因为这种方式不太合理。
Thread的子类进行资源的描述,则这个资源类本身就是一个线程对象,然后又用其它线程来启动这个线程对象,实在是不合理。

所以啊,以后创建多线程的时候就用Runnable(Callable)的子类进行资源的描述,再用Thread中的start()方法来启动线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值