阅读 1476

Java多线程

这是我参与8月更文挑战的第31天,活动详情查看:8月更文挑战

1、程序、进程、线程

1.1 程序

为完成某种特定任务,用某种语言编写的一组指令的集合,它是一段静态的代码

1.2 进程

是程序的一次执行过程,或是正在运行的一个程序,动态的过程

创建-存在-消亡

1.3 线程

是进程的一部分,是程序内部的一条执行路径

每个线程拥有独立的运行栈和程序计数器

进程是程序运行和资源分配的基本单位;

线程是 CPU调度 和 分派 的能够独立运行的基本单位;

并行、并发

  • 并行:多个 CPU 同时执行多个任务;比如:多个人同时做不同事
  • 并发:一个 CPU “同时”(采用时间片机制)执行多个任务,操作同一个资源;比如:秒杀、多个人做同一件事

2、Thread

创建线程方式 1:

​ 继承 Thread 类,重写 run()方法;

  • 不能直接调用run()方法启动线程,这样做只是调用了一个类的普通方法;
  • 不可以再让已经start()的线程,再次调用start()方法,会报异常IllegalThreadStateException

2.1 Thread 常用方法

方法名方法作用
void start()此线程开始执行,Java虚拟机调用此线程的run方法
void run()线程的执行内容
String getName()返回此线程的名称
void setName(String name)设置此线程的名称为参数 name
int getPriority()返回此线程的优先级
void setPriority(int newPriority)更改线程优先等级
static Thread currentThread()返回对当前正在执行的线程对象的引用。
static void sleep(long millis)使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
void yield()当前线程释放 CPU 的执行权(也可能立马又继续
执行了)
void join()在线程 A 中调用线程 B.join(),此时线程 A 进入阻塞
状态,直到线程 B 完全执行完以后,线程 A 才结束阻塞状态
void stop()(已弃用)停止线

2.2 线程的调度

2.2.1 调度策略

时间片:抢占式的,根据线程优先级抢占 CPU 的执行权,高优先级的更容易获得;同优先级线程,先进先出队列。

2.2.2 优先等级

  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIORITY:5(默认)

使用方法:

  • void setPriority(int priority):设置线程优先级
  • int getPriority():获取线程优先级

2.2.3 注意

  • 线程创建时继承父类线程优先级;
  • 低优先级只是获取 CPU 调度的概率低,并不一定是高优先级线程之后才被调用的。

2.3 线程的分类

  • 守护线程:服务于 用户线程(主线程),如:Java 垃圾回收;
    • 若 JVM 中都是守护线程,则当前 JVM 将退出;
  • 用户线程(主线程)
    • start()方法前调用 thread.setDaemon(true) 可以将一个 用户线程 变成一个 守护线程。

3、线程的同步机制

1、同步代码块

synchronized(同步监视器){
    // 需要被同步的代码:线程对共享数据操作的语句
}
复制代码
  • 操作共享数据的代码,即为需要被同步的代码(不能包含代码多了,也不能包含代码少了);
  • 共享数据:多个线程公共操作的变量;
  • 同步监视器,俗称。任何一个类的对象,都可以充当锁;但是多个线程必须共用同一把锁
  • Thread 继承类,一般使用当前类名.class作为同步监视器;
  • Runnable接口实现类,可以考虑使用this充当同步监视器

2、同步方法

如果操作共享数据的代码完整的声明在一个方法中,不妨使用声明整个方法同步

  • 同步方法仍然设计到同步监视器,只是不需要我们显式的声明;
  • 非静态的同步方法,同步监视器是:this;
  • 静态的同步方法,同步监视器是:当前类本身;

3、Lock锁

java.util.concurrent.locks.lock

实现类:

​ ReentrantLock、

​ ReentrantReadWriteLock.ReaLock、

​ ReentrantReadWriteLock.WriteLock

  • JDK 5.0 开始,java 提供了更强大的线程同步机制,:通过显式定义同步锁对象来实现。同步锁使用Lock对象充当;

  • java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具;

    提供了对共享资源的独立访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象;

  • ReentrantLock类实现了Lock,它拥有与Synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。

实现

  • 启动同步:lock();
  • 结束同步:unlock()。

ReentrantLock

image-20210812213554694

公平锁:先来后到

非公平锁:可以插队(默认

4、线程的利弊

  • 同步的方式,解决了线程的安全问题。 --> 好处
  • 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低下。 --> 局限性

4、线程的通信

wait():一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器;

notify():一旦执行此方法,就会唤醒被 wait 的一个线程,如果有多个线程被wait,就唤醒优先级高的;

notifyAll():一旦执行此方法,就会唤醒所有被 wait 的线程。

说明:

  • wait()notify()notifyAll()三个方法必须使用在同步代码块或同步方法中;

  • wait()notify()notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器,

    否则,会出现IllegalMonitorStateException异常

生产者/消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customre)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

Producer(生产者)

// 生产者:控制产品对象
class Producer extends Thread{
    private Product product;
    public Producer(){
    }
    public Producer(Product product){
        this.product = product;
    }

    // 生产产品
    public void run(){
        System.out.println("生产产品================");
        // 调用产品方法生产
        while(true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            product.production();
        }
    }

}
复制代码

Consumer(消费者)

// 消费者:控制产品对象
class Consumer extends Thread{
    private Product product;
    public Consumer(){
    }
    public Consumer(Product product){
        this.product = product;
    }

    // 购买产品
    public void run(){
        System.out.println("购买产品================");
        // 调用产品方法出售
        while(true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            product.sell();
        }
    }
}
复制代码

Product(产品)

// 产品类
class Product{
    private int productCount = 0;
    private Object sync = new Object();
    // 生产产品
    public synchronized void production(){
            if(productCount < 20){
                productCount++;
                System.out.println(Thread.currentThread().getName() + "---生产第" + productCount + "件商品");
                notify();
            } else{
                System.out.println(Thread.currentThread().getName() + "---产品数大于 20,wait()执行");
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    }
    // 出售
    public synchronized void sell(){
            if(productCount > 0){
                System.out.println(Thread.currentThread().getName() + "***购买了第" + productCount + "件商品");
                productCount--;
                notify();
            } else{
                System.out.println(Thread.currentThread().getName() + "---没有商品了,wait()执行");
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    }
}
复制代码

运行(开始生产、消费):

public class ProductTest {

    public static void main(String[] args) {
        Product product = new Product();
        // 生产线
        Producer producer = new Producer(product);
        producer.setName("生产者");
        // 消费线1
        Consumer consumer1 = new Consumer(product);
        consumer1.setName("消费者1");
        // 消费线2
        Consumer consumer2 = new Consumer(product);
        consumer2.setName("消费者2");

        producer.start();
        consumer1.start();
        consumer2.start();
    }
}
复制代码

创建线程-3:实现 Callable 接口

JDK 5.0 新增创建线程方式:

  1. 实现Callable接口;
  2. 使用线程池

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

  • 相比run()方法,可以有返回值;
  • 方法可以抛出异常;
  • 支持泛型的返回值;
  • 需要借助 FutureTask 类,比如获取返回结果

Future 接口

  • 可以对具体RunnableCallable任务的执行结果进行取消、查询是否完成、获取结果等;
  • FutureTaskFuture接口的唯一的实现类;
  • FutureTak同时实现了RunnableFuture接口,它既可以作为Runnable北县城执行,又可以作为Future得到Callable的返回值

实现步骤

  1. 创建一个实现了Callable接口的实现类,可实现泛型,即为call()方法的返回值类型;

  2. 实现 call() 方法,将此线程需要执行的操作声明在 call() 中;

    // 1、创建一个实现了 Callable 接口的实现类,可实现泛型
    class NumThread implements Callable<Integer>{
    
        @Override
        public Integer call() throws Exception {
            System.out.println(Thread.currentThread().getName() + "执行-----");
            int sum = 0;
            for(int i = 0; i < 100; i++){
                if(i % 2 == 0){
                    sum += i;
                }
            }
            return sum;
        }
    }
    复制代码
  3. 创建Callable接口实现类的对象;

  4. Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象;

  5. FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。

  6. 获取Callable接口实现类中call()方法的返回值。

    public class Callable_Test {
    
        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()`方法。
            Thread thread = new Thread(futureTask);
            thread.start();
    
            try {
                // 6、获取`Callable`接口实现类中`call()`方法的返回值。
                Integer sum = futureTask.get();
               System.out.println(sum);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    复制代码

创建线程-4:使用线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对想能影响很大;

思路:梯田创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建、销毁,实现重复利用。

好处:

  1. 提高相应速度(减少了创建新线程的时间);

  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建);

  3. 便于线程管理

相关 API

JDK 5.0 起提供了线程池相关的API:ExecutorService接口、Executors工具类

  • ExecutorService:真正的线程池接口,常用子类 ThreadPoolExecutor
    • void execute(Runnable command):执行任务/命令,没有返回值,一般来执行Runnable;
    • Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
    • void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程;
    • Executors.newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池;
    • Executors.newSingleThreadExecutor():创建一个使用无界队列运行的单个工作线程的执行任务;
    • Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,可以调度命令在给定的延迟之后运行,或定期运行

实现步骤

public class Executors_Test {
    public static void main(String[] args) {
        // 1、提供指定线程数的线程池,根据 Executors 工具类的源码可知,该方法返回的对象其实是 ThreadPoolExecutor 类
        // 因为 ThreadPoolExecutor --> 继承AbstractExecutorService --> 实现ExecutorService
        ThreadPoolExecutor service = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
        // 获取到 ThreadPoolExecutor 类可以使用更多的方法来管理线程池。如:
        service.setCorePoolSize(20);
//        service.setKeepAliveTime(long time,TimeUnit unit);
//        service.setMaximumPoolSize(30);
//        service.setRejectedExecutionHandler(RejectedExecutionHandler handler);
//        service.setThreadFactory(ThreadFactory factory);

        // 2、执行指定的线程操作,需要提供实现 Runnable 接口或 Callable 接口的实现类
        System.out.println(service.getClass());

        // 2.1  使用 Runnable 接口实现类
        service.execute(new Number_Runnable());

        // 2.2 使用 Callable 接口实现类,并有返回值
        Future<Integer> future = service.submit(new Number_Callable());
        try {
            int sum = future.get();
            System.out.println("Callable实现类获取返回值" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        // 3、关闭连接池
        service.shutdown();
    }
}
// 实现 Runnable
class Number_Runnable implements Runnable{

    @Override
    public void run() {
        int sum = 0;
        for (int i = 0; i < 100; i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + "--" + i);
                sum += i;
            }
        }
        System.out.println("Runnable--100以内偶数和为:" + sum);
    }
}
// 实现 Callable并使用泛型控制返回值类型
class Number_Callable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++){
            if(i % 2 == 1){
                System.out.println(Thread.currentThread().getName() + "--" + i);
                sum += i;
            }
        }
        return sum;
    }
}
复制代码

面试题

1、synchronized 和 Lock的区别

  • 相同:二者都可以解决线程安全问题
  • 不相同:
    • synchronized是内置的java关键字;Lock是一个java类;
    • synchronized 机制在执行完相应的同步代码块以后,自动的释放锁;Lock 需要手动启动同步(lock()),同时结束同步也需要手动的实现(unlock())。
    • synchronized 可重入锁,不可以中断,非公平的;Lock 可重入锁,可以判断锁,非公平(可以自己设置)
      • 可重入锁:指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获得过还没释放而阻塞;
  • 优先使用顺序:Lock --> 同步代码块(已经进入方法体,分配了相应资源) --> 同步方法(在方法体外)

2、sleep()wait()的区别

  1. sleep()方法是属于 Thread 类中的;而wait()方法,则是属于 Object 类的;
  2. sleep()方法可以再任意地方调用;wait()只能在同步代码块中调用;
  3. sleep()方法使程序暂停执行指定的时间,让出cpu给其他线程,但是它的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。所以在调用sleep()方法的过程中,线程不会释放对象锁
  4. 调用wait()方法的时候,线程会放弃对象锁,进入等待此线程的等待锁定池,只有针对此对象调用notify()notifyAll()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态;注意:唤醒后,将在wait()的地方继续执行。
文章分类
后端