Java多线程编程

一、进程和线程

  • 进程:进程是资源分配的基本单位。是程序执行的实体

  • 线程:线程是cpu调度的基本单位。进程里面包含多个线程。

关系如图所示

整个电脑管家是一个进程。

线程是电脑管家里面的某个功能。

上述关系可以概述为,进程是一个软件,线程是软件里某个特定的功能。

二、线程的创建

2.1 继承Thread类

/**
 * 创建线程的第一种方式
 * 1.继承Thread类
 * 2.重写run方法
 * 3.创建线程
 * 4.启动线程
 *
 */
public class Threaddemo extends Thread{
    @Override
    public void run() {
        //编写线程的业务,如打印20次线程的名字
        for(int i = 0; i < 20; i ++){
            System.out.println(("我是"+getName()));
        }

    }

    public static void main(String[] args) {
        Threaddemo t1 = new Threaddemo();
        Threaddemo t2 = new Threaddemo();
//        System.out.println(t1.getName());  Thread-0
//        System.out.println(t2.getName());  Thread-1
        t1.start();
        t2.start();
    }
}

看执行结果,是线程0和线程1交替执行的(并发执行)。

  • 并发: 统一时间间隔内,多个线程同时执行(宏观上是同时,起始是交替执行)。

  • 并行:统一时刻,多个线程同时执行。(多个cpu 执行多个线程)。

2.2 实现Runnable类


/**
 * 创建线程的第二种方式
 * 1.自定义类实现Runnable类
 * 2.重写run方法
 * 3.创建自定义类
 * 4.创建Thread类 并传参,参数是自定义类  现在才是线程
 * 5.启动线程
 *
 */
public class Threaddemo1 implements  Runnable {
    @Override
    public void run() {
        //该线程业务
        //打印20次该线程的名字
        /*
        由于该类是继承Runnable类Runnable还不是线程,需要将该类作为参数给Thread类,才能成为线程
        要先获取线程的名字,要先获得线程。
        * */
        Thread thread = Thread.currentThread();
        for(int i = 0; i < 20; i ++){
            System.out.println("我是"+thread.getName());
        }
    }

    public static void main(String[] args) {
        Threaddemo1 threaddemo1 = new Threaddemo1();
        Thread t1 = new Thread(threaddemo1);
        Thread t2 = new Thread(threaddemo1);
        t1.start();
        t2.start();
    }
}

执行如下

2.3 利用Callable接口和Future接口

/**
 * 实现线程的第三种方式
 *      z
 * 利用 Callable接口和 Future 接口方式 实现
 *      这种方式可以获取线程的返回结果,前两种的返回值都是void。
 * 1.自定义类实现Callable接口 ,指定返回的类型给泛型,重写Call方法(业务)
 * 2.创建自定义类对象
 * 2.创建FutureTask 对象(管理call的返回结果)
 */
public class Threaddemo2 implements Callable<Integer> {
    /**
     * 1到100的和
     * @return
     * @throws Exception
     */
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Threaddemo2 threaddemo2 = new Threaddemo2();
        FutureTask<Integer> ft = new FutureTask<>(threaddemo2);
        Thread t1 = new Thread(ft);
        t1.start();
        Integer i = ft.get();
        System.out.println(i);
    }
}
  • 创建一个 Threaddemo2 的实例 threaddemo2

  • 使用 FutureTask 包装 Callable 对象。FutureTask 是一个适配器类,它实现了 RunnableFuture 接口,可以将 Callable 对象转换为线程任务。

  • 创建一个线程 t1,并将 FutureTask 对象作为线程任务。

  • 调用 t1.start() 启动线程。

  • 调用 ft.get() 方法获取线程的返回值。get() 方法会阻塞当前线程,直到 Callablecall() 方法执行完成并返回结果。

  • 最后打印返回值。

2.4 区别

1. 继承Thread

特点

  • 直接继承:通过继承Thread类来创建线程。Thread类本身实现了Runnable接口。

  • 简单直接:适合简单的线程任务,不需要额外的接口实现。

  • 限制:Java不支持多继承,因此如果继承了Thread类,就无法再继承其他类。

适用场景

  • 适合简单的线程任务,且不需要返回值。

  • 不需要与其他类继承结构冲突的场景。

2. 实现Runnable接口

特点

  • 接口实现:通过实现Runnable接口来定义线程任务,然后将Runnable对象传递给Thread类。

  • 灵活性高:可以避免单继承的限制,允许类继承其他类。

  • 无返回值Runnable接口的run()方法没有返回值,无法获取线程的执行结果。

适用场景

  • 适合需要与其他类继承结构兼容的场景。

  • 适合不需要获取线程执行结果的场景。

3. 实现Callable接口并结合Future接口

特点

  • 接口实现:通过实现Callable接口定义线程任务,Callable接口的call()方法可以返回值。

  • 有返回值call()方法可以抛出异常,并且可以返回执行结果。

  • 复杂度较高:需要结合FutureTask类和Future接口来管理线程任务和获取结果。

  • 线程池支持:更适合与线程池(ExecutorService)结合使用,提高资源利用率。

适用场景

  • 适合需要获取线程执行结果的场景。

  • 适合与线程池结合使用,提高线程管理效率。

  • 适合复杂的多线程任务,需要处理异常和返回值。

特性/方式继承Thread实现Runnable接口实现Callable接口 + Future
实现方式继承Thread实现Runnable接口实现Callable接口,结合FutureTask
是否支持多继承不支持(单继承限制)支持(可以继承其他类)支持(可以继承其他类)
是否有返回值无返回值无返回值有返回值(通过Future获取)
是否支持异常处理不支持(run()不能抛出异常)不支持(run()不能抛出异常)支持(call()可以抛出异常)
是否适合线程池不适合(直接使用Thread适合(可以与线程池结合)非常适合(与线程池结合效率更高)
复杂度最简单简单较复杂(需要结合FutureTask
适用场景简单任务、不需要返回值简单任务、需要与其他类继承结构兼容复杂任务、需要返回值、需要异常处理

三、线程的成员方法

3.1 线程的优先级

tips:线程的调度有两种方式,一种是非抢占式调度,线程轮流获取cpu执行。另一种是抢占式调度,cpu在执行某个线程时,还未执行完有更高优先级的线程回抢占cpu。

  • setPriority(int x) x的范围时1~10,10优先级最大。默认优先级是5

3.2 守护线程

  • 定义:守护线程是为用户线程服务的后台线程。当程序中所有用户线程都执行完毕后,守护线程会自动结束。

  • 特点

    • 后台运行:守护线程在后台执行,不会直接影响用户界面。

    • 生命周期依赖:守护线程的生命周期依赖于用户线程,当所有用户线程终止时,守护线程也会自动终止。

    • 低优先级:守护线程通常具有较低的优先级,以确保用户线程能够优先获得 CPU 时间。

    • 不可靠性:守护线程的执行可能不完整,如果用户线程突然终止,守护线程可能无法完成其任务。

创建方法

在 Java 中,可以通过调用线程对象的 setDaemon(true) 方法将线程设置为守护线程

Thread daemonThread = new Thread(() -> {
    while (true) {
        System.out.println("Daemon thread is running");
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();

应用场景

守护线程通常用于执行一些后台任务,常见的应用场景包括:

  • 垃圾回收:Java 虚拟机的垃圾回收线程是一个典型的守护线程。

  • 日志记录:日志记录线程可以作为守护线程运行,定期将日志信息写入文件。

  • 资源清理:资源清理线程可以作为守护线程运行,定期清理不再使用的资源。

  • 定时任务:例如定期清理过期数据等。

3.3 出让线程|礼让线程(了解)

在多线程编程中,“出让线程”或“礼让线程”通常指的是线程的**让步(Yield)**机制。线程让步是指当前线程主动放弃当前的执行机会,让其他线程有机会运行。以下是对线程让步的详细解释:

线程让步的概念

线程让步是通过调用线程的 yield() 方法实现的。当一个线程调用 yield() 方法时,它会主动放弃当前的执行机会,让其他线程有机会运行。yield() 方法的作用是暂停当前线程的执行,并将线程的执行状态从“运行态”(Running)变为“就绪态”(Runnable),等待再次被调度。

线程让步的作用

线程让步的主要作用是优化线程调度,使线程的执行更加公平。在某些情况下,线程可能长时间占用 CPU 资源,导致其他线程无法及时运行。通过调用 yield() 方法,可以让线程主动放弃当前的执行机会,让其他线程有机会运行,从而提高系统的整体性能。

线程让步的实现

在 Java 中,可以通过调用 Thread.yield() 方法实现线程让步。

public class YieldExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread 1: " + i);
                if (i % 2 == 0) {
                    Thread.yield(); // 线程让步
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread 2: " + i);
            }
        });

        t1.start();
        t2.start();
    }
}

在这个例子中,线程 t1 在每次循环时都会调用 Thread.yield() 方法,主动放弃当前的执行机会,让线程 t2 有机会运行。

线程让步的注意事项

  • 线程让步是提示性的yield() 方法只是向线程调度器发出一个提示,表示当前线程愿意放弃执行机会,但线程调度器可能会忽略这个提示。

  • 线程让步不会阻塞线程:调用 yield() 方法后,线程不会进入阻塞状态,而是进入就绪状态,等待再次被调度。

  • 线程让步的优先级:线程让步不会改变线程的优先级,线程调度器仍然会根据线程的优先级进行调度。

3.4 插入线程(了解)

在多线程编程中,join() 方法是一种用于线程同步的机制,它允许一个线程等待另一个线程完成其执行。虽然“插入线程”并不是一个标准术语,但从功能上看,join() 方法确实可以在一定程度上实现线程之间的“插入”或“等待”行为,即让当前线程暂停执行,直到指定的线程完成运行。

join() 方法的作用

join() 方法的主要作用是让当前线程(调用线程)等待目标线程(被调用线程)完成其执行。换句话说,调用线程会暂停执行,直到目标线程运行结束。

join() 方法的使用

在 Java 中,join() 方法是 Thread 类的一个实例方法。它有以下几种重载形式:

  1. void join() throws InterruptedException:等待目标线程完成,不设置超时时间。

  2. void join(long millis) throws InterruptedException:等待目标线程完成,最多等待指定的毫秒数。

  3. void join(long millis, int nanos) throws InterruptedException:等待目标线程完成,最多等待指定的毫秒数和纳秒数。

示例代码

以下是一个简单的示例,展示如何使用 join() 方法:

public class JoinExample {
    public static void main(String[] args) {
        // 创建线程 t1
        Thread t1 = new Thread(() -> {
            System.out.println("Thread t1 is running");
            try {
                Thread.sleep(2000); // 模拟线程 t1 的工作时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread t1 is done");
        });

        // 创建线程 t2
        Thread t2 = new Thread(() -> {
            System.out.println("Thread t2 is running");
            try {
                Thread.sleep(1000); // 模拟线程 t2 的工作时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread t2 is done");
        });

        // 启动线程 t1 和 t2
        t1.start();
        t2.start();

        try {
            // 等待线程 t1 完成
            System.out.println("Main thread is waiting for t1 to finish");
            t1.join(); // 主线程暂停执行,直到线程 t1 完成
            System.out.println("Thread t1 has finished");

            // 等待线程 t2 完成
            System.out.println("Main thread is waiting for t2 to finish");
            t2.join(); // 主线程暂停执行,直到线程 t2 完成
            System.out.println("Thread t2 has finished");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main thread is done");
    }
}

输出结果

假设线程的启动和执行顺序符合预期,程序的输出可能是:

Thread t1 is running
Thread t2 is running
Main thread is waiting for t1 to finish
Thread t1 is done
Thread t2 is done
Main thread is waiting for t2 to finish
Main thread is done

四、线程的生命周期

即线程的状态

  • 新建状态(NEW)

    • 创建后尚未启动。

  • 就绪状态(Runnable)

    • Thread.start()后。线程处于就绪态。此时,线程已经做好了执行的准备,但还没有获得CPU的执行权,处于等待CPU分配资源(时间片)的阶段。

  • 运行状态(Running)

    • 当就绪的线程被调度并获得CPU资源时,线程进入运行状态,开始执行run()方法中的任务。在这个过程中如果run()执行完毕,线程就回死亡变成垃圾。如果被更高优先级的线程抢夺处理机资源回回到就绪态,等待着cpu分配。

  • 阻塞状态(Blocked)

    • 线程在运行过程中,线程sleep或者其他阻塞方式(如等待I/O操作完成、等待锁)。回导致线程阻塞。sleep()方法结束或其他阻塞方式结束,回到就绪态。

    • 阻塞分类

      • 等待阻塞:线程调用wait()方法或join()方法,等待其他线程执行完毕或超时。

      • 同步阻塞:线程在获取同步锁失败时,会进入同步阻塞状态。

      • 其他阻塞:如调用sleep()方法、发出I/O请求等。

  • 死亡状态(Dead)

    • 线程run()方法执行完毕,或者异常结束,线程进入死亡状态。此时,线程的生命周期结束,线程所占用的资源被释放。。

五、Synchronized 锁、Lock锁

5.1Synchronized 锁

  • 锁默认打开,有一个线程进去了,所自动关闭

  • 锁里面的代码全部执行完毕,线程出来,锁自动打开。

使用 synchronized关键字的方式:

  1. 同步方法

    • 在方法声明时使用 synchronized 关键字,这样该方法在同一时刻只能被一个线程访问。

    • 可以是实例方法或静态方法。如果是实例方法,则锁定的是调用该方法的对象实例;如果是静态方法,则锁定的是该类的 Class 对象。

public synchronized void method() {  
    // 同步代码  
}  

public static synchronized void staticMethod() {  
    // 同步代码  
}

  2.同步代码块

  • synchronized 关键字也可以用于代码块,这样可以在方法内部指定更小的同步区域,从而提高并发性。

  • 同步代码块需要一个引用类型的锁对象。

public void method() {  
    synchronized(lockObject) {  
        // 同步代码  
    }  
}

注意事项

  • 过度使用 synchronized 可能会导致性能下降,因为线程在访问同步区域时可能会发生阻塞。

  • 锁对象的选择很重要,通常应该选择私有的、不会被外部访问的对象作为锁对象,以避免不必要的线程阻塞。

    • 比如该类的字节码问文件。

  • 在设计多线程程序时,应该尽量减少同步区域的大小,以提高程序的并发性。

使用锁解决买票的问题。

5.2Lock锁

sychtonized锁,自动上锁自动解锁。作用在方法或代码块上。在jdk5中出现了Lock锁。

Lock是锁的接口,其实现ReentrantLock。可以调用 lock(),和unlock()。手动上锁,手动解锁。

  • 使用lock锁,为了避免进入死锁状态,一般都在finnally解锁

在Java中,Lock接口及其实现类(如ReentrantLockReentrantReadWriteLockStampedLock)是JUC(Java并发包)中锁机制的核心。以下是ReentrantLock的基本使用示例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    private Lock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public int getCount() {
        return count;
    }
}

在这个例子中,increment()方法对共享变量count进行加1操作。通过lock.lock()获取锁,确保同一时间只有一个线程能执行该方法。操作完成后,在finally块中释放锁,以便其他线程可以获取锁并继续执行。

Lock锁的特点

  • 可重入性:同一个线程可以多次获取同一把锁。

  • 公平性:可以设置为公平锁或非公平锁,公平锁会按照线程请求锁的顺序来分配锁。

  • 灵活的锁获取和释放:与synchronized关键字不同,Lock锁需要手动创建并管理锁对象,这提供了更高的可扩展性和可定制性

5.3Lock锁与synchronized的区别

​​​​​

  • 锁的获取和释放synchronized是Java内置关键字,在JVM层面实现,会自动释放锁;而Lock锁是基于Java类库实现的,需要手动释放锁,否则容易造成死锁。

  • 功能特性Lock锁提供了更多的功能,如可判断是否获取到锁、可中断等待、支持公平锁等。

  • 性能:在某些场景下,Lock锁的性能可能略低于synchronized,但它的灵活性和功能更强大。

六、等待和唤醒

在Java中,wait() 方法是一个用于线程间通信的重要方法,当一个线程执行到某个对象的 wait() 方法时,它会释放这个对象锁并进入等待(阻塞)状态,直到其他线程调用该对象的 notify()notifyAll() 方法来唤醒它。

  • wait() 方法必须在同步方法或同步块中调用,因为调用 wait() 会立即释放当前线程持有的锁。并且让线程出入阻塞状态。

  • 调用 wait()notify()notifyAll() 方法时,必须持有对象锁。一般都是用锁调

  • 使用 notify()notifyAll() 方法唤醒等待的线程时,并不会立即释放锁,而是等待当前同步代码块执行完毕后才释放锁。

  • notifyAll() 方法会唤醒所有等待该对象的线程,而 notify() 方法只会唤醒其中一个等待的线程(具体哪个线程是不确定的)。

用wait和notifyall()解决生产者消费者问题。

public class Desk {
    //0表示 无   1表示有
    static int foodFlag = 0;
    //锁对象
    static  Object obj= new Object();

    //消费者最多吃10碗
    static int  cont = 0;
}
/**
 * 吃货
 *  1.吃货获取到cpu
 *  2.判断桌子上有没有食物
 *  3.如果没有 就wait,等待厨师喊他。
 *  4.如果有就吃,吃完通知厨师在做。
 */
public class Consumer extends Thread {
    @Override
    public void run() {
        while (true){
            synchronized (Desk.obj){
                System.out.println("吃货来了");
                if(Desk.cont < 10){
                    if(Desk.foodFlag == 1){
                        Desk.cont++;
                        System.out.println("吃货吃饭-" + Desk.cont);

                        Desk.foodFlag = 0;
                        //唤醒厨师
                        Desk.obj.notifyAll();
                    }else {
                        try {
                            //等待厨师做,就是让出cpu执行。 wait()会立刻释放锁,并让线程处于等待状态。
                            //这样厨师就能拿到锁,和cpu的使用
                            System.out.println("吃货等待,让出cpu和锁");
                            Desk.obj.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }else {
                    break;
                }
            }
        }
    }
}

/** 厨师
 *  1.判断桌子上有没有食物
 *  2.没有食物,就做,做完通知吃货(因为吃货因为桌子上没有食物还在等待着)
 *  3.有食物,就等待着。(等待吃货告知没有食物)
 *
 */
public class Producer extends Thread{
    @Override
    public void run() {
        while (true){
            if(Desk.cont < 10){
                synchronized (Desk.obj){
                    System.out.println("厨师来了");
                    if(Desk.foodFlag == 0 ){ //如果没有食物
                        System.out.println("厨师做食物");
                        Desk.foodFlag = 1;
                        //唤醒处于等待状态的吃货
                        Desk.obj.notifyAll();
                    }else { //如果有食物
                        try {
                            System.out.println("生产者等待,让出锁和cpu");
                            Desk.obj.wait();//让出锁和cpu,让吃货拿到。
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }

            }else {
                break;
            }

        }

    }
}

/**
 * Producer-Consumer 生产者消费者问题
 *      一个吃货,一个厨师,一个桌子。 吃货吃10碗面 厨师做10碗面
 *
 */
public class MyTest {
    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        Producer producer = new Producer();
        consumer.start();
        producer.start();
    }
}

七、阻塞队列

阻塞队列(Blocking Queue)是一种特殊的队列,它支持两个附加的操作:在队列为空时,获取元素的线程会等待队列变为非空;在队列已满时,存储元素的线程会等待队列可用。这种队列通常用于生产者-消费者问题中,以在并发编程中实现线程间的同步和通信。

Java的java.util.concurrent包中提供了多种阻塞队列的实现,包括但不限于:

  • ArrayBlockingQueue:基于数组结构的有界阻塞队列。

  • LinkedBlockingQueue:基于链表结构的阻塞队列,如果创建时没有指定容量,则默认为Integer.MAX_VALUE,即无界队列。

八、线程的完整状态

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值