当前位置: 代码迷 >> 综合 >> Java基础 多线程编程 Thread Runnable jdk 8 Lambda 表达式 注解 Callable接口
  详细解决方案

Java基础 多线程编程 Thread Runnable jdk 8 Lambda 表达式 注解 Callable接口

热度:30   发布时间:2023-12-13 03:14:47.0

Date和Long类型数据进行转换 日起在程序中主要是通过龙类型数据进行转换的

SimpleDateFormat类  对日期进行格式化 把Date类型的格式转换为字符串格式并且可以赋予相应的格式 

System类 系统中支持类

RuntTime类(用于返回当前应用程序的运行环境对象)  也存在GC  调用的还是system类里面的

线程的基本概念:

1 进程与线程的区别

进程:在操作系统的定义中指的是一次命令完整运行

目前的操作系统中是支持多进程启用的,而最早的misdos系统是单进程系统,一但中毒直接死机

单继承与多进程的区别?

缺点在执行两个任务的时候执行的速度取决于第一个任务执行的速度,这种操作对系统来说效率很低

在未来的发展中出现了多进程的系统,可以同时启动多个进程,CPU在多个进程之中进行不同的切换,让程序运行起来感觉很快

 

2 进程与线程的区别?

线程是在进程的基础上上进一步划分: 打开一个QQ.exe代表启动一个进程,在QQ可以聊天,聊天的同时可以传输文件,并且还可以语音聊天 在一个QQ进程中启动多个线程

例如:打开一个QQ 启动一个进程,在QQ上打开聊天 启动一个线程,在打开一个传输数据,又启动一个线程

从以上的结论中可以发现;在一个进程中可以存在多个线程:而线程是可以同时运行的

3多线程的优点:

效率高: 可以最大限度的利用CPU的空闲资源,比如一边编辑文档,一边打印文档中的内容

4 如何使用多线程

在java中存在两种方式可以完成多线程的使用,第一种方式就是 (继承)Thread类 ,在java中编写多线程程序的时候必须存在一个线程的主体类,而这个主体类只要继承了Thread类就可以完成多线程的功能

Thread这个存在于 java.lang.Thread类 可以直接使用不用导包

范例:继承Thread类

class MyThread extends Thread{

 

 

}

以上代码继承了一个Thread类,但是只继承Thread类并不能完成线程任务的编写,还必须覆写Thread类中run()方法,通过run()方法来完成线程任务的编写

范例:覆写run()方法 并编写线程执行的任务代码

class MyThread extends Thread{

private String title;  //给线程起一个名字

 

public MyThread(String title) {

 

this.title=title; //为线程命名

}

 

@Override

public void run() {  //这里编写需要多线程执行的代码

// TODO Auto-generated method stub

 

for(int i=0;i<10;i++) {  //在线程中输出10次i的内容

 

System.out.println(title+"="+i);

 

}

}

 

}

 

线程的主体代码已经编写完成,接下来启动一个线程

如何启动一个线程呢?

// 实例化线程的主体类 MyThread

 

public static void main(String[] args) {

 

MyThread  mt=new MyThread("线程A"); //实例化一个线程主体类 就等于实例化一个线程

 

MyThread mt1=new MyThread("线程B");

 

MyThread mt2=new MyThread("线程C");

 

mt.run(); //启动线程A

mt1.run(); //启动线程B

mt2.run(); //启动线程C

 

}

以上代码貌似启动了多线程,但是咱们分析,线程在程序中是同步运行,多个线程启动的时候会同时去抢占CPU的资源:

例如:军训的时候 饿一中午 到开饭的时间 65个人围一个大桌子 就存在一盆饭

多个线程启动的时候就相当于 抢饭一样,谁先抢到资源谁前执行

根据以上的分析,发现run()方法不能真正启动一个线程,而只是单纯的运行了run()方法中的代码,而想要真正启动一个线程,在Thread类中提供了一个start()方法 来启动一个真正的线程

范例:通过start()方法启动一个线程

// 实例化线程的主体类 MyThread

public static void main(String[] args) {

 

MyThread  mt=new MyThread("线程A"); //实例化一个线程主体类 就等于实例化一个线程

 

MyThread mt1=new MyThread("线程B");

 

MyThread mt2=new MyThread("线程C");

 

mt.start(); //启动线程A

mt1.start(); //启动线程B

mt2.start(); //启动线程C

 

}

 

程序的运行结果:

线程A=0

线程B=0

线程B=1

线程B=2

线程B=3

线程B=4

线程B=5

线程B=6

线程B=7

线程B=8

线程B=9

线程C=0

线程C=1

线程C=2

线程C=3

线程C=4

线程C=5

线程A=1

线程A=2

线程A=3

线程A=4

线程A=5

线程A=6

线程A=7

线程A=8

线程A=9

线程C=6

线程C=7

线程C=8

线程C=9

 

使用了start()方法启动了多线程之后,发现定义的3个线程在执行的时候 不是顺序执行了,而是随机的访问CPU的资源,过程为哪个线程先抢到资源哪个线程就先执行 线程的执行是存在随机性的

案例:

在正常的程序执行中:例如

Main方法中执行:存在三个任务

任务 输出一句话 输出任务1

任务2 会执行字符串拼接操作拼接100000万次 这个拼接的过程会很慢

任务3 输出 任务执行完毕

要求 把任务复杂的任务变为一个线程来执行,提升main方法中的速度

分析:该程序分为3步完成 任务1 于任务3 都需要在main方法中完成,只有任务2需要单独给定一个线程

范例:为任务2单独编写线程程序

class MyThread02 extends Thread{  //为任务2 编写线程任务

 

@Override

public void run() {  //单独的线程任务

// TODO Auto-generated method stub

 

String str=""; //空字符串等待拼接

 

for(int i=0;i<100000;i++) {

 

str+="hello";

}

}

 

 

}

为任务2编写单独的线程

public static void main(String[] args) {

 

MyThread02 mt=new MyThread02(); //实例化一个线程

//首先会存在3个任务

 

System.out.println("执行任务1");

 

mt.start(); //单独启动一个线程 执行任务2

 

System.out.println("任务执行3");

}

 

在主体方法main方法中把复杂的任务2作为一个单独的线程启动,这样就不会影响任务1 月任务3 的执行效率

问题: 为什么启动一个线程,必须使用start()方法?明明线程的业务写在run()方法中,为什么还需要使用start()方法

想要理解这个问题,需要观察Thread类中start()方法的源代码,大家观察一下

观察Thread类发现Thread类实现了Runnable接口

class Thread implements Runnable

接下来观察一下Runnable接口发现Runnable接口中只存在一个Run()方法

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();

}

说明Thread类中覆写的Run()方法其实就是Runnable接口Run方

public synchronized void start() {

        /**

         * This method is not invoked for the main method thread or "system"

         * group threads created/set up by the VM. Any new functionality added

         * to this method in the future may have to also be added to the VM.

         *

         * A zero status value corresponds to state "NEW".

         */

        if (threadStatus != 0)

            throw new IllegalThreadStateException();

 

         线程的状态 如果不等于0 会抛出IllegalThreadStateException() 状态异常, 这个异常的作用说明每个多线程只能启动一次

 

 

        /* Notify the group that this thread is about to be started

         * so that it can be added to the group's list of threads

         * and the group's unstarted count can be decremented. */

        group.add(this);

 

        boolean started = false;

        try {

            start0();

            started = true;

        } finally {

            try {

                if (!started) {

                    group.threadStartFailed(this);

                }

            } catch (Throwable ignore) {

                /* do nothing. If start0 threw a Throwable then

                  it will be passed up the call stack */

            }

        }

    }

 

    private native void start0();

 

通过观察start()方法的源码发现:

同一个实例的线程只能启动一次,如果启动超过一次则会抛出异常IllegalThreadStateException()

继续观察源码发现start()方法会调用一个start0()这个方法而这个方法的定义如下

private native void start0();

其中出现了native关键字,表示调用本地系统函数

原理如下:

在线程启动的时候,线程会抢占CPU的资源,CPU的资源调度,需要本地操作系统的支持

以上图一定要明白清楚的自己的可以画出来,面试过程中会存在为什么启动一个线程调用是start()方法而不是run()方法

线程的第二种实现方式 实现Runnable接口

为什么存在第二种方式,第一种方式继承Thread类有什么不好

最大的问题:java中存在单继承局限,如果一个类继承了Thread类则这个类就没有办法继承别的类

所以为了解决以上的问题,可以使用第二种方式,实现Runnable接口,在接口中没有单继承的局限,一个类可以实现多个接口

范例:通过实现Runnable接口来使用多线程

//实现一个线程的功能,必须同样存在线程的主体类

 

class MyThread implements Runnable{

 

private String title;  //保存线程的名字

 

public MyThread(String title) {

 

this.title=title;

}

 

@Override

public void run() {  //覆写Runnable接口中的run方法

// TODO Auto-generated method stub

//编写线程的功能

 

for(int i=0;i<10;i++) {

 

System.out.println(title+"="+i);

}

}

 

 

}

 

以上线程的主体类,已经编写完成,但是由于现在是实现的Runnable接口,对于start()方法在Thread类中存在,现在没有继承Thread则没有办法继承start()方法

解决以上问题,则需要观察一下Thread类中的构造方法

Thread类中的构造方法如下

public Thread(Runnable target);

观察Thread类的构造方法可以发现 构造方法的参数为Runnable接口对象,也就是说Thread类的构造方法可以接收Runnble接口的对象

在多态中讲过,在接口中一个父接口可以接收所有的子类对象

例如 Runable run=new MyThread(); 应为MyThread类实现了Runnable接口作为Runnable接口的子类 则可以通过构造方法传入给Thread类

通过以上的操作就可以把MyThread传入给Thread类 进而通过Thread类中的start()方法启动一个线程

注意:无论何时启动一个线程必须使用Thread类中的start()方法

范例:Runnable接口实现多线程的情况下启动一个线程

public class RunnableDemo01 {

 

public static void main(String[] args) {

 

//线程启动的主体类,永远需要Thread类

 

//实例化Thread类

 

//通过Thread的构造方法接口Runnble接口的子类对象

Thread t1=new Thread(new MyThread("线程A"));

 

Thread t2=new Thread(new MyThread("线程B"));

 

Thread t3=new Thread(new MyThread("线程C"));

 

//分别启动3个线程

 

t1.start();

 

t2.start();

 

t3.start();

}

}

 

Thread类与Runnable接口的关系

在多线程的实现中现在已经了两种方式,Thread类与Runnable接口,从代码上来讲肯定首选使用Runnable接口,应为可以很好的避免单继承局限,可以为程序做更多的扩展

通过源码分析过 其实Thread类也是实现的Runnable接口,接下来观察一下他们两个的关系

 

多线程开发的本质其实就是多个线程同时对一个资源的抢占,Thread主要描述的就是线程,资源是通过Runnable接口完成的

通过以上的分析,可以进行案例的编写,卖票程序

在卖票程序中 资源是需要多个线程共同的访问 应为票数的总共的概念 每个线程对象代表每一个买票的人

在这种情况下,资源是一定需要多个线程对象同时可以访问的

范例:实现卖票程序

class MyThread02 implements Runnable{

 

private int ticke=5; //一共有5张票

 

@Override

public void run() {

// TODO Auto-generated method stub

for(int i=0;i<100;i++) {

 

if(this.ticke>0) { //只要还有票就可以继续卖

 

System.out.println("卖票 ticke="+ticke--);

}

}

}

 

 

}

 

以上的程序实现卖票线程的主体类,接下来编写测试类启动卖票程序

public class RunnableDemo02 {

 

public static void main(String[] args) {

 

// 实例化的资源线程类

 

MyThread02 mt=new MyThread02();

 

//使用Thread类描述线程对象

 

new Thread(mt).start(); //代表一个线程访问

 

new Thread(mt).start(); //代表第二个线程访问

 

new Thread(mt).start(); //代表第三个线程访问

 

new Thread(mt).start(); //代表第四个线程访问

}

}

 

以上程序虽然创建了 4个线程,但是需要访问的资源只有一个Runnable接口的子类MyThread02 可以实现多个线程对统一资源的访问

回顾:上午内容

多线程的两种实现方式

实现Runnable接口 和继承Thread类

1 实现Runnable接口可以在程序中避免单继承局限,程序可以更好的扩展

2 实现Runnable接口可以更好实现资源共享的

3 继承Thread类存在单继承局限(硬伤)

4 在多线程中Thread类用于藐视一个个的线程对象

多线程在进程的基础上进行的进一步的划分,进程消失则线程一定消失,一个进程下面存在多个线程

下午的内容:

扩展:Lambda表达式

Lambda表达式是JDK1.8的新特性,利用此形式可以实现函数式编程(一个接口或者一个类只存在一个抽象方法) 前提下就可以简化编写方式

Scala 语言中就提供了函数式编程

在面上对象过程中,一直存在的问题,即使编写简单的代码,同样需要进行类的完整定义

例如 实现简单接口的应用

public interface IMessage {

 

// 只存在一个抽象方法

public void send(String str);

}

 

public class IMessageImpl implements IMessage{

 

@Override

public void send(String str) {

// TODO Auto-generated method stub

System.out.println("消息发送"+str);

}

 

 

}

 

public class IMessageTest {

 

public static void main(String[] args) {

 

IMessage msg=new IMessageImpl();

 

msg.send("hello");

}

}

从上面的代码分析,只是简单实现一个接口输出一句话的操作,结果需要编写3个类来完成,这样的操作太麻烦了

在使用lambda方法可以用一句话完成

范例:使用lamdba表达式完成以上功能

public class IMessageTest {

 

public static void main(String[] args) {

 

IMessage msg=(str)->{

 

System.out.println("发送消息"+str);

};

 

msg.send("hello");

}

}

 

发现使用lamdba表达式可以通过一句话就可以完成以上很麻烦的操作,但是函数式有一个前提在接口中或者父类中只存在一个抽象方法

把一个方法在生成的时候也可以使用注解 标明一个方法为函数式方法

例如:给一个接口定义函数式注解方便用户使用Lmbda表达式

@FunctionalInterface  //函数式接口

public interface IMessage {

 

// 只存在一个抽象方法

public void send(String str);

}

 

对于Lmbda表达式提供了几个格式如下:

方法没有参数: ()->{};

方法有参数:(参数1,参数2)->{};

如果现在只有一行语句:(参数1)->语句;

范例: 定义没有参数的方法

public class IMessageTest {

 

public static void main(String[] args) {

 

 

IMessage msg=()->{

 

System.out.println("hell world");

};

 

msg.send();

}

}

范例:定义一个有参数有返回值的方法

public class IMessageTest {

 

public static void main(String[] args) {

 

 

IMessage msg=(i1,i2)->{

 

return i1+i2;

};

 

int result=msg.add(10, 20);

 

System.out.println(result);

}

}

 

利用Lmbda可以简化在面向对象过程中的开发,但是前提是只有一个抽象方法的接口或者类才可以使用

扩展:注解

最早的开发,在进行文件配置的时候,主要都是通过配置文件来完成的,向框架技术最开始的时候全部都是使用*.xml配置文件来完成,方便代码和文件相分离方便维护

但是随着业务量的增大维护起来变得太麻烦了

在JDK 1.6之后推出了注解的概念

@

线程第三种实现方式 实现Callable接口

从传统的开发角度上来说实现一个多线程肯定使用Runnable接口,但是Runnable接口有一个缺点:没有返回值

在Jdk1.5之后推出了callable接口可以给一个线程执行完毕反回结果,接下来观察callableji接口的定义

Public interface callable<V>

在callable接口内设置了一个泛型操作,其实这个泛型就是规定了需要反回的数据类型

有这个泛型的支持就不会造成向下转型出现错误

范例:通过callable接口实现多线程

public class MyThread implements Callable<String>{

 

@Override

public String call() throws Exception {

// TODO Auto-generated method stub

 

//实现线程的业务方法 类似run方法

for(int i=0;i<10;i++) {

 

System.out.println("线程执行i="+i);

}

return "执行完毕"; //与run方法不同的是存在了返回值

 

 

}

 

}

范例:启动callable接口实现的多线程

package org.callable.demo;

 

import java.util.concurrent.ExecutionException;

import java.util.concurrent.FutureTask;

 

public class ThreadTest {

 

public static void main(String[] args) throws InterruptedException, ExecutionException {

 

FutureTask<String> task=new FutureTask<>(new MyThread());

 

new Thread(task).start();

 

//通过Thread启动线程之后 可以通过FutureTask存在get方法可以取得call()方法中的返回值

 

String result=task.get();

 

System.out.println(result);

 

}

}

 

面试题:

Callable 接口 与Runnable接口的区别?

Runnable接口没有返回值, Callable接口可以在执行多线程任务之后返回数据类型

线程的状态:(面试题)

1 任何一个线程对象都是通过Thread类进行封装,调用start()方法进入到就绪状态,就绪状态线程并没有执行

2 线程进入到就绪状态之后,等到资源调度,抢到资源的线程先执行,先执行的线程就进入到运行状态

3 线程在执行的过程可能会存在阻塞状态,当用户调用类似 sleep 或者wait 方法 会进入到阻塞状态

4 终止状态,当线程执行完毕,或者调用线程的停止的方法线程就会进入到终止状态

线程的常用操作方法:

所有线程的操作方法基本上都定义在Thread类中

1线程的命名与取得

线程的运行状态是不确定,如果在不确定的线程中操作一个线程只能依靠线程的名字,线程的名字是非常重要的概念,在Thread方法中提供有线程的命名与取得的方法

1 构造方法: public Thread(Stirng name);

2 public final String getName(); 取得线程的名称

3 public final void setName(String name); 设置线程名称

通过以上的三个方法可以完成线程的命名与取得,但是并不能通过以上方法取得线程当前的对象

Thread类中提供了

1 取得当前线程的方法 public static Thread currentThread();

范例:取得当前线程的名字

@Override

public void run() {

// TODO Auto-generated method stub

// 取得当前线程 并且取得当前线程的名字

 

System.out.println(Thread.currentThread().getName());

}

范例:为线程设置名称

public class RunnableDemo03 {

 

public static void main(String[] args) {

 

// 为线程命名

 

MyThread03 mt=new MyThread03();

 

Thread t1=new Thread(mt);

 

t1.setName("线程A");

 

t1.start();

 

System.out.println(Thread.currentThread().getName());

 

 

}

}

 

为线程设置名称和取得名称在以上的代码中 直接在main方法输出当前线程的名称输出为main说明main方法也是一个线程 在JVM启动的时候会启动两个线程一个是main方法另一个是GC

2 线程的休眠

public static native void sleep(long millis) throws InterruptedException;

如果现在希望一个程序跑的慢一点,或者说暂缓执行,可以使用Thread类中提供的sleep()方

完成

该方法需要的参数 long类型的毫秒时间

范例: 使用Sleep方法暂缓线程

@Override

public void run() {  //覆写Runnable接口中的run方法

// TODO Auto-generated method stub

//编写线程的功能

 

for(int i=0;i<30;i++) {

 

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

 

System.out.println(title+"="+i);

}

}

 

 

练习:用Lmbda表示实现线程的休眠

package org.runnable.demo;

 

public class RunnableDemo04 {

 

public static void main(String[] args) {

 

Runnable run=()->{

 

for(int i=0;i<10;i++) {

 

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

 

System.out.println("线程"+i);

}

 

};

 

new Thread(run).start();

}

}

 

3 线程的终断

在Thread类中里面提供了线程的终断的方法

 public static boolean interrupted()

通过以上方法可以让一个叫做正在休眠的线程终断休眠

 public static boolean interrupted()

以上方法可以判断一个线程是否已经被打断,已经被打断线程不能再次被打断

范例:线程的终断

package org.thread.demo;

 

public class ThreadDeme03 {

 

public static void main(String[] args) {

 

Thread thread=new Thread(()->{

 

 

System.out.println("薛哲同学憋了72个小时需要输出");

 

try {

Thread.sleep(100000); //输出100秒

System.out.println("我只想安静的蹲在这里抽一根烟");

} catch (InterruptedException e) {

// TODO Auto-generated catch block

System.out.println("谁也别来烦我");

e.printStackTrace();

}

 

});

 

thread.start(); //启动线程  开始输出

 

if(!thread.isInterrupted()) { //判断是这个线程是否被打断过 如果为true 没有

 

System.out.println("李哲 嘿嘿一乐 想要偷偷去打断一下");

thread.interrupt();  //打断线程

}

 

 

}

}

总结:

Lmbda 表达式作为jdk.18的新特性需要大家熟练使用

线程的状态:概念 作为面试题一定要熟记

线程的常用方法: Sleep interrupt 这些方法都给一个线程变换状态

作业:

1 创建一个线程每次输出当前的系统时间

2 创建两个线程分别输出 1000 你好 再见

3 设计四个线程 两个进行加 两个进行减操作

分别用 传统方式 和 Lmbda方式完成

4 线程的强制执行

5 线程的礼让

6 线程的优先级

以上几个方法都定义在Thread类中

  相关解决方案