参考资料 https://blog.youkuaiyun.com/Evankaka/article/details/44153709
多线程
线程与进程的概念:
进程中可以分出1-n个线程 进程是资源分配的最小单位 线程是CPU调度的最小单位
进程和线程都有五个生命周期阶段:创建 就绪 运行 阻塞 和 终止
1.实现多线程的几种方法
1.1.继承Thread类
首先要声明一下,开启多线程后,是执行对应类中的run方法,所有需要多线程执行的内容都应放在run方法中
start()是启动多线程的方法,是为当前的方法构造一个线程,是包含在Thread类中的,继承就可以使用了
public class Thread01 extends Thread {//继承Thread类
String name;
public Thread01(String name) {//重写构造方法
this.name = name;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("运行" + name + ":" + i);
try {
sleep((int) (Math.random()*10));//延时来放大多线程的效果
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MAIN{
public static void main(String[] args) {//开启一个进程
Thread01 mt1 = new Thread01("A");
Thread01 mt2 = new Thread01("B");
mt1.start();//开启多线程
mt2.start();
}
}
我们来思考一下执行的顺序,首先是开启了一个main进程,在这个进程中,我们分别构造了两个Thread01对象,这两个对象都继承了Thread,就可以用start方法来开启线程,通过两个start,我们开启了两个线程,这两个线程的执行是完全随机的,是CPU自己随机决定的,因此在执行run方法中的内容的顺序也是随机的,通过多次运行,我们就可以看见区别。
1.2.实现Runnable接口
这里实现的Runnable接口,可以让一个类拥有多线程类的特征,Thread在本质上也是在内部实现了一个Runnable接口才让继承的类有多线程特征,因此实现Runnable方法才是本质的方法。
public class Thread02 implements Runnable{
String name;
public Thread02(String name){
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程"+name+":"+i);
try{
Thread.sleep((int)(Math.random()*10));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
class MAIN2{
public static void main(String[] args) {
Runnable run01 = new Thread02("C");
Runnable run02 = new Thread02("D");
Thread mt01 = new Thread(run01);
Thread mt02 = new Thread(run02);
mt01.start();
mt02.start();
/*
也可以简化写成如下式子
new Thread(new Thread02("E")).start();
*/
}
}
来看实现过程,是先指向接口的实现类,然后再指向Thread类,因为Thread里面有start()方法。当然如果我们不需要拿到Thread对象,也可以按照简写来。
1.3.两者区别
Runnable接口的方法比Thread类是有明显优势的。主要体现在Runnable接口的方法可以更好的进行资源共享,也更加安全,在后续的学习中使用范围更广。
但是我们必须明确的是,线程之间所有的启动都是由CPU自己决定的,是完全随机的。并且每个java文件在运行时,一定会启动main方法线程和垃圾回收线程
2.线程状态转换
在前面,我们提过了线程的五个生命周期,即创建 就绪 运行 阻塞 和 终止。接下来讲解线程在什么情况下处于什么状态。
1.当我们实现了Runnable接口或继承了Thread类之后,我们就得到了一个有多线程特征的类,在main方法中我们通过new Thread()方法创建了一个多线程对象。也可以说是达到了初始状态,即创建状态
2.建立后,通过start()方法,这个对象就就绪了。达到了可运行的标准,这时候线程并没有运行,只是可运行,属于就绪状态
3.接着就是运行状态,java会通过内部的线程池等执行线程
4.在运行过程中,我们可以调用sleep(),wait()方法等使线程进入到阻塞状态。此时线程是不运行的,注意,不同的命令导致的阻塞状态原理是不同的,这里不再深究。
5.当sleep()等方法结束后,线程又会进入到可运行状态,即和start()后是同一个状态,接着进入运行状态
6.当线程执行完后,就进入终止状态,也称死亡状态,垃圾回收线程会负责处理后事
3.线程调度
3.1.线程的优先级
在Java中,不同的线程会有不同的优先级,但必须非常注意的一点是,优先级高的线程不一定先执行,只能说该线程可以拥有更大的执行可能性
线程的优先级由1-10这10个数字决定,优先级越高,数字越大
3.1.1.优先级的设定
在Thread类中,有如下三个定义
public static final int MIN_PRIORITY = 1;//最小优先级
public static final int MAX_PRIORITY = 10;//最大优先级
public static final int NORM_PRIORITY = 5;//默认优先级 线程创建后一般都是默认优先级
线程的优先级是可继承的,即A线程创建了B线程,那么B拥有和A一样的线程优先级
3.1.2.优先级的查看与设置
在Thread类中有getpriority()和setpriority()两个方法,所有继承了Thread类的方法都可以使用
class MAIN2{
public static void main(String[] args) {
Runnable run01 = new Thread02("C");
Thread mt01 = new Thread(run01);
System.out.println(mt01.getPriority());
mt01.setPriority(9);
System.out.println(mt01.getPriority());
}
}
//执行结果为5和9
3.2.线程的睡眠、等待、让步和加入
3.2.1.线程的睡眠
睡眠一般用的是sleep()方法,这是在Thread类下的方法,如果是实现Runnable接口的话,就要写Thread.sleep()方法。注意,sleep()方法是不会让出资源的,若要让出资源需要用到wait()方法
在前面演示多线程的代码中就有sleep()方法
线程睡眠可以放大多线程的效果,因为线程的睡眠可以把资源让给别的线程,这样就不会由于CPU效率高而导致多线程的不明显
3.2.2.线程的等待与唤醒
等待是wait()命令,唤醒是notify()和notifyAll()两个命令
这里需要提一个概念:锁
锁这个概念是针对一个实例对象的,一个实例对象只有一把锁。一个对象在执行的时候,同时只能执行一个线程,锁就是通行证,哪个线程拿到了对象的锁,那么这个线程就会先执行。
锁池和等待池:锁池是竞争锁的地方,等待池是存放被wait()的线程的,只有通过notify()和notifyAll()两个命令才会让其返回到锁池中去竞争锁。未被wait()的,且没有抢到锁的线程都在锁池中等待竞争。
所有可以参与竞争锁的线程都在锁池中等待竞争。当线程执行完后,就会将锁释放,这个对象中的其他处于锁池中线程就会去争夺这个锁,哪个线程得到了,那么就执行那个线程。
wait()命令是将当前执行线程的锁释放,并让该线程进入到等待池中,其余没有竞争到锁的线程则依旧留在锁池中。从而导致当前线程的阻塞。而要唤醒该对象,就需要执行notify()或者notifyAll(),这样就会使原先释放锁的线程再次参与到锁的竞争中。notify()是随机释放一个在等待池中的线程前往锁池去竞争锁,而notifyAll()是释放所有在等待池中的线程去竞争锁。并且,释放的过程是要等到synchronized代码块中的所有内容都执行完毕,才会执行释放。
最后需要注意的是,wait()需要在监视器synchronized()内部去使用。且在java中,Thread类线程执行完run()方法后,一定会自动执行notifyAll()方法
介绍完上面的概念,就可以看下面这段代码了。其中也可以看出sleep和wait的区别
public class WaitandSleep {
final static Object flag = new Object();
public static class T1 implements Runnable{
@Override
public void run() {
synchronized (flag){
System.out.println("T1 is running...");
try{
System.out.println("T1 is going to waiting...");
flag.wait();//准备wait了
//Thread.sleep(2000); 这里将wait注释掉,运行后查看结果即可看出区别
System.out.println("T1 is ending...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static class T2 implements Runnable{
@Override
public void run() {
synchronized (flag){
System.out.println("T2 is running...");
flag.notify();//将T1从等待池中挪至锁池中
System.out.println("T2 is ending...");
}
}
}
}
class MAIN{
public static void main(String[] args) {
new Thread(new WaitandSleep.T1()).start();
new Thread(new WaitandSleep.T2()).start();
}
}
wait 执行结果:
T1 is running…
T1 is going to waiting…
T2 is running…
T2 is ending…
T1 is ending…
sleep 运行结果:sleep只是单纯的等待,并不释放锁,看代码时可以自动忽略来预知结果,而wait要复杂不少
T1 is running…
T1 is going to waiting…
T1 is ending…
T2 is running…
T2 is ending…
3.2.3.线程的让步
让步的方法是yield(),是先让优先级比当前线程高的其他线程先执行,但是该线程随时会被线程调度器选中,即让出线程的占有权,但是时间长短是随机的。sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
3.2.4.线程的加入
加入的方法是xxx.join(int),join是在Thread类下的一个方法。是让当前线程暂停,然后执行join的线程。在join线程执行完后再继续向下执行。这里不再深究,在并行并发中会有更多的可能。
4.与多线程有关的名词解释
主线程:即main方法产生的线程
当前线程:是指通过Thread.currentThread()可以获得的线程
用户线程:主线程必须要等用户线程结束后才可以结束
后台线程:又名守护线程,是为其他线程提供服务的线程。主线程不需要等待后台线程就可以结束
isDaemon()方法可以判断一个线程是否为守护线程
setDaemon()方法可以将一个线程设置为守护线程
前台线程:指接受后台线程服务的线程
currentThread():方法课可以得到当前线程
setName()方法可以为线程设置一个名称
5.线程同步
5.1.线程同步的概念
线程同步是指保证同一对象在同一时间只能有一个线程去访问它
5.2.线程同步的实现
线程同步就是通过上述的锁来实现的。每当一个线程获得了一个对象的锁,其他线程就无法访问该对象及其方法了。而剩余需要访问该对象的线程就会进入阻塞状态
具体可以通过synchronized()来获得。由上述wait()方法中 的介绍可知,synchronized是一种监视器,谁拿到()中的锁,谁就可以执行内部的代码。这里为了保证访问唯一,()内部的对象必须是唯一的,不然在构造时会重复产生,就无法做到线程同步。值得注意的是,()内部的参数是个Object类
5.2.1.synchronized的使用方式
1.直接在方法前修饰
public synchronized void way{
}
2.直接在方法内部包括
public void way{
synchronized(this){
}
}
前面两种方法,监视器的作用范围是等价的,都是一整个方法
3.在方法内部的某处
public void way{
int a;
int b;
final static Object flag = new Object();
synchronized(){
final static Object flag = new Object();
}
system.out.println("a+b="+(a+b));
}
需要注意的是,当某个方法在访问synchronized代码块时,其他的方法仍可以随意访问非synchronized代码块
6.线程数据传递
6.1.通过构造方法传入
在开启多线程之前,我们在建立Thraed类或其子类后,通过构造方法可以把一些简单的数据传入。这样自然也不存在线程存在后再传入的情况
class MyThread1 extends Thread {
private String name;
public MyThread1(String name) {
this.name = name;
}
public void run() {
System.out.println("hello " + name);
}
public static void main(String[] args) {
Thread thread = new MyThread1("world");//在构造方法中传入name
thread.start();
}
}
6.2.通过public方法传入
就是先建立对象,然后再通过类里面的public方法进行设置
class MyThread2 implements Runnable
{
private String name;
public void setName(String name)
{
this.name = name;
}
public void run()
{
System.out.println("hello " + name);
}
public static void main(String[] args)
{
MyThread2 myThread = new MyThread2();//先建立
myThread.setName("world");//再通过setName方法进行传入
Thread thread = new Thread(myThread);
thread.start();
}
}
6.3.通过回调函数传回数据
在多线程中,部分数据是要通过中间部分的计算等等产生的,为了带回这个数据,我们可以定义一个类,将该类传入那个计算方法,并将计算结果传给该类的某个属性或方法,当方法运行结束返回后,该结果也会保存在我们传入的类中
import java.util.Random;
class Data{
public int value = 0;
}
class Work{
public void process(Data data,Integer[] numbers){
for(int n: numbers){
data.value += n;//process方法将三个数的和存入传入的data的value中
}
}
}
class ThreadData extends Thread{
private Work work;
public ThreadData(Work work){
this.work = work;
}
@Override
public void run() {
Data data = new Data();//先设定一个data
Random random = new Random();//随机设定3个数
int n1 = random.nextInt(10);
int n2 = random.nextInt(100);
int n3 = random.nextInt(50);
work.process(data,new Integer[]{n1,n2,n3});//传入前面的data,运行process方法
System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"
+ String.valueOf(n3) + "=" + data.value);//value中的值就是三个数之和,就相当于拿到了数据
}
public static void main(String[] args) {
Thread t = new ThreadData(new Work());//
t.start();//进入到run方法
}
}