线程技术概述

 什么是多线程?

顾名思义,就是从软硬件上实现多条执行路径的技术。多线程在我们平时的日常生活中也有很多的使用,例如在你登录QQ空间的时候,你输入账号密码进行登录这是属于你自己的两个线程,但是在同时也有其他人也在登录QQ空间,此时服务器那边会再给他分配两个线程,这就是多线程的应用。

一、多线程的创建

多线程的创建一共有三种方式,分别为继承Thread类,定义Runable实现类,实现Callable接口。

1.继承Thread类

Run方法中的代码就是线程用来执行的任务代码。 那么为什么不直接用类名直接调用run方法使用呢?为什么还要再用start方法来使用。如果你是直接调用run方法的话,那么他就会被当成跟main方法中的普通语句,就不会给他当成另一条线程去执行任务,此时程序中只有一条主线程。因此需要用start方法启动线程调用run方法,只有调用了start方法,他才会开启一条新的线程。

使用注意: 需要把子线程任务放在主线程前,如果放在后面的话,就是主线程先跑完再轮到子线程去跑,这样的话就起不到多线程的优势了。

 代码:

package com.itheima.d1_create;

/*
Thread线程的创建方式
方式一 定义一个子类MyThread 继承线程类 Thread 重写 run()方法
 */
public class ThreadDemo1 {
    public static void main(String[] args) {
        //创建线程对象
        Thread t= new MyThread();
        //启动线程
        t.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行" + i);
        }
    }
}

//1.创建类继承run方法
class MyThread extends Thread
{
    @Override
    public  void run()
    {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程执行" + i);
        }
    }
}

因为方式一是继承Thread类重写run方法,因此这个实现类就不能再继承其他类了,不利于扩展。

但是他的书写格式很简单。

2.定义Runable实现类

 Thread类的构造器

这种方法用到的就是第二种构造器。因为Runnable接口是函数式接口,因此可以用匿名内部类的方式去简写代码。

代码:

package com.itheima.d1_create;

public class ThreadDemo2 {
    public static void main(String[] args) {
        //创建任务对象
       // Runnable target = new Myrunnable();

        //匿名内部类写法
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程执行");
                }
            }
        };
        //把任务对象交给Thread处理
        Thread t = new Thread(target);
        //启动线程
        t.start();

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

        new Thread(() -> {
            for (int i = 0; i <5 ; i++) {
                System.out.println("子线程2" + i);
            }}
        ).start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行" + i);
        }
    }
}
//定义任务类实现Runnable接口
//class Myrunnable implements Runnable
//{
//
//    @Override
//    public void run() {
//        for (int i = 0; i < 5; i++) {
//            System.out.println("子线程执行" + i);
//        }
//    }
//}

3.实现Callable接口

在上两种方式中,两个方法都是通过run方法来完成任务,然后run方法是void类型,因此如果任务需要返回值的话,上面两种方法是无法满足条件的,这时就用到了第三种方法来解决上两种方法的短板。

 因为Callable接口是泛型接口,因此实现接口的时候需要申明线程完毕任务之后的数据类型,并且用FutureTask类创建对象,还可以用他特有的方法,get方法获取任务完毕后的返回结果。下面FutureTask对象会等待线程任务执行结束之后在返回值,因此返回的值一定是最终值。

 代码:

package com.itheima.d1_create;

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

public class ThreadDemo3 {
    public static void main(String[] args) {
        //创建callable任务对象
        Callable<String> call = new Mycallable(100);
        //把Callable任务对象交给  FutureTask对象
        //FT作用1: 是Runnable的对象 (实现了Runnable接口) 可以交给Thread
        //作用2 : 可以在线程结束之后调用特有的get方法拿到执行完成的结果。
        FutureTask<String> ft = new FutureTask<>(call);

        Thread t= new Thread(ft);

        t.start();

        //get方法一定是在线程结束之后才获取其返回值的。
        try {
            String rs = ft.get();
            System.out.println("执行结果" + rs);
        }  catch ( Exception e) {
            e.printStackTrace();
        }

    }
}
//定义任务类实现callable接口  声明线程任务结束之后返回的结果的数据类型
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 = 0; i <= n ; i++) {
            sum += i;
        }
        return "执行结果为" + sum;
    }
}

 

 三种方式优缺点对比

二、Thread的常用方法

常用方法:getname() 获取线程名, setname()设置线程名,currentThread()获取当前线程对象(也就是谁现在执行就是哪个线程对象), 这里还用到了Thread构造器中的第一种构造器,直接在构造器中给线程设置名称,因此需要在实现类中重写一个带参构造器,又因为name属性是Thread中的属性,因此我们只需要在子类带参构造器中调用父类构造器就可以了。
代码:

package com.itheima.d2_Api;

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(Thread.currentThread().getName() +"输出:"+  i);
        }
    }
}
package com.itheima.d2_Api;

public class ApiDemo {
    /*
    给线程命名
    currentThread 获取当前线程对象
    setname 给线程取名
    getname 给线程取名
     */
    public static void main(String[] args) {
        Thread t = new Mythread("1号");
      //  t.setName("1号线程");
        t.start();
       // System.out.println(t.getName());

        //哪个线程执行它, 它就得到哪个线程对象(当前线程对象)
        //主线程名称就叫main
        Thread m = Thread.currentThread();
        System.out.println(m.getName());
    }
}

在执行完这个方法后,该线程的任务会停止你设置的毫秒数时间。 

 代码”:

package com.itheima.d2_Api;

public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i <= 5; i++) {
            System.out.println("输出" + i);
            if (i == 3)
            {
                //线程休眠
                Thread.sleep(3000);
            }
        }
    }
}

三、线程安全

当多个线程同时操作同一个共享资源的时候,可能会诱发业务安全性问题,这种问题称为线程安全问题。接下来会用一个案例来演示这种问题。

 分析问题:首先需要定义一个账户类作为共同账户(共享资源),创建线程类,在任务中进行取钱任务,然后在类中定义取钱功能,对于能否取钱进行一个简单的逻辑判断,最后创建两个线程小红和小明同时进行取钱,看看会不会出问题。

代码:

package com.itheima.d3_ThreadSafety;

public class Account {
    private String name;
    private int money;

    public Account() {
    }

    public Account(String name, int money) {
        this.name = name;
        this.money = money;
    }
    
    public void drawMoney(int 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("余额不足");
        }
    }
    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

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

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

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

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


}
package com.itheima.d3_ThreadSafety;

import com.itheima.d2_Api.Mythread;

public class MyThread extends Thread{
    Account acc = new Account();
    public MyThread(Account acc,String name)
    {
        super(name);
        this.acc = acc;
    }

    @Override
    public void run ()
    {
        acc.drawMoney(10000);
    }
}

 

package com.itheima.d3_ThreadSafety;

/*
模拟取钱案例
 */
public class ThreadDemo  {
    public static void main(String[] args) {
        Account acc = new Account("ICBC",10000);
        Thread t1 = new MyThread(acc,"小明");
        Thread t2 = new MyThread(acc,"小红");
        t1.start();
        t2.start();
    }
}

出现这种问题的原因是因为两个线程是同时访问了账户类,因为线程是交替执行的,并且任务中都执行了该方法,因此如果小明先进入判断条件并且进行取钱操作的话,这个时候余额还没有更新,但是小红这个时候也进入了条件判断,因此就会出现错误。

四、线程同步

 线程同步的作用就是为了解决,多线程的线程安全问题,核心思想是给共享资源上锁,这样的话即使是多个线程同时执行任务访问共享资源,也只能先让一个进去,这样就实现了多个线程先后访问共享资源。

 

 线程同步实现方式一: 同步代码块  synchronized{} 同步方法:synchronize关键字直接写在方法名上

锁对象用任意唯一的对象的缺点:会影响其他无关线程的执行,因为同步代码块的原理是使用同步锁对象,而同步锁对象对于其他线程也是唯一的,因此如果其他线程也想执行同步代码块中的操作的话,就会被拦截。因此在设置同步锁对象的时候,规范上建议使用共享资源作为同步锁对象,例如,上面案例中小红和小明的共享账户acc对象,因为在上面的方法是实例方法,属于对象,当创建对象的时候,实例方法也会跟着加载,因此这个时候这个共享账户对象是只属于小红和小明的唯一对象,因此就可以使用this关键字来代替,谁调用的这个方法,那么就用该对象来当锁。因此对于实例方法建议使用this作为对象,对于静态方法建议使用字节码对象,也就是类对象作为锁对象。

同步方法的底层原理与同步代码块一样,只不过作用范围不一样,同步方法的作用范围比同步代码块更大一些。

 

package com.itheima.d4_thread_synchronized_code.d3_ThreadSafety;

public class Account {
    private String name;
    private int money;

    public Account() {
    }

    public Account(String name, int money) {
        this.name = name;
        this.money = money;
    }

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

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

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

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

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

    public void drawMoney(int 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("余额不足");
        }
    }
    }
}

 方式二:Lock锁

使用方式:

代码:

package com.itheima.d5_thread_synchronized_lock.d3_ThreadSafety;

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

public class Account {
    private String name;
    private int money;
    //用final修饰 不能改变锁  例如 lock = null
    private final Lock lock = new ReentrantLock();

    public Account() {
    }

    public Account(String name, int money) {
        this.name = name;
        this.money = money;
    }

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

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

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

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

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

    public void drawMoney(int money) {
        String name = Thread.currentThread().getName();
        lock.lock();
        try {

            if (this.money >= money)
            {
                System.out.println(name + "来取钱了,吐了" + money);
                this.money -= money;
                System.out.println(name + "取钱成功,余额"+ this.money);
            }else {
                System.out.println("余额不足");
            }
        } finally {
            lock.unlock();
        }

    }
}

建议将解锁方法写在finally块中,这样即使try中出现异常也必须执行解锁这行代码,这样就不会导致死锁问题(因为无法解锁导致其他线程也访问不了该资源)的出现。 

五、线程池 

1.线程池概述

线程池复用技术:当一个线程执行完任务之后他不会死亡,而是去处理新的任务。 

不用线程池如果每次都有一个新的请求,每次都会创建一个新的线程,这样会造成内存开销很大,同时也会抢夺CPU处理,导致其他方面的线程无法执行。

工作原理:

假如此时线程池中有三个任务,有很多任务进入任务队列来申请线程调度,这个时候线程池中的线程就会与队列中的任务去绑定,此时队列后面的任务就会进行等待,当线程执行完前面的任务后,他会继续调用队列中等待的任务。任务队列中的任务都是实现了Runable和Callable接口的任务。

2.线程池实现的API、参数说明

创建线程池对象一共有两种方法  ExecutorService接口是线程池中的顶级接口

第一种方式中的参数分别代表了什么:

/*
创建线程池对象
 public ThreadPoolExecutor(int corePoolSize,   核心线程数
                      int maximumPoolSize,     指定线程池最大线程数(核心线程数与临时线程数总和)
                      long keepAliveTime,      指定临时线程最大存活时间
                      TimeUnit unit,           最大时间规定单位是 秒 分钟 小时等等...
                      BlockingQueue<Runnable> workQueue,   任务队列最大数 任务队列是实现了 Runnable和Callable接口
                      ThreadFactory threadFactory,       线程工厂(用于创建线程)里面封装的是创建线程的三种方法
                      RejectedExecutionHandler handler)   如果任务队列满了,新任务来了的话需要去拒绝
                       核心线程和临时线程都在忙 任务队列也满了,新的任务过来的时候才会开始任务拒绝
 */

3.线程池处理Runnable任务

 线程池处理Runnable方法的执行流程:  创建Runnable实现类,创建线程池,使用线程池的execute方法调用Runnable实现类任务。

线程池的特性:多线程之间并发执行任务,不会像线程一样执行完任务就死亡,线程池在执行完队列中的任务之后,不会停止运行,而是会继续等待任务调用。

 代码中测试了线程池参数中特性: 

package com.itheima.ThreadPoolDemo1;

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,   任务队列最大数 任务队列是实现了 Runnable和Callable接口
                              ThreadFactory threadFactory,       线程工厂(用于创建线程)里面封装的是创建线程的三种方法
                              RejectedExecutionHandler handler)   如果任务队列满了,新任务来了的话需要去拒绝
                               核心线程和临时线程都在忙 任务队列也满了,新的任务过来的时候才会开始任务拒绝
         */
        //线程池的创建
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 6,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(5) ,new ThreadPoolExecutor.AbortPolicy());
        // 给任务线程池处理
        Runnable target = new MyRunnable();

        //让线程池中的线程执行任务
        //现在每个任务都绑定了一个线程。
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
//        pool.execute(target);
//        pool.execute(target);//也执行的过来 不需要再去额外创建临时线程

        //给任务加个线程休眠看看会不会创建新的临时线程    不会 等待队列中最大长度为5
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);

        //队列满 线程休眠  创建临时线程
        pool.execute(target);
        pool.execute(target);

        //不创建,拒绝策略被触发
        pool.execute(target);

    }
}

  注意:shutdownNow方法一般不会去使用,这个方法即使你线程中的任务没有完成,他也会立即关闭线程池。

 最后一个策略,当你的线程池开始拒绝任务的时候,这个任务会绕过线程池,利用main方法中总线程进行处理。

4.线程池处理Callable任务 

  线程池处理Callable任务方法的执行流程:  创建Callable实现类,创建线程池,使用线程池的submit方法调用Callable实现类任务,这种任务与Runnable方法的不同之处在于任务可以返回值,因此这个方法会返回一个Future的任务对象,利用这个任务对象可以调用get方法获取返回值,Futer任务对象是TaskFuter的父类。

 代码:

package com.itheima.ThreadPoolDemo1;

import java.util.concurrent.*;

public class ThreadPoolDemo2 {

    //线程池的创建
    //线程池处理Callable任务
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 6,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(5) ,new ThreadPoolExecutor.AbortPolicy());

        //FutreTask的实现接口的父类接口
        Future<String> f1=  pool.submit(new MyCallable(100));
        Future<String> f2=  pool.submit(new MyCallable(200));
        Future<String> f3=  pool.submit(new MyCallable(300));

        //获取返回值
        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
    }
}

5.Executors工具类实现线程池

 线程池第二种创建方式:通过Executors工具类创建。 这些工具类中有个致命的缺点,这四个方法的底层中请求队列的长度为 Integer.maxValue 也就是几乎可以有无限个请求,如果线程来不及处理,内存中就会堆积大量的请求,导致OOM(内存溢出)。因此在大型并发系统中不会用工具类来创建线程池对象。

 第二个线程只能创建固定线程的线程池,因此如果三个线程都因为执行异常结束,是没有临时线程来帮他执行新的任务的。

 六、定时器

定时器在我们以后的开发中会用到很多,主要的实现方式有两种,通常我们会用第二种。

 1.Timer类定时器

Timer类创建定时器中的参数说明(任务,延迟执行时间,执行周期)

然而这种定时器有个致命的缺点,因为Timer是单线程处理的,因此如果线程中的任务出现执行异常,那么后面的任务就不会再执行或者他会违背定时器的原则,比如下面的代码,如果按照正常执行流程第二个任务应该再第一个任务后两秒执行,但是因为第一个线程中线程睡眠所以延迟了五秒,因此我们通常使用第二种定时器方法。

2.ScheduledExecutorService (之前工具类中的方法

通过Executors工具类创建线程池对象再通过调用方法来创建定时器,方法中的参数(Runnable类任务,开始日期,执行周期,执行周期单位(秒 分 时 天)) ,因为这个定时器是基于线程池实现的,因此很好的补足了上面定时器创建的短板,当一个线程中任务出现异常的时候,他不会影响到其他任务的执行,任务会调度其他线程来执行。

 代码:

package com.itheima.d7_timer;

import java.util.Date;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class TimerDemo2 {
    public static void main(String[] args) {
        //创建线程池做定时器
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);

        //开启定时任务
        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行 AAA" + new Date());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },0,2, TimeUnit.SECONDS); //1.定时任务 2 初始化延迟时间 3.周期时间 4.周期时间单位

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()  + "正在执行 BBB" + new Date());
            }
        },0,2,TimeUnit.SECONDS);
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值