Java中的多线程

一、什么是多线程

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

二、线程的创建方式

2.1 方法一:继承Thread类

1. 定义子类MyThread继承Thread类,重写run()方法

package com.hkd.thread;

public class MyThread extends Thread {

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

2. main中创建MyThread类的对象,调用线程对象的start方法开启线程(启动后还是执行run方法)

package com.hkd.thread;

public class ThreadDemo1 {
    public static void main(String[] args) {
        // new一个新线程对象
        Thread t = new MyThread();
        // 调用start方法启动线程
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行输出"+ i);
        }
    }
}

疑问

  1. 为什么不调用run(),而是调用start()开启线程?
    直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。只有调用start方法才是启动一个新的线程执行。
  2. 为什么子线程要放在主线程之前写?
    如果主线程放在子线程之前了,主线程一直是先跑完的,相当于单线程了。

方法一的优缺点:

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

2.2 方法二:实现Runnable接口

1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法

package com.hkd.thread;

public class MyRunnable implements Runnable {

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

2. 创建MyRunnable任务对象,把MyRunnable任务对象交给Thread处理;调用线程对象的start方法开启线程。

package com.hkd.thread;

public class ThreadDemo1 {
    public static void main(String[] args) {
        // 创建一个任务对象
        Runnable target = new MyRunnable();
        // 把任务对象交给Thread处理
        Thread t = new Thread(target);
        // 开启线程
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行输出"+ i);
        }
    }
}

在创建线程的时候,不会立即执行run(),而是等到start()执行的时候,才会开始执行run()。

方式二的优缺点:

  1. 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
  2. 缺点:编程多一层对象包装(任务对象),如果线程有执行结果是不可以直接返回的。

2.3 方法二的另一种写法(匿名内部类)

package com.hkd.thread;

public class ThreadDemo1 {
    public static void main(String[] args) {
	    new Thread(() -> {
	                for (int i = 0; i < 10; i++) {
	                    System.out.println("子线程执行输出"+ i);
	                }
	            }).start();
	        // 把任务对象交给Thread处理
	        Thread t = new Thread(target);

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行输出"+ i);
        }
    }
}

2.4 方法三:实现Callable接口

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

package com.hkd.thread;

import java.util.concurrent.Callable;

// 需声明线程任务执行完毕后的结果数据类型
public class MyCallable implements Callable<String> {

    private int n;
    public MyCallable(int n){
        this.n = n;
    }

    // 重写call方法
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "子线程的执行结果:"+ sum;
    }
}

2. 用FutureTask把Callable对象封装成线程任务对象,把这个对象交给Thread处理,调用start方法开启线程,执行任务;线程执行完毕后,通过FutureTask的get方法获取任务执行的结果

package com.hkd.thread;

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

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

        // 创建Callable对象
        Callable<String> call1 = new MyCallable(100);
        // 把Callable任务对象封装成FutureTask,它是Runnable对象(实现Runnable接口),可以交给Thread
        // 可以在线程执行完毕之后通过get方法得到线程执行的结果
        FutureTask<String> f1 = new FutureTask<>(call1);
        // 交给线程处理
        Thread t1 = new Thread(f1);
        t1.start();

        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2 = new FutureTask<>(call2);
        Thread t2 = new Thread(f2);
        t2.start();

		// get方法获取任务执行的结果,如果上面代码没有执行完是不执行这里的
        try {
            String rs1 = f1.get();
            System.out.println("线程1结果:"+ rs1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            String rs2 = f2.get();
            System.out.println("线程2结果:"+ rs2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

方法三的优缺点:

  1. 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。可以在线程执行完毕后去获取线程执行的结果。
  2. 缺点:编码复杂一点。

三、线程提供的API

在这里插入图片描述
在这里插入图片描述

package com.hkd.thread.com.hkd.thread2;

public class ThreadMain {
    public static void main(String[] args) throws Exception {
        Thread t1 = new MyThread();
        // 给当前线程命名
        t1.setName("一号");
        t1.start();
        System.out.println(t1.getName());

        Thread t2 = new MyThread();
        t1.setName("二号");
        t2.start();
        System.out.println(t2.getName());

        // 当前正在运行的线程对象
        Thread m = Thread.currentThread();
        System.out.println(m.getName());

        for (int i = 0; i < 5; i++) {
            System.out.println("main线程输出:" + i);
            if(i==3)
                // 让当前线程休眠
                Thread.sleep(3000);
        }
    }
}
package com.hkd.thread.com.hkd.thread2;

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "输出:" +i);
        }
    }
}

四、线程安全问题

4.1 线程安全问题的原因

  • 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题
  • 出现安全问题的原因:多线程并发,同时访问共享资源,存在修改共享资源

4.2 解决方法——线程同步

  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
  • 线程同步的核心思想:
    加锁。把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

4.2.1 解决方法一——同步代码块

  • 作用:把出现线程安全问题的核心代码给上锁。
  • 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行
  • synchronized给核心代码上锁
  • 规范上:建议使用共享资源作为锁对象
  • 对于实例方法:建议使用this作为锁对象
  • 对于静态方法:建议使用字节码(类名.class)对象作为锁对象
    public void drawmoney(double money) {
        // 1. 获取谁来取钱
        String name = Thread.currentThread().getName();
        // 同步代码块
        synchronized (this) {
            // 2. 判断账户余额够不够
            if(this.money >= money){
                System.out.println(name + "来取钱成功,吐出" + money);
                this.money -= money;
                System.out.println(name + "取钱后剩余" + this.money);
            } else {
                System.out.println(name + "取钱,余额不足");
            }
        }
    }

4.2.2 同步方法

  • 作用:把出现线程安全问题的核心方法给上锁。
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
  • 对于实例方法默认使用this作为锁对象。
  • 对于静态方法默认使用类名.class对象作为锁对象。
//同步方法(锁起来)
    public synchronized void drawmoney(double money) {
        // 1. 获取谁来取钱
        String name = Thread.currentThread().getName();

        // 2. 判断账户余额够不够
        if (this.money >= money) {
            System.out.println(name + "来取钱成功,吐出" + money);
            this.money -= money;
            System.out.println(name + "取钱后剩余" + this.money);
        } else {
            System.out.println(name + "取钱,余额不足");
        }
    }

疑问:

同步代码块和同步方法哪个更好一些?
答:同步代码块锁的范围更小,同步方法锁的范围更大。

4.2.3 Lock锁

可以灵活地上锁和解锁。

public void drawmoney(double money) {
        // 1. 获取谁来取钱
        String name = Thread.currentThread().getName();

        // 上锁
        lock.lock();
        try {
            // 2. 判断账户余额够不够
            if (this.money >= money) {
                System.out.println(name + "来取钱成功,吐出" + money);
                this.money -= money;
                System.out.println(name + "取钱后剩余" + this.money);
            } else {
                System.out.println(name + "取钱,余额不足");
            }
        } finally {
            //解锁
            lock.unlock();
        }
    }

4.3 线程通信

- 什么是线程通信、如何实现?

  • 所谓线程通信就是线程间相互发送数据。

- 线程通信常见形式

  • 通过共享一个数据的方式实现。
  • 根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。

- 线程通信实际应用模型

  • 生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。
  • 一般要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。

4.4 线程通信的三个方法

在这里插入图片描述
上述方法应该用当前同步锁对象调用

五、线程池

5.1 什么是线程池

线程池就是一个可以复用线程的技术。

  • 不使用线程池的问题

    如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。

5.2 谁创建线程池

JDK 5.0起提供了代表线程池的接口:ExecutorService

5.3 如何得到线程对象

使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象,
ThreadPoolExecutor构造器的参数说明在这里插入图片描述

5.4 线程池面试题

1. 临时线程是什么时候创建的?

新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

2. 什么时候开始拒绝任务?

核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。

5.5 线程池处理Runnable任务

使用ExecutorService的方法:void execute(Runnable target)

package com.hkd.threadpool;

import java.util.concurrent.*;

/**
 * 自定义线程池对象
 */
public class threadpooldemo {
    public static void main(String[] args) {
        /**
         * public ThreadPoolExecutor(int corePoolSize,
         *                               int maximumPoolSize,
         *                               long keepAliveTime,
         *                               TimeUnit unit,
         *                               BlockingQueue<Runnable> workQueue,
         *                               ThreadFactory threadFactory)
         */
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        // 给任务线程池,Runnable任务
        Runnable target = new MyRunnable();
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
    }
}

5.6 线程池处理Callable任务

使用ExecutorService的方法:Future<T> submit(Callable<T> command)

package com.hkd.threadpool;

import com.hkd.thread.MyCallable;

import java.util.concurrent.*;

/**
 * 自定义线程池对象
 */
public class threadpooldemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        /**
         * 处理Callable任务
         */
        // 提交Callable任务,返回Future任务对象
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}

在线程池执行submit()方法的时候,假设第一个线程执行的时候,第二个线程是否会执行是看线程调度器决定的,如果线程池中有足够的空闲线程,那么第二个线程任务可以立马执行,但如果线程池中的所有线程都在执行其他任务,则第二个线程将会等待,直到有空闲线程可以使用。

5.7 新任务拒绝策略

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值