文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java多线程的具体介绍与使用笔记小结

2024-04-02 19:55

关注

在这里插入图片描述

一、基本概念:线程、进程

1.1、进程与线程的具体介绍

线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。

进程(process),是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期

1.2、对于CPU而言的理解

单核CPU和多核CPU的理解

并行与并发

1.3、为什么要使用多线程

以单核CPU 为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?

多线程程序的优点:

二、线程的创建与使用

2.1、如何去创建和启动一个线程

每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作的,经常把 run() 方法的主体称为线程体通过该 Thread 对象的 start() 方法来启动这个线程,而非直接调用 run()

2.2、Thread类的具体分析

构造器:

Thread():创建新的 Thread 对象
Thread(String threadname):创建线程并指定线程实例名
Thread(Runnable target):指定创建线程的目标对象,它实现了 Runnable 接口中的 run 方法
Thread(Runnable target, String name):创建新的 Thread 对象
创建线程的两种方式:
JDK1.5 之前创建新执行线程有两种方法:
继承 Thread 类的方式
实现 Runnable 接口的方式
方式一:继承 Thread 类
定义子类继承 Thread 类。
子类中重写 Thread 类中的 run 方法。
创建 Thread 子类对象,即创建了线程对象。
调用线程对象 start 方法:启动线程,调用 run 方法。

代码演示:


* 多线程的创建,方式一:继承于Thread类
 * 1. 创建一个继承于Thread类的子类
 * 2. 重写Thread类的run() --> 将此线程执行的操作声明在run()中
 * 3. 创建Thread类的子类的对象
 * 4. 通过此对象调用start()
 * 例子:遍历100以内的所有的偶数

//1、创建一个继承于Thread类的子类
class Thread01 extends Thread {
    //2、重写Thread类的run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //3、创建Thread类的子类的对象
        Thread01 thread01 = new Thread01();
        //4.通过此对象调用start()
        thread01.start();

        //创建第二个线程
        Thread01 thread02 = new Thread01();

        //注意:这里我们不能直接手动调用 run()方法
        //thread01.run();

        //注意:当我们再次调用start()时会直接报错:IllegalThreadStateException,所以一个线程只能用一次
        //thread01.start();

        thread02.start();


        //当前操作仍然是在main线程中执行
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i + "主线程被执行了");
            }

        }
    }
}

方式二:实现 Runnable 接口

2.3、两种实现方式的联系与区别

联系:

Thread 内部其实也是实现了 Runnable 接口

在这里插入图片描述

区别:

实现方式的好处:

2.4、注意事项:

2.5、代码测试

需求:创建两个分线程,让其中一个线程输出1-100之间的偶数,另一个线程输出1-100之间的奇数。


public class ThreadTest01 {
    public static void main(String[] args) {

        //线程一:偶数
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + "线程一偶数的执行数:" + i);
                    }
                }
            }
        }.start();


        //线程二:奇数
        new Thread() {
            @Override
            public void run() {

                for (int i = 0; i < 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + "线程二奇数的执行数:" + i);
                    }
                }
            }
        }.start();
    }
}

2.6、Thread类的相关方法

void start(): 启动线程,并执行对象的 run() 方法

run(): 线程在被调度时执行的操作String

getName(): 返回线程的名称

void setName(String name): 设置该线程名称

static Thread currentThread(): 返回当前线程。在 Thread 子类中就是 this,通常用于主线程和 Runnable 实现类static void yield(): 线程让步

暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程若队列中没有同优先级的线程,忽略此方法

join() : 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止

低优先级的线程也可以获得执行

static void sleep(long millis):(指定时间:毫秒)

令当前活动线程在指定时间段内放弃对 CPU 控制,使其他线程有机会被执行,时间到后重排队。

抛出 InterruptedException 异常

stop(): 强制线程生命期结束,不推荐使用

boolean isAlive(): 返回 boolean,判断线程是否还活着

在这里插入图片描述

在这里插入图片描述

2.7、测试以上方法:


* @description: 多线程具体方法的使用
 * @date 2021/4/16 18:32
 */


class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);

                //设置线程睡眠时间
               
            }
            //当满足当前条件时,终止线程
            


        }

    }
}

public class ThreadMethodTest {
    public static void main(String[] args) {
        MyThread thread = new MyThread();

        thread.start();
        //给主线程命名
        thread.setName("我是线程一号");
        //将分线程优先级设置成最大
        thread.setPriority(Thread.MAX_PRIORITY);

        Thread.currentThread().setName("我是主线程");
        //将主线程的优先级设置成最小
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
            }
            //join()表示:线程A中调用线程B的join()方法,那么此时线程A进入阻塞状态,直到线程B完全执行完以后,线程A才
            //结束阻塞状态。
            

        }
        //isAlive():判断当前线程是否存活
//        System.out.println("当前线程是否存活:" + thread.isAlive());


    }
}

结果:

使用 sleep() 方法时的结果
在这里插入图片描述
使用 Join方法时的结果
在这里插入图片描述

三、线程的生命周期

JDK中用Thread.State类定义了线程的几种状态:

要想实现多线程,必须在主线程中创建新的线程对象。Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

新建: 当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态

就绪: 处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件,只是没分配到 CPU 资源

运行: 当就绪的线程被调度并获得 CPU 资源时,便进入运行状态, run() 方法定义了线程的操作和功能

阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态

死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

生命周期结构图

在这里插入图片描述

四、线程的同步

4.1、为什么要线程同步

4.2、举例说明:

创建三个窗口卖票,总票数为100张.使用继承Thread类的方式


class Window extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(getName() + "买票编码号为:" + ticket);
                ticket--;
            } else {
                break;
            }
        }
    }
}

public class TicketWindowTest {
    public static void main(String[] args) {
        //创建线程
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        //设置线程名字
        w1.setName("窗口一:");
        w2.setName("窗口二:");
        w3.setName("窗口三:");

        //开启线程
        w1.start();
        w2.start();
        w3.start();
    }
}

正常情况下的效果:

在这里插入图片描述

不正常的情况下的效果:

在这里插入图片描述

上段代码出现的漏洞:

问题: 卖票过程中,出现了重票、错票,出现了线程的安全问题

原因: 当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票

解决: 当一个线程 a 在操作 ticket 的时候,其他线程不能参与进来。直到线程 a 操作完 ticket 时,其他线程才可以开始操作 ticket。这种情况即使线程a出现了阻塞,也不能被改变

在这里插入图片描述

4.3、Synchronized的使用方法

Java对于多线程的安全问题提供了专业的解决方式:同步机制


 1、同步代码块:
    synchronized (对象){
          // 需要被同步的代码;
    }

2、synchronized还可以放在方法声明中,表示整个方法为同步方法。
	例如:
    public synchronized void show (String name){ 
            ….
   }

4.3、 同步机制中的锁

同步锁机制:

在《Thinking in Java》中,是这么说的: 对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

synchronized的锁是什么?

注意:

4.4、同步的范围

如何找问题,即代码是否存在线程安全?(非常重要)

如何解决呢?(非常重要)

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中

注意:

对于购票代码的 bug 改进


class Windows02 implements Runnable {

    private int ticke = 100;

    @Override
    public void run() {
        while (true) {
            //使用同步代码方式来解决线程安全问题,this:表示当前对象:【Windows02】
//            synchronized (this) {
//                if (ticke > 0) {
//                    try {
//                        Thread.sleep(100);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//                    System.out.println(Thread.currentThread().getName() + "购票号码为:" + ticke);
//                    ticke--;
//                } else {
//                    break;
//                }
//            }

            show();
        }
    }

    //这里我们直接使用【同步方法】的方式来处理线程安全问题
    //在方法中加入:synchronized的效果等同上面的this,因为指代的都是当前对象,只是在同步方法中帮我们做了隐试操作。
    private synchronized void show() {
        if (ticke > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "购票号码为:" + ticke);
            ticke--;
        }
    }
}

public class TickeWindowTest02 {
    public static void main(String[] args) {
        Windows02 windows = new Windows02();

        Thread thread = new Thread(windows);
        Thread thread01 = new Thread(windows);
        Thread thread02 = new Thread(windows);

        thread.setName("窗口一:");
        thread01.setName("窗口二:");
        thread02.setName("窗口三:");

        thread.start();
        thread01.start();
        thread02.start();


    }
}

测试结果:

在这里插入图片描述

4.5、释放锁的操作

4.6、不释放锁的操作

应尽量避免使用 suspend() 和 resume() 来控制线程

4.7、线程的死锁的问题

1、 什么事死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

代码演示:


//死锁的演示
class A {
    public synchronized void foo(B b) { //同步监视器:A类的对象:a
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 进入了A实例的foo方法"); // ①
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 企图调用B实例的last方法"); // ③
        b.last();
    }

    public synchronized void last() {//同步监视器:A类的对象:a
        System.out.println("进入了A类的last方法内部");
    }
}

class B {
    public synchronized void bar(A a) {//同步监视器:b
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 进入了B实例的bar方法"); // ②
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 企图调用A实例的last方法"); // ④
        a.last();
    }

    public synchronized void last() {//同步监视器:b
        System.out.println("进入了B类的last方法内部");
    }
}

public class DeadLock implements Runnable {
    A a = new A();
    B b = new B();

    public void init() {
        Thread.currentThread().setName("主线程");
        // 调用a对象的foo方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }

    public void run() {
        Thread.currentThread().setName("副线程");
        // 调用b对象的bar方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }

    public static void main(String[] args) {
        DeadLock dl = new DeadLock();
        new Thread(dl).start();
        dl.init();
    }
}

测试结果:概率性的出现

在这里插入图片描述

2、解决方法

3、什么是Lock锁

4、具体如何使用:

在这里插入图片描述

5、synchronized 与 Lock 锁有何区别

Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了作用域自动释放

Lock 只有代码块锁,synchronized 有代码块锁和方法锁

使用 Lock 锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序:

Lock →同步代码块(已经进入了方法体,分配了相应资源) → 同步方法(在方法体之外)

五、线程的通信

5.1、方法介绍与注意事项:

wait() 与 notify() 和 notifyAll()

wait(): 令当前线程挂起并放弃 CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用 notify()或notifyAll() 方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。

notify(): 唤醒正在排队等待同步资源的线程中优先级最高者结束等待

notifyAll (): 唤醒正在排队等待资源的所有线程结束等待.

注意事项:

这三个方法只有在 synchronized 方法或 synchronized 代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException 异常。

因为这三个方法必须有锁对象调用,而任意对象都可以作为 synchronized 的同步锁,因此这三个方法只能在 Object 类中声明。

sleep() 和 wait() 有何不同之处:

相同点: 一旦执行方法,都可以使得当前的线程进入阻塞状态。

不同点:

两个方法声明的位置不同: Thread 类中声明 sleep() , Object 类中声明 wait()

调用的要求不同: sleep() 可以在任何需要的场景下调用。 wait() 必须使用在同步代码块或同步方法中

关于是否释放同步监视器: 如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,wait()会释放锁。

案例一:

使用两个线程打印 1-100。线程1, 线程2 交替打印


class Number implements Runnable {
    private int number = 1;
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                //因为现在使用的是当前对象,所以前面省略this.
                //如果使用的是其他对象,那么就用对象.的方式去调用该方法
                notify();
                if (number <= 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " :" + "打印数为" + number);
                    number++;
                    try {
                        //调用该方法时,线程进入阻塞状态
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread thread = new Thread(number);
        Thread thread01 = new Thread(number);
        thread.setName("线程一");
        thread01.setName("线程二");

        thread.start();
        thread01.start();
    }
}

执行结果:

在这里插入图片描述

六、JDK5.0新增的线程创建方式

新增方式一:实现Callable接口

与使用 Runnable 相比, Callable 功能更强大些

相比 run() 方法,可以有返回值

方法可以抛出异常

支持泛型的返回值需要

借助 FutureTask 类,比如获取返回结果

可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
FutrueTask 是 Futrue 接口的唯一的实现类
FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值

没使用线程池: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
使用线程池后: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用,类似生活中的公共交通工具。

提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
corePoolSize: 核心池的大小
maximumPoolSize: 最大线程数
keepAliveTime: 线程没有任务时最多保持多长时间后会终止

JDK 5.0 起提供了线程池相关 API:ExecutorService 和 Executors
ExecutorService: 真正的线程池接口。常见子类 ThreadPoolExecutor
void execute(Runnable command) : 执行任务命令,没有返回值,一般用来执行 Runnable
Future submit(Callable task): 执行任务,有返回值,一般又来执行 Callable

Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n): 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() : 创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n): 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

创建线程的方式三:使用 Callable 接口


//1.创建一个实现Callable的实现类
class NumThread implements Callable<Integer> {
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}


public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask<Integer> futureTask = new FutureTask<Integer>(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Integer sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

创建线程方式四:使用线程池技术


class NumberThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();


        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

//        service.submit(Callable callable);//适合使用于Callable
        //3.关闭连接池
        service.shutdown();
    }

}

到此这篇关于Java多线程的具体介绍与使用笔记小结的文章就介绍到这了,更多相关java多线程介绍与使用内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯