01_多线程与线程安全

多线程

什么是多线程

多线程是指从软硬件上实现的多条执行流程的技术(多条线程由 CPU 负责调度执行)

多线程的创建方式一(继承 Thread 类)
  • 优点:写代码的逻辑清晰、简单
  • 缺点:线程类已经继承 Thread,无法继承其他类,不利于功能的扩展
public class Test {
    public static void main(String[] args) {
        // 3. 创建 MyThread 线程类的对象代表一个线程
        Thread t = new MyThread();
        // 4. 启动线程
        t.start();

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

// 1. 让子类继承 MyThread 线程类
class MyThread extends Thread {
    // 2. 必须重写 Thread 类的 run 方法
    @Override
    public void run() {
        // 线程要执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程 MyThread 输出: " + i);
        }
    }
}
多线程的创建方式二(实现 Runnable 接口)
  • 优点:任务类只是实现接口,可以继续继承其他类,实现其他接口,扩展性强
  • 缺点:需要多一个 Runnable 对象
// 普通写法

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

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

// 1. 定义一个任务类,实现 Runnable 接口
class MyRunnable implements Runnable {
    // 2.重写 Runnable 类的 run 方法
    @Override
    public void run() {
        // 线程要执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程 MyRunnable 输出: " + i);
        }
    }
}
// 匿名内部类写法(即上面代码的简洁版)

public class Test {
    public static void main(String[] args) {
        // 1. 直接创建 Runnable 接口的匿名内部类形式(任务对象)
//        Runnable target = new Runnable() {
//            @Override
//            public void run() {
//                // 2. 在这里写好线程要执行的任务
//                for (int i = 0; i < 5; i++) {
//                    System.out.println("子线程输出: " + i);
//                }
//            }
//        };
//        new Thread(target).start();

        // 简化形式1:
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
//                for (int i = 0; i < 5; i++) {
//                    System.out.println("子线程输出: " + i);
//                }
//            }
//        }).start();

        // 简化形式2:
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程输出: " + i);
            }
        }).start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出: " + i);
        }
    }
}
多线程的创建方式三(实现 Callable 接口)
  • JDK 5.0 提供了 Callable 接口和 FutureTask 类来实现
  • 优点:(1)线程任务类只是实现接口,可以继承其他类和实现接口,扩展性强;(2)最大的优点——可以返回线程执行完毕后的结果
  • 缺点:写代码相对复杂一些
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class Test {
    public static void main(String[] args) throws Exception {
        // 3. 创建 Callable 对象
        Callable<String> call = new MyCallable(100);

        // 4. 把对象封装成一个 FutureTack 对象(任务对象)
        // 未来任务对象的作用?
        // a.是一个任务对象,实现了 Runnable 对象
        // b.可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后返回的结果
        FutureTask<String> task = new FutureTask<>(call);

        // 5. 把任务对象交给一个 Thread 对象
        new Thread(task).start();

        // 6. 获取线程执行完毕后返回的结果
        // 注意:如果执行到这里,假如上面的线程还没执行完毕
        // 这里的代码会暂停,等待上面的线程执行完毕才会去获取结果
        String res = task.get();
        System.out.println(res);
    }
}

// 1. 让这个类实现 Callable 接口
class MyCallable implements Callable<String> {
    private int n;

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

    // 2. 重写 call 方法
    @Override
    public String call() throws Exception {
        // 在这里写线程的任务,返回线程执行完毕的结果
        // 需求:求 1 ~ n 的和
        int sum = 0;
        for (int i = 0; i <= n; i++) {
            sum += i;
        }
        return "1到" + n + "的和是:" + sum;
    }
}
Thread 常用方法
  • 获取线程的名字
public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        t1.start();
        System.out.println(t1.getName());  // 获取t1线程的名字  Thread-0

        Thread t2 = new MyThread();
        t2.start();
        System.out.println(t2.getName());  // 获取t2线程的名字  Thread-1

        System.out.println(Thread.currentThread());  // 获取当前线程的名字(当前代码所在的线程是主线程)  Thread[main,5,main]
        for (int i = 0; i < 3; i++) {
            System.out.println("主线程输出>>> " + i);
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("子线程输出>>> " + i);
        }
    }
}
  • 设置线程名字的两种方法、主线程等待、线程的暂停操作
public class Test {
    public static void main(String[] args) throws Exception {
        Thread t1 = new MyThread();
        t1.setName("1号线程");  // 为线程取名字
        t1.start();
        t1.join();  // 让主线程等待t1线程执行完才继续往下执行
        System.out.println("------");

        Thread t2 = new MyThread("2号线程");    // 为线程设置名字的第二种方法(记得重写构造器)
        t2.start();
//        t2.join();

        Thread t = Thread.currentThread();
        t.setName("主线程");
        for (int i = 0; i < 3; i++) {
            System.out.println(t.getName() + "输出>>> " + i);
        }
    }
}

class MyThread extends Thread {
    public MyThread() {

    }

    public MyThread(String name) {
        super(name);  // 为线程设置名字的第二种方法
    }

    @Override
    public void run() {
        try {
//            Thread.sleep(5000);  // 让当前的线程,先暂停5000毫秒(5秒)
            Thread t = Thread.currentThread();
            for (int i = 0; i < 3; i++) {
                System.out.println(t.getName() + "输出>>> " + i);
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

线程安全

什么是线程安全

多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题

取钱问题的引出
public class Test {
    public static void main(String[] args) {
        // 1. 创建一个账户对象,代表两个人的共享账户
        Account account = new Account("ICBC-110", 1000);

        // 2. 创建两个线程,分别代表小明、小红,再去同一个账户对象中取钱1千
        new MyThread("小明", account).start();  // 小明
        new MyThread("小红", account).start();  // 小红
    }
}

class MyThread extends Thread {
    private Account account;

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

    @Override
    public void run() {
        // 取钱
        account.drawMoney(1000);
    }
}

class Account {
    private String cardId;
    private double money;

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    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;
    }

    // 取钱功能
    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 + "取钱失败,余额不足");
        }
    }
}
// 上述代码的运行结果:

// 小红取钱成功,金额:1000.0元
// 小明取钱成功,金额:1000.0元
// 小明取钱后,余额为 -1000.0元
// 小红取钱后,余额为 0.0元
取钱问题的解决

线程同步:解决线程安全问题的方案

线程同步的思想:让多个线程实现先后依次访问共享资源,从而解决了安全问题

线程同步的常见方案:

  • 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能加锁进来

加锁的场景方式:

  • 方式一:同步代码块
  • 方式二:同步方法
  • 方式三:Lock 锁
同步代码块

锁对象的使用规范

  • 建议使用共享资源作为锁对象,以确保 “锁” 的唯一性和正确的作用域,对于实例方法建议使用 this 作为锁对象
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象
synchronized(" 请在这里填写‘锁对象’ "){
    被上锁(被同步的)代码块......
}
public class Test {
    public static void main(String[] args) {
        // 1. 创建一个账户对象,代表两个人的共享账户
        Account account = new Account("ICBC-110", 1000);

        // 2. 创建两个线程,分别代表小明、小红,再去同一个账户对象中取钱1千
        new MyThread("小明", account).start();  // 小明
        new MyThread("小红", account).start();  // 小红
    }
}

class MyThread extends Thread {
    private Account account;

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

    @Override
    public void run() {
        // 取钱
        account.drawMoney(1000);
    }
}

class Account {
    private String cardId;
    private double money;

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    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;
    }

    // 取钱功能
    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 + "取钱失败,余额不足");
            }
        }
    }
}
同步方法

同步方法的底层原理:

  • 同步方法的底层其实也是由隐式锁对象的,只是锁的范围是整个方法代码
  • 如果方法是实例方法:同步方法默认用 this 作为锁对象

"同步代码块"好还是"同步方法"好?

  • 范围上:"同步代码块"锁的范围更小,性能更好
  • 可读性上:同步方法更好
public class Test {
    public static void main(String[] args) {
        // 1. 创建一个账户对象,代表两个人的共享账户
        Account account = new Account("ICBC-110", 1000);

        // 2. 创建两个线程,分别代表小明、小红,再去同一个账户对象中取钱1千
        new MyThread("小明", account).start();  // 小明
        new MyThread("小红", account).start();  // 小红
    }
}

class MyThread extends Thread {
    private Account account;

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

    @Override
    public void run() {
        // 取钱
        account.drawMoney(1000);
    }
}

class Account {
    private String cardId;
    private double money;

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    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;
    }

    // 取钱功能
    // 加上 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 + "取钱失败,余额不足");
            }
        }
    }
}
Lock 锁

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

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

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

public class Test {
    public static void main(String[] args) {
        // 1. 创建一个账户对象,代表两个人的共享账户
        Account account = new Account("ICBC-110", 1000);

        // 2. 创建两个线程,分别代表小明、小红,再去同一个账户对象中取钱1千
        new MyThread("小明", account).start();  // 小明
        new MyThread("小红", account).start();  // 小红
    }
}

class MyThread extends Thread {
    private Account account;

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

    @Override
    public void run() {
        // 取钱
        account.drawMoney(1000);
    }
}

class Account {
    private String cardId;  // 卡号
    private double money;  // 钱
    // 创建了一个锁对象(每个银行卡都应该有一个对应的锁)
    private final Lock lk = new ReentrantLock();

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    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;
    }

    // 取钱功能
    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();  // 获取线程的名字,判断是谁来取钱了
        lk.lock();  // 加锁(加锁的前提是:一定要确保可以解锁!)
        // 判断余额是否足够
        try {
            {
                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 {
            lk.unlock();  // 解锁
        }

    }
}

你可能感兴趣的:(Java高级,java)