大家好我是wave,本篇文章给大家介绍一些有关线程的基本概念与线程的一些基本操作
线程与进程
- 进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。还存在资源开销、包含关系、内存分配、影响关系、执行过程等区别。同一进程的线程共享本进程的地址空间和资源(堆和方法区),而进程之间的地址空间和资源相互独立。
- 举个栗子就是我们从桌面随便打开一个应用程序,比如开启一个qq或者wegame,都算是启动了一个进程。而线程是一个比进程更小的单位,比如在qq打开多个聊天窗口与别人聊天,就是一种多线程的表现。
并发与并行
- 在计算机的早期cpu是只有一个核的,也就是说只能同时执行一个任务,但是我们仍然可以在电脑上同时运行很多的软件,这是为什么呢?其实这就涉及到一个概念叫并发。
并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程或线程快速交替的执行。
- 随着计算机发展,现在的cpu基本都是多核的了,所有也有一个并行的概念。也就是不同的任务由不同的核来执行,就可以产生并行的情况。
- 为什么要使用多线程呢?
- 如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
- 那么要如何启动一个线程呢?
Thread
如果一个类继承了Thread类,那么这个类就可以看成是一个线程,调用这个类的start方法,就可以启动线程。但是要记得一定要重写这个Thread类中的run方法,这个线程的业务逻辑就应该写在run方法中,start方法被调用后就会运行run方法里面的业务逻辑。
Attention:本篇文章有大量代码,建议大家自行运行一下,感受线程运行的感觉,文章末尾会给出博客链接,公众号无法复制代码可以去博客上复制代码
演示一个非线程的类
//演示一个非线程的类
class Hero{
public String name;//名字
public int hp;//生命值
public int damage;//伤害
Hero(String name,int hp,int damage){
this.name = name;
this.hp = hp;
this.damage = damage;
}
Hero(){}
public void attack(Hero hero){
try {
//方便查看攻击过程
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//攻击动作
hero.hp -= this.damage;
System.out.println(this.name + "正在攻击" + hero.name + "," + hero.name + "当前生命值为" + hero.hp);
if(hero.isDead()){
System.out.println(hero.name + "已经死了");
}
}
public boolean isDead(){
return hp < 0 ? true : false;
}
}
public class Solution {
public static void main(String[] args) {
Hero Darius = new Hero();
Darius.name = "德莱厄斯";
Darius.hp = 616;
Darius.damage = 50;
Hero Anivia = new Hero();
Anivia.name = "艾尼维亚";
Anivia.hp = 300;
Anivia.damage = 30;
Hero Draven = new Hero();
Draven.name = "德莱文";
Draven.hp = 500;
Draven.damage = 65;
Hero leesin = new Hero();
leesin.name = "盲仔";
leesin.hp = 455;
leesin.damage = 80;
//诺手攻击冰鸟
while(!Anivia.isDead()){
Darius.attack(Anivia);
}
//德莱文攻击盲僧
while(!leesin.isDead()){
Draven.attack(leesin);
}
}
}
- 我们可以看到代码是顺序执行的,所以必须先执行完诺手攻击冰鸟才能进行德莱文攻击盲僧。那在真实场景下肯定不是这样的呀,诺手的攻击和德莱文的攻击应该是同步进行的,那使用线程来优化一下吧~
class Hero{
public String name;//名字
public int hp;//生命值
public int damage;//伤害
Hero(String name,int hp,int damage){
this.name = name;
this.hp = hp;
this.damage = damage;
}
Hero(){}
public void attack(Hero hero){
try {
//方便查看攻击过程
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//攻击动作
hero.hp -= this.damage;
System.out.println(this.name + "正在攻击" + hero.name + "," + hero.name + "当前生命值为" + hero.hp);
if(hero.isDead()){
System.out.println(hero.name + "已经死了");
}
}
public boolean isDead(){
return hp < 0 ? true : false;
}
}
class Kill extends Thread{
private Hero hero1;
private Hero hero2;
public Kill(Hero h1, Hero h2){
this.hero1 = h1;
this.hero2 = h2;
}
//重写run方法
public void run(){
while(!hero2.isDead()){
hero1.attack(hero2);
}
}
}
public class Solution {
public static void main(String[] args) {
Hero Darius = new Hero();
Darius.name = "德莱厄斯";
Darius.hp = 616;
Darius.damage = 50;
Hero Anivia = new Hero();
Anivia.name = "艾尼维亚";
Anivia.hp = 300;
Anivia.damage = 30;
Hero Draven = new Hero();
Draven.name = "德莱文";
Draven.hp = 500;
Draven.damage = 65;
Hero leesin = new Hero();
leesin.name = "盲仔";
leesin.hp = 455;
leesin.damage = 80;
Kill killThread1 = new Kill(Darius,Anivia);
killThread1.start();
Kill killThread2 = new Kill(Draven,leesin);
killThread2.start();
}
}
- 这里使用了Kill类,让英雄的攻击操作是在不同的线程完成的,这样就可以达到德莱厄斯和德莱文的攻击是同时进行的了,而不是先执行写在前面的代码,再执行写在后面的代码。
Runnable
Runnable演示案例
//演示Runnable
class Hero{
public String name;//名字
public int hp;//生命值
public int damage;//伤害
Hero(String name,int hp,int damage){
this.name = name;
this.hp = hp;
this.damage = damage;
}
Hero(){}
public void attack(Hero hero){
try {
//方便查看攻击过程
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//攻击动作
hero.hp -= this.damage;
System.out.println(this.name + "正在攻击" + hero.name + "," + hero.name + "当前生命值为" + hero.hp);
if(hero.isDead()){
System.out.println(hero.name + "已经死了");
}
}
public boolean isDead(){
return hp < 0 ? true : false;
}
}
class Kill implements Runnable{
private Hero hero1;
private Hero hero2;
public Kill(Hero h1, Hero h2){
this.hero1 = h1;
this.hero2 = h2;
}
//重写run方法
public void run(){
while(!hero2.isDead()){
hero1.attack(hero2);
}
}
}
public class Solution {
public static void main(String[] args) {
Hero Darius = new Hero();
Darius.name = "德莱厄斯";
Darius.hp = 616;
Darius.damage = 50;
Hero Anivia = new Hero();
Anivia.name = "艾尼维亚";
Anivia.hp = 300;
Anivia.damage = 30;
Hero Draven = new Hero();
Draven.name = "德莱文";
Draven.hp = 500;
Draven.damage = 65;
Hero leesin = new Hero();
leesin.name = "盲仔";
leesin.hp = 455;
leesin.damage = 80;
Kill killThread1 = new Kill(Darius,Anivia);
//新建一个线程类,把KillThread1作为参数传入
//因为实现Runnable接口并没有start方法,所以需要一个Thread类才能启动线程
new Thread(killThread1).start();
Kill killThread2 = new Kill(Draven,leesin);
new Thread(killThread2).start();
}
}
- 仔细对比Thread与Runnable各自实现一个线程的区别,你会发现Kill类上除了把继承Thread类改成了实现Runnable类之外并没有改变。有改变的地方是main函数中现实Runnable接口的Kill类需要当做一个参数传入Thread类中,因为只实现了Runnable接口并没有start方法,也就是不能作为线程的启动类。
- 打开Thread与Runnable的源码,你也会发现Runnable只定义了一个run方法。而Thread类中就有大量的方法,有些方法是native关键字修饰的,所以启动一个线程必须是一个Thread类。这也是实现了Runnable接口的类还需要当做参数传入Thread类的愿因。
Thread类源码片段
public
class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
Runnable接口源码
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
- 如果你对Runnable里面为啥有抽象方法不解的话就仔细阅读下面文字。
- 函数式接口,首先是一个接口,然后就是在这个接口里面只能有一个抽象方法,但是可以有多个非抽象方法的接口。
- Java 8为函数式接口引入了一个新注解@FunctionalInterface,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。
- 函数式接口可以被隐式转换为 lambda 表达式。
lambda表达式创建一个线程
//lambda表达式创建一个线程
public static void main(String[] args) {
new Thread(()->{
int n = 0;
for (int i = 0; i < 10; i++) {
n++;
}
System.out.println(n);//输出10
}).start();
}
有关Java8的知识后面会单独出一篇文章给大家介绍~
Callable
Callable使用案例
class Hero{
public String name;//名字
public int hp;//生命值
public int damage;//伤害
Hero(String name,int hp,int damage){
this.name = name;
this.hp = hp;
this.damage = damage;
}
Hero(){}
public void attack(Hero hero){
try {
//方便查看攻击过程
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//攻击动作
hero.hp -= this.damage;
System.out.println(this.name + "正在攻击" + hero.name + "," + hero.name + "当前生命值为" + hero.hp);
if(hero.isDead()){
System.out.println(hero.name + "已经死了");
}
}
public boolean isDead(){
return hp < 0 ? true : false;
}
}
class Kill implements Callable{
private Hero hero1;
private Hero hero2;
public Kill(Hero h1, Hero h2){
this.hero1 = h1;
this.hero2 = h2;
}
@Override
public Object call() throws Exception {
while(!hero2.isDead()){
hero1.attack(hero2);
}
//比Runnable多一个返回值
return "战斗结束";
}
}
public class Solution {
public static void main(String[] args)throws Exception {
Hero Darius = new Hero();
Darius.name = "德莱厄斯";
Darius.hp = 616;
Darius.damage = 50;
Hero Anivia = new Hero();
Anivia.name = "艾尼维亚";
Anivia.hp = 300;
Anivia.damage = 30;
Hero Draven = new Hero();
Draven.name = "德莱文";
Draven.hp = 500;
Draven.damage = 65;
Hero leesin = new Hero();
leesin.name = "盲仔";
leesin.hp = 455;
leesin.damage = 80;
//创建一个callable
Kill killThread1 = new Kill(Darius,Anivia);
//把callable放入futureTask
FutureTask futureTask1 = new FutureTask(killThread1);
//把futureTast放入Thread类,再启动线程
new Thread(futureTask1).start();
Kill killThread2 = new Kill(Draven,leesin);
FutureTask futureTask2 = new FutureTask(killThread2);
new Thread(futureTask2).start();
//获取运行结果
System.out.println(futureTask1.get());
System.out.println(futureTask2.get());
}
}
- 除了Thread与Runnable之外Java还有一种创建线程的方式,就是实现Callable接口,再把Callable接口放入FutureTask类中,因为FutureTask实现了Runnable接口,然后就可以用Thread类启动线程了。
- 对比Callable的代码与Runnable的代码其实也没有大多的改动,仅仅是把Run方法改成了Call方法,然后多一个返回值。
- 使用Callable的好处就在于Thread类与Runnable接口实现一个线程是没有返回值的,也就是这个线程运行之后的结果需要使用共享变量或线程的通信,所以比较不方便。Callable的话就可以直接获取返回值。
- Callable在很多地方都是配合线程池一起使用的,后序线程池的文章也会再和大家讲解的。
本次文章到这里就结束了,如果你是新手小白的话,建议运行运行文章中的代码,可以更直观的感受线程是什么样的。