Java#34(多线程)

目录

线程(thread): 是一个程序内部的一条执行路经(之前写的代码中,main方法的执行就是一条单独的执行路径) 

单线程: 一个程序只有一条执行路线

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

一.创建多线程

1.继承Thread类

2.实现Runnable接口

3.利用Callable, FutureTask接口实现

二.Thread的常用方法 

1.Thread的构造方法

2.Thread获取和设置线程名称

3.Thread类获得当前线程的对象

三.线程安全

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

四.线程同步

加锁: 让多个线程实现先后依次访问共享资源

1.同步代码块

2.同步方法

3.Lock锁

五.线程通信

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

线程通信常见模型

六.线程池

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

2.线程池常见面试题

3.ExecutorService的常用方法

4.Executors得到线程池对象的常用方法


线程(thread): 是一个程序内部的一条执行路经(之前写的代码中,main方法的执行就是一条单独的执行路径) 

单线程: 一个程序只有一条执行路线

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

一.创建多线程

1.继承Thread类

步骤:

(1)定义一个子类继承Thread,然后重写run()方法

(2)创建子类的对象

(3)调用子类对象的start()方法启动线程(启动后执行的是run()方法)

代码示范: 第一段是子类的代码, 第二段是测试类的代码

//1.定义一个子类继承Thread,重写run方法
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程"+i);
        }
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        //2.创建子线程对象
        MyThread mt = new MyThread();
        //3.调用start()方法启动子线程(启动后执行的是run()方法)
        mt.start();

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

结果展示: (主线程和子线程的执行没有先后之分)(要将主线程的执行任务放在子线程之后,)

用继承Thread类创建多线程的优点和缺点

优点: 编程简单

缺点: 线程类已经继承Thread类, 无法继承其他类

2.实现Runnable接口

步骤:

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

(2)创建任务对象

(3)将任务对象交给Thread处理表

(3)调用线程对象的start( )方法启动线程

代码示范: 第一段是类的代码,第二段是测试的代码

//创建多线程:子类实现Runnable接口
public class MyThread1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程"+i);
        }
    }
}
public class Test1 {
    public static void main(String[] args) {
        /*第一种写法//创建任务对象,任务对象不是Thread的子类,无法直接调用start()方法
        MyThread1 mt = new MyThread1();
        //把任务对象交给Thread处理
        Thread t1 = new Thread(mt);
        //调用start()方法
        t.start();*/
        /*第二种写法
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程"+i);
                }
            }
        });
        t2.start();*/
        //第三种写法
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程" + i);
            }
        });
        t3.start();

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

结果展示: 

优点: 线程任务类只是实现接口, 可以继续继承类实现接口,扩展性强

缺点: 编程要多一层对象包装, 如果线程有执行结果无法直接返回

3.利用Callable, FutureTask接口实现

步骤:

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

(2)用FutureTask把Callable对象封装成线程任务对象

(3)把线程任务对象交给Thread处理

(4)调用Thread的start方法启动线程

(5)线程执行完毕后, 通过FutureTask的get方法获取执行结果

代码示范: 第一段是类的代码, 第二段是测试的代码

import java.util.concurrent.Callable;

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;
        for (int i = 0; i <= n; i++) {
            sum += i;
        }
        return "结果是"+sum;
    }
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Test2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //子线程1的代码
        //创建任务类对象
        MyCallable call1 = new MyCallable(50);
        //用FutureTask把Callable对象封装成线程任务对象
        FutureTask<String> f1 = new FutureTask<>(call1);
        //把线程对象交给Thread处理
        Thread t1 = new Thread(f1);
        //通过调用start方法启动线程
        t1.start();
        //线程执行完毕后,通过FutureTask的get方法获取任务执行结果
        String s1 = f1.get();
        System.out.println("子线程1的"+s1);
        //子线程2的代码
        MyCallable call2 = new MyCallable(100);
        //用FutureTask把Callable对象封装成线程任务对象
        FutureTask<String> f2 = new FutureTask<>(call2);
        //把线程对象交给Thread处理
        Thread t2 = new Thread(f2);
        //通过调用start方法启动线程
        t2.start();
        //线程执行完毕后,通过FutureTask的get方法获取任务执行结果
        String s2 = f2.get();
        System.out.println("子线程2的"+s2);
    }
}

结果展示: 

优点: 线程任务类只是实现接口可以继续继承类和实现接口, 扩展性强

可以在线程完毕后去获取线程执行的结果

缺点: 代码有点复杂

二.Thread的常用方法 

1.Thread的构造方法

            方法名称                                                                          说明
public Thread(String name)                                      可以为当前线程指定名称
public Thread(Runnable target)                         封装Runnable对象成为线程对象
public Thread(Runnable target ,String name )  封装Runnable对象成为线程对象,并指定线程名称

2.Thread获取和设置线程名称

           方法名称                                                                    说明
String getName()                          获取当前线程的名称,默认线程名称是Thread-索引
void setName(String name)     将此线程的名称更改为指定的名称,通过构造器也可以设置线程名称

3.Thread类获得当前线程的对象

                        方法名称                                                                说明
public static Thread currentThread()              返回对当前正在执行的线程对象的引用
代码示范: 第一段类的代码, 第二段是测试的代码

public class MyThread extends Thread{
    public MyThread() {
    }
    //调用父类的方法
    public MyThread(String name) {
        super(name);
    }

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

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

        Thread t1 = new MyThread("一号子线程");
        //t1.setName("一号");//设置线程名称
        t1.start();

        Thread t2 = new MyThread("二号子线程");
        t2.start();
        //输出子线程1的名称
        System.out.println(t1.getName());
        //输出子线程2的名称
        System.out.println(t2.getName());
        //输出主线程的名称
        System.out.println(currentThread().getName());
        //设置主线程名称
        currentThread().setName("主线程");
        for (int i = 0; i < 5; i++) {
            System.out.println(currentThread().getName()+"输出: "+ i);
        }


    }
}

结果展示: 

扩展:Thread类的线程休眠方法
                             方法名称                                                      说明
public static void sleep(long time) 让当前线程休眠指定的时间后再继续执行,单位为毫秒
代码示范: 

mport static java.lang.Thread.currentThread;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 5; i++) {
            if(i==3){
                Thread.sleep(5000);
            }
            System.out.println(currentThread().getName()+"输出: "+i);
        }
    }
}

三.线程安全

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

原因:多个线程同时访问同一个共享资源且存在修改该资源

如: 两个人在同一个账户取钱

代码示范: 

public class Account {
    private String Id;//卡号
    private double money;//账户余额

    public Account() {
    }

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

    /**
     * 获取
     *
     * @return Id
     */
    public String getId() {
        return Id;
    }

    /**
     * 设置
     *
     * @param Id
     */
    public void setId(String Id) {
        this.Id = Id;
    }

    /**
     * 获取
     *
     * @return money
     */
    public double getMoney() {
        return money;
    }

    /**
     * 设置
     *
     * @param money
     */
    public void setMoney(double money) {
        this.money = money;
    }

    public String toString() {
        return "Account{Id = " + Id + ", money = " + money + "}";
    }

    public void drawMoney(double money) {
        //1.谁来取钱
        String name = Thread.currentThread().getName();
        //2.判断钱是否足够
        if (this.money >= money) {
            //3.取钱
            System.out.println(name + "来取钱成功,吐出: " + money);
            //4.更新余额
            this.money -= money;
            System.out.println(name+"来取钱后,剩余: "+this.money);
        }else{
            System.out.println(name+"来取钱失败,余额不足");
        }
    }
}
public class DrawThread extends Thread{
    //接收处理的账户对象
    private Account acc;

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

    @Override
    public void run() {
        //取钱
        acc.drawMoney(100000);
    }
}
public class Test1 {
    public static void main(String[] args) {
        //创建一个共享账户
        Account acc = new Account("abc-123",100000);
        //创建两个线程,代表两个人来取钱
        new DrawThread(acc,"小明").start();
        new DrawThread(acc,"小红").start();
    }
}

结果展示: 

四.线程同步

加锁: 让多个线程实现先后依次访问共享资源

1.同步代码块

(1)作用: 把出现线程安全问题的核心代码上锁

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

(3)格式: synchronized(同步锁对象){

        操作共享资源的代码(核心代码)

}

(4)锁对象的规范要求:

建议使用共享资源作为锁对象

对于实例方法建议使用this作为锁对象

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

(格式:synchronized (类名.class){ })

以上一个取钱的线程安全问题为例

 修改后的结果展示: 

2.同步方法

作用: 把出现线程安全问题的核心方法给上锁

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

格式: 修饰符 synchronized 返回类型 方法名称(形参列表){

        操作共享资源的代码

}

以上一个取钱的线程安全问题为例 修改后的结果展示:

3.Lock锁

为了更清晰的表达如何加锁和释放锁, JDK5以后提供了一个新的锁对象Lock, 更加灵活,方便Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象
             方法名称                                       说明
public ReentrantLock()            获得Lock锁的实现类对象
Lock的API
方法名称                         说明
void lock()                    获得锁
void unlock()                释放锁

代码示范: 使用Lock锁对Account类进行修改

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

public class Account {
    private String Id;//卡号
    private double money;//账户余额
    private final Lock lock = new ReentrantLock();//用final修饰,锁对象是唯一且不可替换的

    public Account() {
    }
    public Account(String Id, double money) {
        this.Id = Id;
        this.money = money;
    }
    public void drawMoney(double money) {
        //1.谁来取钱
        String name = Thread.currentThread().getName();
        lock.lock();//上锁
        //2.判断钱是否足够(核心代码)
        try {
            if (this.money >= money) {
                //3.取钱
                System.out.println(name + "来取钱成功,吐出: " + money);
                //4.更新余额
                this.money -= money;
                System.out.println(name + "来取钱后,剩余: " + this.money);
            } else {
                System.out.println(name + "来取钱失败,余额不足");
            }
        } finally {
            lock.unlock();//解锁
        }
    }

    /**
     * 获取
     *
     * @return Id
     */
    public String getId() {
        return Id;
    }

    /**
     * 设置
     *
     * @param Id
     */
    public void setId(String Id) {
        this.Id = Id;
    }

    /**
     * 获取
     *
     * @return money
     */
    public double getMoney() {
        return money;
    }

    /**
     * 设置
     *
     * @param money
     */
    public void setMoney(double money) {
        this.money = money;
    }

    public String toString() {
        return "Account{Id = " + Id + ", money = " + money + "}";
    }


}

结果展示: 

五.线程通信

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

所谓线程通信就是线程间相互发送数据,线程通信通常通过共享一个数据的方式实现
线程间会根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做

线程通信常见模型

生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费数据(一产一消)

要求:生产者线程生产完数据后,唤醒消费者,然后等待自己(把自己关闭, 把cpu占的空间腾出来,提高运行效率);消费者消费完该数据后,唤醒生产者,然后等待自己
 

六.线程池

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

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

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

如何得到线程池对象?

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

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

ThreadPoolExecutor构造方法的参数说明
public ThreadPoolExecutor(int corePoolSize,

                                                int maximumPoolSize,

                                                long keepAliveTime,

                                                TimeUnit unit,

                                                BlockingOueue<Runnable> workQueue ,

                                                ThreadFactory threadFactory,
                                                RejectedExecutionHandler handler)
参数一:指定线程池的线程数量(核心线程): corePoolSize------->不能小于0
参数二:指定线程池可支持的最大线程数: maximumPoolsize------>最大数量>=核心线程数量
参数三:指定临时线程最大存活时间: keepAliveTime-------->不能小于0
参数四:指定存活时间的单位(秒、分、时、天): unit--------->时间单位
参数五:指定任务队列(在等待执行的任务): workQueue---------->不能为null
参数六:指定用哪个线程工厂创建线程: threadFactory---------->不能为null
参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler--------->不能为null

2.线程池常见面试题

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

3.ExecutorService的常用方法

       方法名称                                                                           说明
void execute(Runnable command)   执行任务/命令,没有返回值,一般用来执Runnable任务
Future<T> submit(Callable<T> task)   执行任务,返回未来任务对象获取线程结果,一般拿来执行 callable 任务
void shutdown()                                                  等任务执行完毕后关闭线程池
List<Runnable> shutdownNow()       立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

代码示范: 处理Runnable任务

public class MyRunnable extends Thread{
    @Override
    public void run() {
        for (int i = 1; i < 4; i++) {
            System.out.println(Thread.currentThread().getName()+"输出: "+i);
        }
        System.out.println(Thread.currentThread().getName()+"完成一次");
    }
}
import java.util.concurrent.*;

public class Test1 {
    public static void main(String[] args) {
        /*public ThreadPoolExecutor(int corePoolSize,
                                    int maximumPoolSize,
                                    long keepAliveTime,
                                    TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue,
                                    ThreadFactory threadFactory,
                                    RejectedExecutionHandler handler)*/
        ExecutorService pool = new ThreadPoolExecutor(3,6,4
                ,TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),Executors.defaultThreadFactory()
                ,new ThreadPoolExecutor.AbortPolicy());
        Runnable mr = new MyRunnable();
        pool.execute(mr);
        pool.execute(mr);
        pool.execute(mr);
        pool.execute(mr);
        pool.execute(mr);
        pool.execute(mr);
    }
}

结果展示: 

 代码示范: 处理Callable任务

import java.util.concurrent.Callable;

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;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName()+"输出: "+sum;
    }
}
import java.util.concurrent.*;

public class Test2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = new ThreadPoolExecutor(3,6,4
                , TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory()
                ,new ThreadPoolExecutor.AbortPolicy());
        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));
        Future<String> f5 = pool.submit(new MyCallable(500));

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

结果展示: 

4.Executors得到线程池对象的常用方法

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象
(1)public static ExecutorService newCachedThreadPool()                                                  线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉
(2)public static ExecutorService newFixedThreadPool(int nThreads)            创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它
(3)public static ExecutorService newSingleThreadExecutor ()                        创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程
(4)public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)    创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务
注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的

代码示范: 

public class MyRunnable extends Thread{
    @Override
    public void run() {
        for (int i = 1; i < 4; i++) {
            System.out.println(Thread.currentThread().getName()+"输出: "+i);
        }
        System.out.println(Thread.currentThread().getName()+"完成完成本轮输出");
    }
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test3 {
    public static void main(String[] args) {
        //创建固定线程数量的线程池
        ExecutorService pool = Executors.newFixedThreadPool(2);
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());

    }
}

结果展示: 

缺点: Executors不适合大型互联网场景的线程池方案
建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则规避资源耗尽的风险

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值