2022-08-02 第八组 学习笔记

目录

多线程

什么是线程?

什么是多线程?

多线程用在哪里?

创建线程(3种)

一、继承Thread类,并且重写run()方法。 

启动线程

 方法一的优缺点

 总结

方式一是如何实现多线程的?

方式二、实现Runnable接口

 方式二的优缺点

总结

多线程的实现方案二:实现Runnable接口(匿名内部类形式)

 简化代码(使用箭头函数(lambda表达式))

前两种线程创建方式都存在一个问题

方法三、利用Callable和FutureTask接口实现

1.得到任务对象

2.把线程任务对象交给Thread处理。

3.调用Thread的start方法启动线程,执行任务

4.线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。

方法三的优点和缺点

总结

Thread常用API

Java中提供两种类型的线程

用户线程

守护程序线程

线程的休眠方法

线程的生命周期(!重要!)

CPU多核缓存结构

英特尔提出了一个协议MESI协议

可见性

线程安全

指令重排

volatile关键字的作用

线程的可见性

线程争抢


多线程

什么是线程?

线程(thread)是一个程序内部的一条执行路径。

main方法的执行就是一条单独的执行路径。

程序中如果只有一条执行路径,那么这个程序就是单线程的程序。

什么是多线程?

指从软硬件上实现多条执行流程的技术。

多线程用在哪里?

购票系统、上传和下载、消息通信、淘宝、京东....

创建线程(3种)

一、继承Thread类,并且重写run()方法。 

Thread类中的run方法不是抽象方法,Thread类也不是抽象类

  1. 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  2. 创建MyThread类的对象
  3. 调用线程对象的start()方法启动线程(启动后还是执行run方法的)

首先,定义一个Ch01的类,再创建一个外部类MyThread,让它去继承Thread类。继承完之后发现没有报错,说明,他不是抽象类。反之,如果报错,说明它是抽象类,并且run方法是抽象方法。

//定义一个线程继承Thread类
class MyThread extends Thread {
//重写run方法
}
public class Ch01 {

}

然后,重写run方法。

class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("子线程执行输出"+i);
        }
    }
}

启动线程

当MyThread继承了Thread类之后,它就是一个独立的线程。要让线程启动,调用线程的start方法。


 思考:MyThread中无start方法,如何调用?

想要调方法,就得先创建对象。对象调方法。

public class Ch01 {

    public static void main(String[] args) {
        System.out.println(1);
        MyThread myThread = new MyThread();
        myThread.start();
        System.out.println(3);
        System.out.println(4);
    }
}

测试结果

为什么结果输出的是run方法中的内容?

当调用start方法启动一个线程时,会执行重写的run方法的代码。调用的是start,执行的是run。

为什么不直接调run方法,而是调用start启动线程?   

直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。

只有调用start方法才是启动一个新的线程执行。

接下来,在主线程中再输出几个语句,运行看结果。

package com.jsoft.morning;

class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("子线程执行输出"+i);
        }
    }
}

public class Ch01 {

    public static void main(String[] args) {

        MyThread myThread = new MyThread();

        myThread.start();

        for (int i = 0; i < 3; i++) {
            System.out.println("子"+i);
        }
    }
}

运行测试

 可以看出每次运行的结果都不一样。实际上是:主线程把子线程启动之后,主线程跑得很快,然后有可能子线程就抢到了控制台的打印,所以子线程也跑,然后主线程又抢到了..线程的优先级,概率问题!做不到百分百。

 方法一的优缺点

  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

把主线程任务放在子线程之前。

如果放在主线程之前,主线程一直都是先跑完的,相当于一个单线程的效果

public class Ch01 {

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            System.out.println("子"+i);
        }
        MyThread myThread = new MyThread();
    }
}

测试结果

可以看出无论如何运行,主线程都是一直先跑的。

 总结

方式一是如何实现多线程的?

  • 继承Thread方法
  • 重写run方法
  • 创建线程对象
  • 调用start()方法启动

方式二、实现Runnable接口

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。
  4. 调用线程对象的start()方法启动线程
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程"+i);
        }
    }
}
public class Ch02 {

    public static void main(String[] args) {
  //      System.out.println(1);
//       创建一个任务对象
        Runnable target = new MyRunnable();
    }
}
 Thread t = new Thread(target);
t.start();

完整代码

package com.jsoft.morning;

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程"+i);
        }
    }
}
public class Ch02 {

    public static void main(String[] args) {
//       创建一个任务对象
        Runnable myThread2 = new MyRunnable();
        Thread t = new Thread(myThread2);
        //启动线程
        t.start();
        for (int i = 0; i < 4; i++) {
            System.out.println("主线程"+i);
        }
    }
}

运行测试

 方式二的优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
//继承
class MyRunnable extends Object implements Runnable {   }
//实现接口
class MyRunnable  implements Runnable,Cloneable {   }
  • 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。

总结

第二种方法是如何创建线程的?

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。
  4. 调用线程对象的start()方法启动线程

多线程的实现方案二:实现Runnable接口(匿名内部类形式)

  1. 可以创建Runnable的匿名内部类对象。
  2. 交给Thread处理。
  3. 调用线程对象的start()启动线程。
package com.jsoft.morning;

public class ThreadDemo2 {

    public static void main(String[] args) {
        //创建一个任务对象
        Runnable target=new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程"+i);
                }
            }
        };
        //把任务对象交给Thread处理
        Thread t=new Thread(target);
        //启动线程
        t.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程"+i);
        }
    }
}

 简化代码(使用箭头函数(lambda表达式))

package com.jsoft.morning;

/**
 * 使用箭头函数(lambda表达式)
 */
public class ThreadDemo2 {

    public static void main(String[] args) {
        //创建一个任务对象
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程" + i);
                }
            }
        };
        //把任务对象交给Thread处理
        Thread t = new Thread(target);
        //启动线程
        t.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程" + i);
        }
    }
}

把target后面的部分去掉

new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程" + i);
                }
            }
        }

放到 Thread t = new Thread(target),代替target。

Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程" + i);
                }
            }
        });

再把整个new,去掉,代替t.start();的t。

new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程" + i);
                }
            }
        }).start();

这个时候发现,new Runnable是灰色的,说明可以不写,替换,CTRL+左键查看Runnable源码发现,是个函数。

 将run方法去掉

new Runnable() {
            @Override
            public void run

 加上->

 new Thread(()-> {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程" + i);
                }
      }).start();

前两种线程创建方式都存在一个问题

  • 他们重写的run方法均不能直接返回结果。
  • 不适合需要返回线程执行结果的业务场景。

因此,JDK5.0提供了Callable和FutureTask来实现

方法三、利用Callable和FutureTask接口实现

1.得到任务对象

(1)定义类实现Callable接口,重写call方法,封装要做的事情。

(2)用FutureTask把Callable对象封装成线程任务对象。

2.把线程任务对象交给Thread处理。

3.调用Thread的start方法启动线程,执行任务

4.线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。

package com.jsoft.morning;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
 * 实现Callable接口
 */
//定义一个任务类,实现Callable接口,应该声明线程任务执行完毕后结果的数据类型
class MyCallable implements Callable<String> {
    //重写call方法
    public String call() throws Exception {
        System.out.println(2);
        return "call方法的返回值";
    }
}

public class Ch04 {

    public static void main(String[] args) {
        System.out.println(1);
        // Callable-->FutureTask-->RunnableFuture-->Runnable-->Thread
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        new Thread(futureTask).start();
        System.out.println(3);
        System.out.println(4);
    }
}
方法名称说明
public FutureTask<>(Callable call)把Callable对象封装成FutureTask对象。
public V get() throws Exception获取线程执行call方法返回的结果。

方法三的优点和缺点

优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。

可以在线程执行完毕后去获取线程执行的结果。

缺点:编码复杂

总结
 

方式优点缺点
继承Thread类编程比较简单,可以直接使用Thread类中的方法扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现Runnable接口扩展性强,实现该接口的同时还可以继承其他的类编程相对复杂。不能返回线程执行的结果
实现Callable接口扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果编程相对复杂

Thread常用API

Java中提供两种类型的线程

用户线程

QQ,主程序就是用户线程。

守护程序线程

守护线程为用户线程提供服务,仅在用户线程运行时才需要。守护线程对于后台支持任务非常有用,比如:垃圾回收。

大多数JVM线程都是守护线程。

创建守护线程 ,任何线程继承创建它的线程守护进程状态。由于主线程是用户线程,因此在main方法内启动的任何线程默认都是守护线程。

public class Ch05 extends Thread {

    @Override
    public void run() {
        super.run();
    }

    public static void main(String[] args) {

        Ch05 ch05 = new Ch05();
        // ch05就变成了守护线程
        ch05.setDaemon(true);
        ch05.start();
    }
}

Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。

线程的休眠方法

方法名称说明
public static void sleep(long time)让当前线程休眠指定的时间后再继续执行,单位为毫秒
public class Ch06 {

    public static void sleep(int i) {
        try {
            // 线程休眠1秒
            Thread.sleep(i);
            System.out.println("哈哈哈...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        sleep(3000);
    }
}

线程的生命周期(!重要!)

NEW:这个状态主要是线程未被start()调用执行

RUNNABLE:线程正在JVM中被执行,等待来自操作系统的调度

BLOCKED:阻塞。因为某些原因不能立即执行需要挂起等待。

WAITING:无限期等待。Object类。如果没有唤醒,则一直等待。

TIMED_WAITING:有限期等待,线程等待一个指定的时间

TERMINATED:终止线程的状态,线程已经执行完毕。

等待和阻塞两个概念有点像,阻塞因为外部原因,需要等待,

等待一般是主动调用方法,发起主动的等待。等待还可以传入参数确定等待时间。

CPU多核缓存结构

CPU缓存为了提高程序运行的性能,现在CPU在很多方面对程序进行优化。

处理速度:CPU>内存>硬盘

在CPU处理内存数据时,如果内存运行速度太慢,就会拖累CPU的速度。为了解决这样的问题,CPU设计了多级缓存策略。

CPU分为三级缓存:每个CPU都有L1,L2缓存,但是L3缓存是多核公用的。

CPU查找数据时,CPU  ->  L1  ->  L2  ->  L3  ->  内存  ->  硬盘

英特尔提出了一个协议MESI协议

1、修改态,此缓存被动过,内容与主内存中不同,为此缓存专有

2、专有态,此缓存与主内存一致,但是其他CPU中没有

3、共享态,此缓存与主内存一致,其他的缓存也有

4、无效态,此缓存无效,需要从主内存中重新读取

可见性

thread线程一直在高速读取缓存中的isOver,不能感知主线程已经把isOVer改成了true

这就是线程的可见性的问题。

怎么解决?

volatile能够强制改变变量的读写直接在内存中操作。

public class Ch03 {

    private volatile static boolean isOver = false;

    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while(!isOver){
                }
                System.out.println(number);
            }
        });
        thread.start();
        Thread.sleep(1000);
        number = 50;
        // 已经改了,应该能退出循环了
        isOver = true;
    }
}

线程安全

多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。

指令重排

是指编译器或CPU为了优化程序的执行性能而对指令进行重新排序的一种手段。

volatile关键字的作用

volatile 的主要作用有两点:

保证变量的内存可见性

禁止指令重排序

指令3不能被排到1和2前面, 但是1和2之间没有依赖关系,编辑器就可以重排1和2。 不会对程序的执行顺序产生干扰。

public class Ch01 {

    {
        int a = 1; // 1
        int b = 2; // 2
        int c = a + b; // 3
    }
  public static void main(String[] args) {
        int [] nums = new int[]{1,2,3,4,5};
        for (int i = 0; i < nums.length; i++) {
            System.out.println(nums[i]);
        }
    }
}

线程的可见性

public class VolatileExample {

    /**
     * main 方法作为一个主线程
     */
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        // 开启线程
        myThread.start();

        // 主线程执行
        for (; ; ) {
            if (myThread.isFlag()) {
                System.out.println("主线程访问到 flag 变量");
            }
        }
    }

}

/**
 * 子线程类
 */
class MyThread extends Thread {

    private boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 修改变量值
        flag = true;
        System.out.println("flag = " + flag);
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

线程争抢

解决线程争抢最好的解决方法:加锁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值