Java高级-多线程

多线程

  • 1.线程创建的方法
    • 1.1.方法一 继承Thread类
    • 1.2.方法二 实现Runnable接口
    • 1.3.方法三 实现Callable接口
  • 2.线程安全
    • 2.0.线程不安全的案例
    • 2.1.方式一:同步代码块
    • 2.2.方式二:同步方法
    • 2.3.方式三:Lock锁
  • 3.线程池
    • 3.1.创建线程池
    • 3.2.线程池处理Runnable任务
    • 3.3.线程池处理Callable任务
  • 4.并发和并行

1.线程创建的方法

1.1.方法一 继承Thread类

继承Thread类

  • 1.定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  • 2.创建MyThread类的对象
  • 3.调用线程对象的start()方法启动线程

优缺点:

  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展
public class MyThread extends Thread{
    @Override
    public void run() {
        // 线程的执行任务
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程MyThread -> " + i);
        }
    }
}
public class ThreadTest1 {
    public static void main(String[] args) {
        // 创建MyThread线程类的对象 代表一个线程
        Thread thread = new MyThread();
        // 启动线程(自动执行run方法)
        thread.start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程MainThread -> " + i);
        }
    }
}

1.2.方法二 实现Runnable接口

实现Runnable接口

  • 1.定义线程任务类MyRunnable实现Runnable接口,重写run()方法
  • 2.创建MyRunnable任务对象
  • 3.把MyRunnable任务对象交给Thread处理
  • 4.调用线程的start()方法启动线程

优缺点:

  • 优点:任务类只是实现接口,可以继续继承其他类/实现其他接口,拓展性强
  • 缺点:需要多一个Runnable对象
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程MyRunnable-> " + i);
        }
    }
}
public class ThreadTest2 {
    public static void main(String[] args) {
        // 创建任务对象
        Runnable target = new MyRunnable();
        // 把 任务对象 交给 线程对象 处理
        new Thread(target).start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程Main -> " + i);
        }
    }
}

匿名内部类的写法

  • 1.创建Runnable的匿名内部类对象
  • 2.再交给Thread线程对象
  • 3.再调用线程对象的start()启动线程
public class ThreadTest2_2 {
    public static void main(String[] args) {
        // 1.创建匿名内部类
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程 -> " + i);
                }
            }
        };

        // 2.再交给Thread线程对象
        Thread thread = new Thread(target);
        // 3.启动线程
        thread.start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程Main -> " + i);
        }
    }
}

1.3.方法三 实现Callable接口

实现Callable接口

  • 1.创建任务对象
    • 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据
    • 把Callable类型的对象封装成FutureTask(线程任务对象)
  • 2.把线程任务对象交给Thread对象
  • 3.调用线程对象的start()启动线程
  • 4.线程执行完毕后,通过FutureTask对象的get方法去获取线程任务的执行结果

优缺点:

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后获取线程执行的结果
  • 缺点:编码复杂
public class MyCallable implements Callable<String> {
    private int n;

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

    @Override
    public String call() throws Exception {
        int sum = 0;
        // 假设 求1~n的和 并返回
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "线程求出1~" + n + "的和:" + sum;
    }
}

public class ThreadTest3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建Runnable对象
        Callable<String> callable = new MyCallable(100);
        // 封装成FutureTask
        FutureTask<String> futureTask = new FutureTask<>(callable);
        // 交给Thread对象
        Thread thread = new Thread(futureTask);
        // 启动线程
        thread.start();

        // 获取线程执行完毕后返回的结果
        String result = futureTask.get();
        System.out.println("result = " + result);
    }
}

2.线程安全

2.0.线程不安全的案例

案例:模拟线程安全问题。小红和小明同时取钱(同一个账户)

public class ThreadTest {
    public static void main(String[] args) {
        Account account = new Account("ICBC-100", 100000);

        new DrawThread(account, "小明").start(); // 小明
        new DrawThread(account, "小红").start(); // 小红

    }
}
public class DrawThread extends Thread{
    private final Account account;

    public DrawThread(Account account, String name) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        account.drawMoney(100000);
    }
}
public class Account {
    private String cardId;
    private double money; // 余额

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }
    
    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();
        if (this.money >= money) {
            System.out.println(name + "来取钱" + money + "成功");
            this.money -= money;
            System.out.println(name + "取钱后,剩余:" + this.money);
        } else {
            System.out.println(name + "来取钱,余额不足");
        }
    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}

2.1.方式一:同步代码块

**作用:**把访问共享资源的核心代码给上锁,一次保证线程安全

synchronized (同步锁) {
    访问共享资源的核心代码
}

注意事项

  • 对于当前同时执行的线程来说,同步锁必须是同意把(同一个对象),否则会出bug

在案例中添加同步代码块 (实例方法使用this作为锁对象)

public void drawMoney(double money) {
    String name = Thread.currentThread().getName();
    synchronized (this) {
        if (this.money >= money) {
            System.out.println(name + "来取钱" + money + "成功");
            this.money -= money;
            System.out.println(name + "取钱后,剩余:" + this.money);
        } else {
            System.out.println(name + "来取钱,余额不足");
        }
    }
}

如果要求在静态方法中保证线程安全,同步锁应该是 类名.class

public static void test() {
    synchronized (类名.class) {
        // 核心代码
    }
}

2.2.方式二:同步方法

作用:把访问共享资源的核心方法给上锁,以此保证线程安全

原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进入执行

修饰符 synchronized 返回值类型 方法名称 (形参列表) {
    操作共享资源的代码
}

在案例中添加同步方法

public synchronized void drawMoney(double money) {
    String name = Thread.currentThread().getName();
    if (this.money >= money) {
        System.out.println(name + "来取钱" + money + "成功");
        this.money -= money;
        System.out.println(name + "取钱后,剩余:" + this.money);
    } else {
        System.out.println(name + "来取钱,余额不足");
    }
}

底层原理

  • 底层也是与隐式锁对象,锁的范围是整个方法代码
  • 如果方法是实例方法:同步方法默认用 this 作为锁的对象
  • 如果方法是静态方法:同步方法默认用 类名.class 作为锁的对象

2.3.方式三:Lock锁

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活/方便/强大

Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象

public class Account {
    // 创建一个锁对象
    private final Lock lock = new ReentrantLock();
    
	//略
    
    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();
        try {
            lock.lock(); //加锁
            if (this.money >= money) {
                System.out.println(name + "来取钱" + money + "成功");
                this.money -= money;
                System.out.println(name + "取钱后,剩余:" + this.money);
            } else {
                System.out.println(name + "来取钱,余额不足");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

注意事项

  • 1.用 final 修饰
  • 2.用 try...catch...finally 包裹住加锁与解锁操作,保证即使出现异常,也可确保能解锁

3.线程池

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

不断创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来

3.1.创建线程池

线程池接口:ExecutorService

**方式一:**使用 ExecutorService 的实现类 ThreadPoolExecutor 创建一个线程池对象

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,  BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
  • 参数一:corePoolSize 指定线程池的核心线程数量
  • 参数二:maximumPoolSize 线程池的最大线程数量(maximumPoolSize > corePoolSize)
  • 参数三:keepAliveTime 临时线程的存活时间
  • 参数四:unit 指定临时线程存活的时间单位(秒/时/分/天)
  • 参数五:workQueue 指定线程池的任务队列
  • 参数六:ThreadFactory 指定线程池的线程工厂
  • 参数七:RejectedExecutionHandler 指定线程池的任务拒绝策略(线程都在忙,任务队列也满了,新任务来了该怎么处理)
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
策略 (参数七) 详解
ThreadPoolExecutor.AbortPolicy() 丢弃任务并抛出RejectedExecutionException异常。默认策略
ThreadPoolExecutor.DiscardPolicy() 丢弃任务,但是不抛出异常。(不推荐做法)
ThreadPoolExecutor.DiscardOldestPolicy() 抛弃队列等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy() 有主线程负责调用任务的run()方法从二绕过线程池直接执行 (老板亲自执行任务)

注意事项

  • 临时线程什么时候创建
    • 新任务提交时发现核心线程在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
  • 什么时候开始拒绝新任务
    • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务

**方式二:**使用 Excutors (线程池的工具类) 调用方法返回不同特点的线程池对象

方法名称 说明
ExcutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池
ExcutorService newSingleTHreadExecutor() 创建只有一个线程的线程池
ExcutorService newCachedThreadPool() 线程数量随着任务增加而增加
ExcutorService newScheduledThreadPool(int corePoolSize)
ExecutorService pool = Executors.newFixedThreadPool(3);
ExecutorService pool = Executors.newSingleThreadExecutor();

核心线程数量到底配置多少

计算密集型的任务:核心线程数量 = CPU的核数 + 1

IO密集型的任务:核心线程数量 = CPU的核数 * 2

3.2.线程池处理Runnable任务

public static void main(String[] args) {
    // 1.通过ThreadPoolExecutor创建一个线程池对象
    ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

    Runnable target = new MyRunnable();
    // 线程池会自动创建新线程,自动处理这个任务,自动执行
    pool.execute(target); // 第一个线程
    pool.execute(target); // 第二个线程
    pool.execute(target); // 第三个线程
    pool.execute(target);
    pool.execute(target);
    pool.execute(target);
    pool.execute(target);
    // 临时线程的创建时机
    pool.execute(target);
    pool.execute(target);
    // 到了新任务拒绝的时机
    pool.execute(target);

    // 线程池不会主动关闭,程序会一直运行
    pool.shutdown(); // 等待线程池任务全部完成后,再关闭线程池
    //pool.shutdownNow();// 立即关闭线程,不管是否还有任务在执行
}

3.3.线程池处理Callable任务

public class ThreadPoolTest2 {
    public static void main(String[] args) throws Exception{
        // 1.通过ThreadPoolExecutor创建一个线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());

        // 2.使用线程处理Callable任务
        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() = " + f1.get());
        System.out.println("f2.get() = " + f2.get());
        System.out.println("f3.get() = " + f3.get());
        System.out.println("f4.get() = " + f4.get());

        // 线程池不会主动关闭,程序会一直运行
        pool.shutdown(); // 等待线程池任务全部完成后,再关闭线程池
        //pool.shutdownNow();// 立即关闭线程,不管是否还有任务在执行
    }
}

4.并发和并行

**进程:**正常运行的程序/软件就是一个独立的进程

**线程:**线程是属于进程的,一个进程中可以同时运行很多个线程

进程中的多个线程是并发和并行执行的

并发:

  • 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能执行,CPU会轮询为系统的每个线程服务。由于CPU切换的速度很快,给我们感觉就是线程都在同时执行,这就是并发

并行:

  • 在同一个时刻上,同时有多个线程被CPU调度执行

多线程是怎么执行的

  • 并发和并行同时执行

你可能感兴趣的:(Java高级特性,java,开发语言)