Java多线程,从零基础到入门

2023-12-14 21:13:05

多线程简介

多线程的两个概念——并发和并行简介

并发:在同一时刻,有多个指令在单个cpu上交替执行

并行:在同一时刻,有多个指令在多个cpu上同时执行

并发和并行是有可能同时进行的。

例如你的电脑是2核4线程的,遇到多个线程,在并行的同时每条线程也在并发

多线程的实现方式

有3种方法

1.利用Thread类创造多线程

创造新执行线程有两种方法

1.

(1)定义一个类声明为Thread的子类,

(2)该子类重写Thread的run方法。

(3)然后就可以实例化并启动子例(创建对象并调用方法)。

下面是一个例子

class MyThread extends Thread{
	public void run() {
		int cnt = 0;
		while(cnt<20) {
			cnt++;
			System.out.println(getName()+"run在运行");
		}
	}
}


public class Main {
    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交替运行,即并发

提问:为什么是用start而不是run?

答:因为线程的启动是通过调用start()方法来实现的,而非直接调用run()方法。

当调用start()方法时,它会启动一个新的线程,并在新线程中执行run()方法的内容。这样做的好处是,每个线程都有自己独立的执行路径,可以同时执行多个线程,实现并发操作。

如果直接调用run()方法,那么run()方法将会在当前线程(主线程)中直接执行,而不会创建新的线程,那就是单线程了,跟以前的调用没有区别。这样做的结果是,run()方法的执行会阻塞主线程,直到run()方法执行完毕才会继续执行后续代码,无法实现并发操作。

因此,在使用多线程时,我们应该通过调用start()方法来启动新线程,并让新线程执行run()方法的内容,以实现并发执行。

2.利用Runnable接口的方式进行实现

(1)创造一个类继承Runnable接口

(2)重写里面的run方法

(3)创造自己的类的对象(表示要执行的任务)

(4)创造一个Thread类对象,并开启多线程


class MyRun implements Runnable{

	@Override
	public void run() {
		// TODO Auto-generated method stub
		//System.out.println(getName()+"run方法在运行");
		//这样是错的,因为getName是Thread里的方法,无法直接调用
		
		//用Thread.currentThread可以获取到当前线程的对象
		Thread t = Thread.currentThread();
		for(int i = 0;i<10;i++) {			
			System.out.println(t.getName()+"run方法在执行");
			
			//System.out.println(Thread.currentThread().getName()+"run方法在执行");
			//这样的链式编程也可以,尽量用这种
		}
		
	}
	
}

public class Main {
    public static void main(String[] args){
    	//实例化MyRun对象
    	//表示多线程要执行的任务
    	MyRun mr = new MyRun();
        
    	Thread t1 = new Thread(mr);
    	Thread t2 = new Thread(mr);
    	
    	//给线程设置名字
        t1.setName("线程1");
        t2.setName("线程2");
        
        //开启多线程
        t1.start();t2.start();
    }
}

提问:

3.利用Callable和Future接口方式实现

第三种实现方法相当于对前面方法的补充。因为我们可以看到Thread和Runnable里的run方法都没有返回值,无法获取多线程运行的结果,第三种方法就弥补了这个缺陷。

特点:可以获取多线程运行的结果

(1)创建MyCallable类实现Callable接口

(2)重写call方法(是有返回值的,表示多线程的运行结果)

(3)创建MyCallable的对象(表示要多线程执行的任务)

(4)创建FutureTask的对象(管理多线程运行结果)。因为Future是一个接口,无法直接实例化,所以要创建一个类

(5)创建Thread类的对象,并启动

class MyCallable implements Callable<Integer>{
                             //<>里写返回值的类型
	@Override
	public Integer call() throws Exception {
		// TODO Auto-generated method stub
		
		//任务,算1-9的和
		int sum = 0;
		for(int i = 1;i<=9;i++) {
			sum+=i;
		}
		return sum;
	}
	
}



public class Main {
    public static void main(String[] args) throws InterruptedException, ExecutionException{
    	MyCallable mc = new MyCallable();
    	
    	FutureTask<Integer> ft = new FutureTask<>(mc);
    	
    	Thread t1 = new Thread(ft);
    	t1.start();
    	Integer res = ft.get();
    	System.out.println(res);
    	
    }
}

提问:不是说用Future接口实现吗,为什么用了FutureTask?

首先要知道一点,FutureTask类是编译器已经为我们实现好的一个类。

FutureTask是实现了Future接口的一个具体类,它实现了Future接口的所有方法,并且还提供了一些额外的方法。FutureTask可以用来包装一个CallableRunnable对象,将其异步执行,同时允许对其执行状态进行查询和取消任务的执行。

小结

多线程中常用的成员方法

Thread的构造方法

Thread的成员方法

下面是对以上方法的解读

线程的命名

我们可以用setName和构造方法对线程命名,但用构造方法命名,需要调用父类(也就是Thread)的构造方法,因为 子类不会继承到父类的 Thread(String name)构造方法!

package demo;
import java.util.*;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.Random;
import java.util.Arrays;
import javax.swing.*;
import java.awt.*;
import java.awt.*;
import java.awt.event.*;


class MyThread extends Thread{
	

	public MyThread() {
		super();
		// TODO Auto-generated constructor stub
	}

	//我们可以用setName和构造方法对线程命名,但用构造方法命名,
	//需要调用父类(也就是Thread)的构造方法,
	//因为 子类不会继承到父类的 Thread(String name)构造方法!
	public MyThread(String name) {
		super(name);
		// TODO Auto-generated constructor stub
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<10;i++) {
			System.out.println(getName()+"@"+i);
		}
	}
	
	
}

public class Main {
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyThread t1 = new MyThread("线程1"); 
        MyThread t2 = new MyThread("线程2"); 
    	
        t1.start();t2.start();
    	
    }
}

获取当前线程与线程休眠

要注意这两个方法是静态方法,因此可以通过类名来使用

public class Main {
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        System.out.println(Thread.currentThread().getName());
    	
    }
}

再讲一下sleep方法。在哪条线程用sleep,哪条线程就休眠。不要理解成sleep会让整个程序都休眠!下面是验证。

如果没有用sleep,那么一定是线程1先执行完,因为线程1只用输出0个数,线程2要输出1000个数。

但在线程1加上sleep后,线程1每输出一个数就会休眠1秒,在休眠的时候线程2就执行完毕了。因此是线程2先执行完。

package demo;
import java.util.*;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.Random;
import java.util.Arrays;
import javax.swing.*;
import java.awt.*;
import java.awt.*;
import java.awt.event.*;


class MyThread extends Thread{
	

	public MyThread() {
		super();
		// TODO Auto-generated constructor stub
	}

	//我们可以用setName和构造方法对线程命名,但用构造方法命名,
	//需要调用父类(也就是Thread)的构造方法,
	//因为 子类不会继承到父类的 Thread(String name)构造方法!
	public MyThread(String name) {
		super(name);
		// TODO Auto-generated constructor stub
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<10;i++) {
			System.out.println(getName()+"@"+i);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	
}

class MyThread2 extends Thread{
	
	public MyThread2() {
		;
	}
	public MyThread2(String name) {
		super(name);
		// TODO Auto-generated constructor stub
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<1000;i++) {
			System.out.println(getName()+"@"+i);
			
		}
	}
	
	
}

public class Main {
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyThread t1 = new MyThread("线程1");
        MyThread2 t2 = new MyThread2("线程2");
        t1.start();
        t2.start();
        
        
    }
}

线程的优先级

优先级共有 1-10 十个等级,5是默认的等级

线程的优先级高意味着它更有可能被CPU调度器选中执行,但并不意味着一定会抢占CPU。在多线程编程中,线程的优先级高只是增加了被选中执行的概率,而不是绝对的抢占。

详细地说,java采取的是抢占式调度,即多个线程去抢夺cpu的执行权,但是抢不抢的到是随机的,体现了随机性

因此,就算此时有两个线程,设置线程1的优先级是1,线程2的优先级是10。线程1依然有可能比线程2先执行完。因为线程2的优先级虽然高,但这是代表线程2先被执行的概率高,而不是一定会被优先执行!

package demo;
import java.util.*;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.Random;
import java.util.Arrays;
import javax.swing.*;
import java.awt.*;
import java.awt.*;
import java.awt.event.*;


class MyThread extends Thread{
	

	public MyThread() {
		super();
		// TODO Auto-generated constructor stub
	}

	//我们可以用setName和构造方法对线程命名,但用构造方法命名,
	//需要调用父类(也就是Thread)的构造方法,
	//因为 子类不会继承到父类的 Thread(String name)构造方法!
	public MyThread(String name) {
		super(name);
		// TODO Auto-generated constructor stub
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<10;i++) {
			System.out.println(getName()+"@"+i);
		}
	}
	
	
}



public class Main {
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyThread t1 = new MyThread("线程1");
        MyThread t2 = new MyThread("线程2");
        t1.setPriority(1);
        t2.setPriority(10);
        
        //按照概率来讲,线程1更有可能先执行完
        t1.start();
        t2.start();
        
        
    }
}

守护线程

线程可以分为守护线程和非守护线程。

当其他非守护线程执行完毕,守护线程会陆续结束。(注意,不是马上结束,而是会再执行一会才结束)

我们把非守护线程比作女神线程,守护线程比作备胎线程,当女神不存在,那么备胎也没有存在的必要了。验证一下

package demo;
import java.util.*;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.Random;
import java.util.Arrays;
import javax.swing.*;
import java.awt.*;
import java.awt.*;
import java.awt.event.*;


class MyThread extends Thread{
	

	public MyThread() {
		super();
		// TODO Auto-generated constructor stub
	}

	//我们可以用setName和构造方法对线程命名,但用构造方法命名,
	//需要调用父类(也就是Thread)的构造方法,
	//因为 子类不会继承到父类的 Thread(String name)构造方法!
	public MyThread(String name) {
		super(name);
		// TODO Auto-generated constructor stub
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<10;i++) {
			System.out.println(getName()+"@"+i);
			
		}
	}
	
	
}

class MyThread2 extends Thread{
	
	public MyThread2() {
		;
	}
	public MyThread2(String name) {
		super(name);
		// TODO Auto-generated constructor stub
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<1000;i++) {
			System.out.println(getName()+"@"+i);
			
		}
	}
	
	
}

public class Main {
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyThread t1 = new MyThread("女神");
        MyThread2 t2 = new MyThread2("备胎");
        
        t2.setDaemon(true);
        t1.start();
        t2.start();
        
        
    }
}

可以看到,女神线程结束后,备胎线程没有马上结束,而是陆续结束。

如果不是陆续结束的话,那么会打印到 备胎@999。运行结果显然不是这样的。

举个守护线程的例子,助于理解。

比如:我们在qq聊天,边聊天边给对方发文件,此时就有两条线程。

如果关闭了聊天窗口,那么发送文件的线程也会停止。

显然,在这里,传输文件线程就是守护线程,因为它会随着聊天线程的结束而陆续结束。

礼让线程

假如现在有线程1和线程2,此时线程1有cpu执行权

如果不使用礼让线程,那么线程1执行一次后依然拥有cpu的执行权,直到执行权被别的线程抢夺。这种情况的结果很大概率是线程1一下子就执行完了

如果使用礼让线程,那么线程1执行一次后,就会主动放弃cpu的执行权,然后所有线程(当然也包括线程1)再进行抢夺.

如运行结果所示,线程的执行变得更均匀了,因为会有使用礼让线程

package demo;
import java.util.*;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.Random;
import java.util.Arrays;
import javax.swing.*;
import java.awt.*;
import java.awt.*;
import java.awt.event.*;


class MyThread extends Thread{
	

	public MyThread() {
		super();
		// TODO Auto-generated constructor stub
	}

	//我们可以用setName和构造方法对线程命名,但用构造方法命名,
	//需要调用父类(也就是Thread)的构造方法,
	//因为 子类不会继承到父类的 Thread(String name)构造方法!
	public MyThread(String name) {
		super(name);
		// TODO Auto-generated constructor stub
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0;i<100;i++) {
			System.out.println(getName()+"@"+i);
			Thread.yield();
		}
	}
	
	
}



public class Main {
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyThread t1 = new MyThread("线程1");
        MyThread t2 = new MyThread("线程2");
        
        t1.start();
        t2.start();
        
        
    }
}

插入线程

插入线程是指在运行中的程序中创建一个新的线程,并将其插入到已有的线程执行流中。

下面这段代码,有两个线程

public class Main {
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyThread t1 = new MyThread("线程1");
        
        t1.start();
        
        for(int i = 0;i<10;i++) {
        	System.out.println("main线程"+i);
        }
        
    }
}

如果我们想执行完线程1在执行main线程,我们就可以用插入线程

public class Main {
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyThread t1 = new MyThread("线程1");
        
        t1.start();
        //将t1插入到当前线程之前
        //当前在main函数里,所以当前是main线程
        t1.join();
        
        for(int i = 0;i<10;i++) {
        	System.out.println("main线程"+i);
        }
        
    }
}

文章来源:https://blog.csdn.net/clmm_/article/details/134544790
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。