Java 多线程学习笔记

参考资料 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方法
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值