多线程(Java)

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

一、创建多线程

Java 通过 java.lang.Thread 类的对象来代表线程,main 方法也是一条线程,常作为主线程

1.方式一:继承Thread 类

        ①定义一个子类 MyThread 继承线程类 Thread,在类中重写 run() 方法

        ②创建 MyThread 类的对象

        ③在主线程 main 中调用线程对象的 start() 方法启动线程(执行run方法)

优点:编码简单

缺点:线程类已经继承 Thread,无法继承其他类(Java 不支持多继承),不利于功能的扩展

public class MyThread extends Thread{
    @Override
    public void run() {
        for(int i = 0;i<5;i++){
            System.out.println("MyThread:"+i);
        }
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        for(int i = 0;i<5;i++){
            System.out.println("main:"+i);
        }
    }
}

注意:1)一定要用 start() 方法启用线程,如果使用 run() 方法,就变成了普通的方法调用

           2)不要把主线程任务放到子线程之前,因为主线程不调用 start() 方法就不会启动子线程,最后导致主线程任务执行结束才执行子线程

2.方式二:实现 Runnable 接口

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

②创建 MyRunnable 任务对象

③把 MyRunnable 任务对象交给 Thread 对象接收,再调用 start() 方法

优点:任务类只是实现接口,可以继承其他类,或实现其他接口,扩展性强

缺点:需要多创建一个Runnable对象

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i<5;i++){
            System.out.println("子线程:"+i);
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Runnable r = new MyRunnable();
        new Thread(r).start();
        for(int i = 0;i<5;i++){
            System.out.println("main主线程:"+i);
        }
    }
}

补充:方式二的简化写法(利用匿名内部类和Lambda表达式)

public class Test {
    public static void main(String[] args) {
        new Thread(() -> {
            for(int i = 0;i<5;i++){
                System.out.println("子线程:"+i);
            }
        }).start();
        for(int i = 0;i<5;i++){
            System.out.println("main主线程:"+i);
        }
    }
}

3.方式三:利用 Callable 接口、FutureTask 类来实现

①创建任务对象

定义一个类实现 Callable 接口, 重写 call 方法,封装要执行的任务和要返回的数据,把 Callable 类型的对象封装成 FutureTask (线程任务对象)

②把线程任务对象交给 Thread 对象。

③调用 Thread 对象的 start 方法启动线程。

④线程执行完毕后,通过 FutureTask 对象的的 get 方法去获取线程任务执行的结果

优点:线程任务类只是实现接口,可以继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果

public class MyCallable implements Callable {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }
    @Override
    public String call() throws Exception {
        int sum = 0;
        for(int i = 1;i<=n;i++){
            sum+=i;
        }
        return "经子线程计算可得,1-"+ n +"的和为:"+ sum;
    }
}
public class Test {
    public static void main(String[] args) throws Exception{
        Callable c1 = new MyCallable(100);
        FutureTask f1 = new FutureTask<>(c1);
        new Thread(f1).start();
        Callable c2 = new MyCallable(200);
        FutureTask f2 = new FutureTask<>(c2);
        new Thread(f2).start();
        String rs2 = f2.get();
        System.out.println(rs2);
        String rs1 = f1.get();
        System.out.println(rs1);
    }
}

注意:1)Callable 是泛型接口,FutureTask 是泛型类

           2)在主线程中调用 get 方法时,如果当前子线程未执行完,则主线程会暂停等待

二、Thread 的常用方法

多线程(Java)_第1张图片多线程(Java)_第2张图片

public class MyThread extends Thread{
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        for(int i = 0;i<5;i++){
            System.out.println(this.getName()+"=>"+i);
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Test {
    public static void main(String[] args) throws Exception{
        Thread t1 = new MyThread("1号线程");
        t1.setName("1号线程");
        t1.start();
        t1.join();
        System.out.println(t1.getName());

        Thread t2 = new MyThread("2号线程");
        t2.setName("2号线程");
        t2.start();
        System.out.println(t2.getName());

        Thread main = Thread.currentThread();
        System.out.println(main.getName());
    }
}

Thread 还有很多方法,这里不再介绍

三、线程安全

1.认识线程安全问题

1)存在多个线程在同时执行

2)同时访问一个共享资源

3)存在修改该共享资源

例:银行取钱模型

有两个人同时对同一个账户操作,该账户有10万元,取钱需要三个步骤:

1)输入取钱金额         2)判断余额是否足够        3)取出钱并更新账户余额

一个人要取6万元,另一个人要取8万元,在系统中他们同时输入金额,此时第一个人的系统网络较好,先判断了余额足够,但在他取出钱之前,第二个人也判断了余额足够,此时他们都取出了钱,导致银行账户凭空取出了4万元

2.模拟线程安全问题

   以上述取钱模型为例:

        1)创建一个账户类,用于模拟存钱和取钱

public class Account {
    private double money;
    public Account(double money) {
        this.money = money;
    }
    public double getMoney() {
        return money;
    }
    public void setMoney(double money) {
        this.money = money;
    }
    public void drewMoney(double money){
        String name = Thread.currentThread().getName();
        System.out.println("欢迎您进入取钱系统,"+ name);
        if(money < this.money){
            System.out.println(name+"要取"+money+"元");
            this.money -= money;
            System.out.println(name+"成功取出"+money+"元,当前余额:"+this.money+"元");
        }
        else{
            System.out.println("取钱失败,余额不足~~");
        }
    }
}

        2)创建一个ATM类,用于模拟不同机器同时取钱(不同线程同时运作)

public class ATM extends Thread{
    private Account acc;
    private double money;
    public ATM(String name, double money, Account acc) {
        super(name);
        this.money = money;
        this.acc = acc;
    }
    @Override
    public void run() {
        acc.drewMoney(money);
    }
}

        3)在主线程中同时开启两个子线程

public class Test {
    public static void main(String[] args) {
        Account acc = new Account(100000);
        new ATM("cxk",60000,acc).start();
        new ATM("fzc",80000,acc).start();
    }
}

        4)结果如下(触发了线程安全问题)

多线程(Java)_第3张图片

四、线程同步 

1.认识线程同步

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

线程同步的思想:让多个线程实现以一定的先后顺序依次访问共享资源

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

2.同步代码块

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

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

多线程(Java)_第4张图片

锁对象的规范:1)建议使用共享资源作为锁对象,对于实例方法建议使用 this 作为锁对象

                         2)对于静态方法建议使用字节码(类名.class)对象作为锁对象

    public static void test(){
        synchronized (Account.class){
        }
    }
    public void drewMoney(double money){
        String name = Thread.currentThread().getName();
        System.out.println("欢迎您进入取钱系统,"+ name);
        synchronized (this) {
            if(money < this.money){
                System.out.println(name+"要取"+money+"元");
                this.money -= money;
                System.out.println(name+"成功取出"+money+"元,当前余额:"+this.money+"元");
            }
            else{
                System.out.println("取钱失败,余额不足~~");
            }
        }
    }

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

3.同步方法

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

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行多线程(Java)_第5张图片

    public synchronized void drewMoney(double money){
        String name = Thread.currentThread().getName();
        System.out.println("欢迎您进入取钱系统,"+ name);
        if(money < this.money){
            System.out.println(name+"要取"+money+"元");
            this.money -= money;
            System.out.println(name+"成功取出"+money+"元,当前余额:"+this.money+"元");
        }
        else{
            System.out.println("取钱失败,余额不足~~");
        }
    }

注意事项:同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码,默认与同步代码块的锁相同

补充:同步代码块和同步方法如何选择

范围上:同步代码块锁的范围更小,同步方法锁的范围更大,所以同步代码块在程序运行时更占优势

可读性上:同步方法更加简洁,可读性更好

4. Lock 锁

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

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

    private final Lock lock = new ReentrantLock();
    public void drewMoney(double money){
        String name = Thread.currentThread().getName();
        System.out.println("欢迎您进入取钱系统,"+ name);
        try {
            lock.lock();
            if(money < this.money){
                System.out.println(name+"要取"+money+"元");
                this.money -= money;
                System.out.println(name+"成功取出"+money+"元,当前余额:"+this.money+"元");
            }
            else{
                System.out.println("取钱失败,余额不足~~");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }

注意:1)在创建 Lock 锁对象时,尽量用 final 修饰(显得专业)

           2)使用 unlock 方法解锁时,如果前面的代码出现了bug,则会导致无法正常解锁,所以要使用 finally 语句修饰 unlock 方法

五、线程通信

当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺

1.常见模型:生产者和消费者模型

生产者线程负责生产数据

消费者线程负责消费生产者生产的数据

生产者生产完数据后要等待,通知消费者消费;消费者消费完数据后也要等待,通知生产者生产

2. Object 类提供的等待和唤醒方法

多线程(Java)_第6张图片

注意:1)上述方法应该使用当前同步锁对象进行调用(实例用 this,静态用类名.class)

           2)一定要先唤醒其他线程,再等待自己,不然可能会出现死锁

六、认识线程池

1.认识线程池

线程池:一个可以复用线程的技术

作用:用户每发起一个请求,后台就需要创建一个新线程来处理,而创建新线程的开销很大,并且请求过多时,会产生大量的线程,这样会严重影响系统的性能。所以需要使用线程池通过复用线程来处理多个相同类型的任务

工作原理:线程池中分为两个部分

1)工作线程(WorkThread)

固定数量的线程,用来处理线程池接收的任务,当一个工作线程处理完当前任务时,会继续处理下一个任务,不再创建新线程

2)任务队列(WorkQueue)

可控制数量的任务对象,依次被工作线程处理,必须是由 Runnable 或 Callable 任务接口实现的

2.创建线程池

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

1)使用 ExecutorService 的实现类 ThreadPoolExecutor 自创建一个线程池对象

参数一:corePoolSize:指定线程池的核心线程的数量

参数二:maximumPoolSize:指定线程池的最大线程数量(核心线程数 + 临时线程数)

参数三:keepAliveTime:指定临时线程的存活时间

参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)

参数五:workQueue:指定线程池的任务队列

参数六:threadFactory:指定线程池的线程工厂,用于创建线程

参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)

public class Test {
    public static void main(String[] args) {
//        public ThreadPoolExecutor(int corePoolSize,
//                                  int maximumPoolSize,
//                                  long keepAliveTime,
//                                  TimeUnit unit,
//                                  BlockingQueue workQueue,
//                                  ThreadFactory threadFactory,
//                                  RejectedExecutionHandler handler)
        new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,
                                new ArrayBlockingQueue<>(5),
                                Executors.defaultThreadFactory(),
                                new ThreadPoolExecutor.AbortPolicy());
    }
}

注意:1)TimeUnit 是一个枚举类,包含了各种时间单位

           2)WorkQueue 有两种表现形式:LinkedBlockingDeque (以链表形式存储,不限制大小),ArrayBlockingQueue (以数组形式存储,需要设置大小)

           3)ThreadFactory 可以直接创建 ThreadFactory 对象然后重写 newThread 方法,也可以使用 Executors 工具类的 defaultThreadFactory 方法返回的线程工厂

           4)hander 可以直接调用 ThreadPoolExecutor 类的静态方法 AbortPolicy(如果任务队列已满,则抛出异常)

2)使用 Executors (线程池的工具类)调用方法返回不同特点的线程池对象

多线程(Java)_第7张图片

注意:1)这些方法的底层,都是通过线程池的实现类 ThreadPoolExecutor 创建的线程池对象

           2)在大型的并发环境中使用 Executors 可能会有系统风险

补充:创建线程池的某些原理问题

1)临时线程什么时候创建

新任务提交时发现核心线程都在忙,任务队列也满了,并且当前线程数小于最大线程数,此时才会创建临时线程

2)什么时候会开始拒绝新任务

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

3)创建的线程池应该设置多少核心线程数

计算密集型任务:核心线程数量 = CPU内核数 + 1                                                                             IO 密集型任务:核心线程数量 = CPU内核数 * 2

3.线程池处理 Runnable 和 Callable 任务

ExecutorService 的常用方法多线程(Java)_第8张图片

1)处理 Runnable 任务

public class Test {
    public static void main(String[] args) {
        ExecutorService pool = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        Runnable target = new MyRunnable();
        try {
            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); // 任务队列已满,临时线程已满,抛出异常
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            pool.shutdown();
        }
        // pool.shutdownNow();
    }
}

2)处理 Callable 任务

public class Test {
    public static void main(String[] args) {
        ExecutorService pool = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        Callable c = new MyCallable();
        try {
            Future f1 = pool.submit(c);
            Future f2 = pool.submit(c);
            Future f3 = pool.submit(c);
            Future f4 = pool.submit(c);
            Future f5 = pool.submit(c);
            Future f6 = pool.submit(c);
            Future f7 = pool.submit(c);
            Future f8 = pool.submit(c);
            System.out.println(f1.get());
            System.out.println(f2.get());
            System.out.println(f3.get());
            System.out.println(f4.get());
            System.out.println(f5.get());
            System.out.println(f6.get());
            System.out.println(f7.get());
            System.out.println(f8.get());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            pool.shutdown();
        }
        // pool.shutdownNow();
    }
}

注意:try 语句是用来捕获拒绝任务时抛出的异常,不然会导致线程池无法正常关闭

任务拒绝策略:多线程(Java)_第9张图片

七、补充知识

1.进程

1)正在运行的程序( 软件)就是一个独立的进程

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

3)进程中的多个线程其实是由并发和并行执行的

2.并发

1)一个处理器同时处理多个任务

2)逻辑上的同时:处理器以极快的速度不断轮换执行任务

3.并行

1)多个处理器同时处理多个任务

2)物理上的同时:多个处理器同时处理一批任务,再同时切换到下一批任务

4.线程的生命周期

线程的各种状态的不断转变(从创建到结束)

线程的 6 种状态:Thread 类的内部枚举类中存储

多线程(Java)_第10张图片

多线程(Java)_第11张图片

5.悲观锁

直接认为此线程会修改公共资源,不加以判断直接加锁,使其他同类型线程排队

线程安全,但性能较差

上述的线程同步就是悲观锁的思想

public class MyRunnable implements Runnable{
    private int count;
    @Override
    public void run() {
        for(int i = 0;i<10;i++){
            synchronized (this) {
                System.out.println(Thread.currentThread().getName()+"count = "+(++count));
            }
        }
    }
}

6.乐观锁

一开始不上锁,所有线程一起执行,等到出现线程不安全的问题时开始控制

线程安全,性能较好

public class MyRunnable implements Runnable{
    // 整型乐观锁 -- 由原子类实现
    private AtomicInteger count = new AtomicInteger();
    @Override
    public void run() {
        for(int i = 0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"count = "+ count.addAndGet(1));
        }
    }
}

这里只做介绍,以后会深入学习

你可能感兴趣的:(java,开发语言)