多线程基础

目录

一.线程概述

1.进程

2.线程

3.进程与线程关系图:

二.线程的创建

1.继承Thread类

2.实现Runnable接口

3.实现Callable接口

三.线程的常用方法

1.线程命名和获取线程名

2.线程休眠

四.线程安全和多线程同步

1.线程安全

2.多线程同步

1.案例回顾

2.线程同步的核心思想

3.用synchronized关键字加锁:

4.用Lock对象加锁

5.线程死锁

五.线程的生命周期及状态转换

1.线程的状态

2.Java线程的状态

3.线程各状态间的转换

六.线程的调度

1.基本概念

2.线程优先级

3.线程休眠

4.线程让步

5.线程插队

七.多线程通信

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

2.线程通信常见形式

3.线程通信实际应用场景

4.Object类的等待和唤醒方法:

5.案例及代码实现

八.线程池

1.线程池概述

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

3.线程池处理Runnable任务

4.线程池处理Callable任务

5.Executors工具类实现线程池

九.定时器

1.概念

2.定时器的2种实现方式

3.Timer定时器

4.ScheduledExecutorService定时器


一.线程概述

1.进程

进程:在一个操作系统中,每个独立执行的程序都可以称之为一个进程。

2.线程

2.1.线程:一个进程中可以由多个执行单元同时运行,来完成一个或多个程序任务,这些执行单元就叫线程。一个进程中至少有一个线程。当Java程序启动时,就会创建一个进程,该 进程会默认创建一个线程,这个线程会去运行main()方法中的代码。正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的。

【注意】CPU一次只能只能执行一个线程,多线程的本质就是CPU轮流在多个单线程之间切换,只不过CPU切换的速度极快,让人觉得多个线程在同时执行。

2.2.线程的并发

并发:CPU分时轮询的执行线程。

2.3.线程的并行

并行:同一个时刻同时在执行。如果电脑是4核8线程的,那么就是并行就是CPU最多让8个线程同时执行,而并发就是CPU在切换线程时最多8个8个地切换,因此,多个线程其实是并发与并行同时进行的。

 

3.进程与线程关系图:

 

二.线程的创建

1.继承Thread类

1.1.实现过程:

(1)定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法

(2)创建MyThread类的对象

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

1.2.代码实现:

package cn.zcj.Thread;

public class ThreadDemo_01 extends Thread{
    public static void main(String[] args) {
        //3.new一个线程
        MyThread myThread = new MyThread();
        //4.调用start()方法启动线程,此时实际调用的依旧是run方法
        myThread.start();
        //4.1.run()方法
        //myThread.run();
        for(int i = 51;i<100;i++){
            System.out.println("主线程输出:"+i);
        }
    }
}

/**
 * 1.定义一个线程类继承Thread类
 */
class MyThread extends Thread{
    //2.重写run方法,里面是定义线程以后要干什么的
    @Override
    public void run() {
        for(int i = 0;i<50;i++){
            System.out.println("子线程输出:"+i);
        }
    }
}

 输出结果:

1.3.优缺点

优点:编码简单

缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

1.4.思考两个问题:

(1)为什么代码中把开启线程的start方法放在main方法的for循环之前?

答:这样才能呈现出多线程效果,如果main方法的for循环之后再开启线程,那么整个过程就相当于main方法的for业务已经执行完了再去执行新建线程里的业务,就是一个单线程效果了。

(2)调用start方法与调用run方法的区别?

答:start方法是开启线程,run方法是相当于调用普通方法。

2.实现Runnable接口

2.1.实现过程:

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

(2)创建MyRunnable任务对象

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

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

2.2.代码实现一:

package cn.zcj.Thread;

public class ThreadDemo_02{
    public static void main(String[] args) {
        //3.创建一个任务对象
        MyRunnable myRunnable = new MyRunnable();
        //4.把任务对象交给Thread处理
        Thread thread = new Thread(myRunnable);
        //5.启动对象
        thread.start();

        for (int i = 51;i < 100;i++){
            System.out.println("主线程任务:"+i);
        }
    }
}

/**
 * 1.定义一个线程任务,实现Runnable接口
 */
class MyRunnable implements Runnable{
    //2.重写run()方法,定义线程的执行任务
    @Override
    public void run() {
        for (int i = 0;i < 50;i++){
            System.out.println("子线程任务:"+i);
        }
    }
}

2.3.输出结果:

2.4.优缺点:

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

缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。

2.5.另一种写法(与第一种写法效果相同):

package cn.zcj.Thread;

public class ThreadDemo_02Other {
    public static void main(String[] args) {
        //匿名内部类写法
        Runnable myRunnable = new Runnable(){
            @Override
            public void run() {
                for (int i = 0;i < 100;i++){
                    System.out.println("子线程任务:"+i);
                }
            }
        };
        //把任务对象交给Thread处理
        Thread thread = new Thread(myRunnable);
        //启动对象
        thread.start();
        for (int i = 101;i < 200;i++){
            System.out.println("主线程任务:"+i);
        }
    }
}

 

3.实现Callable接口

3.1.实现过程:

(1)得到任务对象

a.定义类实现Callable接口,重写call方法,封装要做的事情。

b.用FutureTask把Callable对象封装成线程任务对象。

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

(3)调用Thread的start方法启动线程,执行任务

(4)线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。

3.2.代码实现:

package cn.zcj.Thread;

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

public class ThreadDemo_03 {
    public static void main(String[] args) {
        //3.创建Callable任务对象
        MyCallable myCallable = new MyCallable(10);
        //4.把Callable任务对象,交给FutureTask对象
        //FutureTask对象作用一:是Runnable的对象(FutureTask实现了Runnable接口),可以交给Thread
        //FutureTask对象作用二:可以在线程执行完毕之后通过其get方法得到线程执行完成的结果
        FutureTask<Integer> call = new FutureTask<>(myCallable);
        //5.交给线程处理
        Thread thread = new Thread(call);
        //6.启动线程
        thread.start();
        //7.打印线程返回的结果
        //线程返回的结果有两种:正常执行结果和异常结果,所以这里要抓取异常
        //如果线程正在执行中,get方法来拿线程的返回结果,是拿不到值的,必须等待线程执行结束
        try {
            System.out.println(call.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

//1.定义一个任务类,实现Callable接口,应该申明线程执完毕后的结果的数据类型
class MyCallable implements Callable<Integer>{
    private Integer s = 0;
    public MyCallable(Integer s){
        this.s = s;
    }
    //2.重写call方法(任务方法)
    @Override
    public Integer call() throws Exception {
        Integer sum = new Integer(0);
        for (int i = 0 ; i< s; i++){
            sum += i;
        }
        return sum;
    }
}

3.3.输出结果:

3.4.优缺点:

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

优点二:可以在线程执行完毕后去获取线程执行的结果。

缺点:编码复杂一点。

3.5.三种创建线程的方式对比:

 

三.线程的常用方法

1.线程命名和获取线程名

1.1.方法参数

 

1.2.代码案例:

package cn.zcj.Thread;

public class ThreadDemo_04Name {
    public static void main(String[] args) {
        //线程取名方式一:
        MyThread04 t1 = new MyThread04("1号");
        t1.start();
        //线程取名方式二:
        //t1.setName("1号");
        System.out.println(t1.getName());
        
        MyThread04 t2 = new MyThread04("2号");
        t2.start();
        //t2.setName("2号");
        System.out.println(t2.getName());
        //哪个线程执行它,它就得到哪个线程的对象(即当前线程对象)
        //主线程的名称就叫main
        Thread tMain = Thread.currentThread();
        for (int i=0;i<6;i++) {
            System.out.println(tMain.getName()+"线程输出:"+i);
        }
    }
}

class MyThread04 extends Thread{

    public MyThread04() {
    }

    public MyThread04(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i =0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"线程输出:"+i);
        }
    }
}

 1.3.输出结果:

2.线程休眠

2.1.方法参数

 

2.2.代码实现

package cn.zcj.Thread;

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

 

四.线程安全和多线程同步

1.线程安全

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

【补充】多个线程去读取同一个共享资源是不会存在线程安全问题的,多个线程去修改同一个共享资源才会存在线程安全问题。

1.2.线程安全问题出现的原因:多个线程同时访问同一个共享资源且存在修改该资源。

1.3.线程不安全的案例模拟

1.3.1.需求

小明和小红花同时去银行取钱,账户余额10万,小红和小明每人都取10万;不加锁,银行最终亏损10万。

1.3.2.需求分析

①:需要提供一个账户类,创建一个账户对象代表2个人的共享账户。

②:需要定义一个线程类,线程类可以处理账户对象。

③:创建2个线程对象,传入同一个账户对象。

④:启动2个线程,去同一个账户对象中取钱10万。

1.3.3.代码实现:

package cn.zcj.Thread;

public class ThreadDemo_06 {
    public static void main(String[] args) {
        Account account = new Account("123456", 10000d);
        //创建线程并开启线程
        new ThreadMoney(account,"小红").start();
        new ThreadMoney(account,"小明").start();
    }
}

//账户对象
class Account{
    private String CardId;
    private Double money;

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

    public String getCardId() {
        return CardId;
    }

    public void setCardId(String cardId) {
        CardId = cardId;
    }

    public Double getMoney() {
        return money;
    }

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

    public static void f(){
        synchronized (Account.class){
            System.out.println("静态方法加锁");
        }
    }

    public void drawMoney(double money){
        //1.获取当前线程是谁来取钱
        String name = Thread.currentThread().getName();
        //this == account共享账户对象
            //2.判断账户余额是否充足
            if(this.money >= money){
                //3.取钱
                System.out.println(name+"来取钱成功,取出:"+money);
                //4.更新余额
                this.money -= money;
                System.out.println(name+"取钱后,还剩:"+this.money);
            }else {
                //5.余额不足
                System.out.println(name+",您的余额不足!");
            }
    }
}

//处理业务的线程对象
class ThreadMoney extends Thread{
    //接收处理的账户对象
    private Account account;

    public ThreadMoney(){
    }

    //提供可以给线程取名的构造方法
    public ThreadMoney(Account account,String name){
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        //取钱的方法
        account.drawMoney(10000);
    }
}

1.3.4.输出结果:

2.多线程同步

1.案例回顾

(1)取钱案例出现问题的原因?

多个线程同时执行,发现账户都是够钱的。

(2)如何才能保证线程安全呢?

让多个线程实现先后依次访问共享资源,这样就解决了安全问题

2.线程同步的核心思想

线程同步的核心思想就是加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

3.用synchronized关键字加锁:

3.1.同步方法----在方法上面使用synchronized加锁

略。

3.2.同步代码块----将某一段代码块包起来用synchronized加锁

synchronized(lock){
    //操作共享资源代码
    ...
}

  解释:

上述代码中,lock是一个锁对象,是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位设置为0。当一个新的线程执行到这个同步代码块时,由于锁对象标志位置的值为0,新的线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位又会被设置为1,新的线程才能进入同步代码块执行其中的代码。如此循环往复。

图解:

3.3.案例改进:在上文1.3.3的代码中,给核心代码块加锁即可

 运行结果:线程安全

3.4.同步方法和同步代码块谁更好?

答:同步方法锁定的范围更大,同步代码块锁定的范围相对更小;由于同步代码块的锁定范围通常比同步方法的锁定范围小,所以执行时消耗的时间也更少,但是不利于代码的整体阅读,实际场景推荐使用同步方法。

3.5.注意事项:

【补充】被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。

3.6.补充:

同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。

4.用Lock对象加锁

4.1.Lock简介

同步锁:即Lock锁。同步锁最大的优势在于可以让某个线程持续获取同步锁失败后返回,不再继续等待,并且同步锁在使用时更加灵活。Lock锁比synchronized关键字锁定的代码资源更加精细,可以精确到具体哪一行。

 

4.2.代码案例:

4.2.1.创建账户对象

package cn.zcj.Thread.threadComunication;

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

public class Account {

    private String CardId;
    //余额,关键信息
    private Double money;
    //final修饰后,锁对象唯一且不可替换,非常专业的写法
    private final Lock lock = new ReentrantLock();

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

    public String getCardId() {
        return CardId;
    }

    public void setCardId(String cardId) {
        CardId = cardId;
    }

    public Double getMoney() {
        return money;
    }

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

    public void keepMoney(double money) {
        //1.获取当前线程是谁来取钱
        String name = Thread.currentThread().getName();
        //2.判断账户余额是否为0
        lock.lock();
        if (this.money > 0) {
            //3.有钱,就取钱
            this.money -= money;
            //4.取钱
            System.out.println(name + "来取钱成功,取:" + money);
            System.out.println(name + "来取钱后,还剩:" + this.money);
        } else {
            //5.没钱,不取钱
            System.out.println(name+"账户余额不为0,暂时不能取钱!");
        }
        lock.unlock();
    }

}

 4.2.2.创建线程对象

package cn.zcj.Thread.threadComunication;

public class ThreadMoney extends Thread{
    private Account account;

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

    @Override
    public void run() {
        //取钱的方法
        account.keepMoney(10000);
    }
}

 4.2.3.测试

package cn.zcj.Thread.threadComunication;

public class Test {
    public static void main(String[] args) {
        Account account = new Account("123456", 10000d);

        new ThreadMoney(account,"小红").start();
        new ThreadMoney(account,"小明").start();
    }
}

4.2.4.测试结果:

4.3.总结:

5.线程死锁

5.1.概念

线程死锁:两个线程在运行时都在等待对方的锁,这样就造成了程序的停滞,这种现象被称为线程死锁。

5.2.代码实现

package cn.zcj.Thread;


class DeadLockThread implements Runnable {
    //定义两个不同的锁对象
    static Object China = new Object();
    static Object England = new Object();
    private boolean flag;

    public DeadLockThread(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            while (true) {
                synchronized (China) {
                    //China锁对象上的同步代码块
                    System.out.println(Thread.currentThread().getName() + "China对象已经被锁");
                    synchronized (England) {
                        //England锁对象上的同步代码块
                        System.out.println(Thread.currentThread().getName() + "England对象已经被锁");
                    }
                }
            }
        } else {
            while (true) {
                synchronized (England) {
                    //England锁对象上的同步代码块
                    System.out.println(Thread.currentThread().getName() + "England对象已经被锁");
                    synchronized (China) {
                        //China锁对象上的同步代码块
                        System.out.println(Thread.currentThread().getName() + "China对象已经被锁");
                    }
                }
            }
        }

    }
}

public class Example {
    public static void main(String[] args) throws InterruptedException {
        //创建两个DeadLockThread对象
        DeadLockThread thread1 = new DeadLockThread(true);
        DeadLockThread thread2 = new DeadLockThread(false);
        //创建线程并开启线程
        new Thread(thread1, "ChinesePeople线程的").start();
        new Thread(thread2, "EnglishPeople线程的").start();
    }
}

  代码解释:

ChinesePeople线程开始执行时,会先将China对象锁住,而EnglishPeople线程开始执行时,也会先将England对象锁住;但是ChinesePeople线程如果要执行完毕就必须获得England锁,同理EnglishPeople线程如果要执行完毕就必须获得China锁,两个线程都需要对方占用的锁,但都无法释放自己所拥有的锁,于是这两个线程都处于了挂起状态,造成死锁。

5.3.输出结果

五.线程的生命周期及状态转换

1.线程的状态

线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换。

理解线程的状态有利于提升并发编程的理解能力。

2.Java线程的状态

Java总共定义了6种状态

6种状态都定义在Thread类的内部枚举类中。如图:

 

线程的6种状态解释:

 

6种状态对应的线程方法

 

3.线程各状态间的转换

 

sleep苏醒前后不会改变锁状态,如果线程sleep前有(或没有)抢到锁对象,那么在线程休眠时,不会将锁对象释放,因此用sleep方法休眠的线程苏醒后由于锁对象依然存在(或不存在),因此不会参与锁竞争去抢锁。但是用wait方法使线程进入等待状态时,当前线程会将锁对象释放,若等待状态的锁对象被唤醒,那么会立即参与到锁竞争中去抢锁。一句话:sleep前后,线程的锁对象状态一致;wait前后,线程的锁对象状态不一致。

六.线程的调度

1.基本概念

线程的调度:一个程序是多线程并发执行的,但是线程不是在同一时刻执行的,某个线程如果想被执行,必须得到CPU的使用权。JVM会按照特定的机制为程序中的每一个线程分配CPU使用权,这种机制被称为线程的调度。

线程调度的两种模型:分时调度模型和抢占式调度模型(JVM默认使用抢占式调度模型)

分时调度模型:让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。

抢占式调度模型:让可运行池中所有就绪态的线程争抢CPU的使用权,而优先级高的线程抢到使用权的概率大于优先级低的线程。

2.线程优先级

2.1.优先级参数:

最高优先级为10,最低优先级为1,普通优先级为5,main方法就是普通优先级,其优先级值为5。

【补充】不同的操作系统对优先级的支持是不同的,不一定能很好地和java中线程的优先级一一对应。因此,在设计程序时,不能将功能的实现依赖于线程的优先级,而是将线程的优先级作为提高程序执行效率的一种手段。

2.2.代码实现:

package cn.zcj.Thread;

public class Example {
    public static void main(String[] args) {
        //分别创建两个Thread线程
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+"输出"+i);
            }
        },"优先级较低的线程");
        Thread thread2 = new Thread(() -> {
            for (int i = 10; i < 20; i++) {
                System.out.println(Thread.currentThread().getName()+"输出"+i);
            }
        },"优先级较高的线程");
        //分别设置优先级,1--10之间,数字越大,优先级越高,
        thread1.setPriority(Thread.MIN_PRIORITY);
        thread2.setPriority(Thread.MAX_PRIORITY);
        //开启两个线程
        thread1.start();
        thread2.start();
    }
}

 2.3输出结果:

3.线程休眠

上文已经介绍,略。

4.线程让步

4.1.线程让步:线程运行到某一条件时,使当前线程暂停,让所有线程再次争夺CPU使用权,抢到CPU使用权的线程可以执行。

【补充】线程调度通过yield()方法实现,虽然yield()方法和sleep()方法有点类似,都是可以让当前正在运行的线程暂停,但是yield()方法不会阻塞该线程,它只是将线程转换为就绪状态,让系统的调度器重新调度一次。

4.2.yield()方法与 wait()方法比较

(1) wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而yield()是让线程由“运行状态”进入到“就绪状态”。

(2) wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁

4.3.代码实现:

package cn.zcj.Thread;

public class Example {
    public static void main(String[] args) {
        //创建两个线程
        YieldThread thread1 = new YieldThread();
        YieldThread thread2 = new YieldThread();
        //开启两个线程
        thread1.start();
        thread2.start();
    }
}

class YieldThread extends Thread{
    @Override
    public void run() {
        for (int i = 0;i < 5;i++){
            System.out.println(Thread.currentThread().getName()+"输出"+i);
            if (2 == i){
                System.out.println("线程让步");
                //线程运行到这里做出让步
                Thread.yield();
            }
        }

    }
}

 4.4.输出结果:

4.5.使用场景:百度

5.线程插队

5.1.线程插队:

当某个线程(main线程)中调用其他线程的join()方法时,调用的线程将被阻塞,直到join()方法加入的线程执行完后它才会继续运行。

5.2.代码实现

package cn.zcj.Thread;

public class Example {
    public static void main(String[] args) throws InterruptedException {
        //创建线程
        EmergencyThread thread1 = new EmergencyThread();
        Thread thread = new Thread(thread1, "thread");
        //开启线程
        thread.start();
        for (int j = 6 ;j<10;j++){
            System.out.println(Thread.currentThread().getName()+"线程输出"+j);
            if (8 == j){
                //调用join()方法
                thread.join();
            }
        }
    }
}

class EmergencyThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0;i < 5;i++){
            System.out.println(Thread.currentThread().getName()+"输出"+i);
        }
    }
}

5.3.输出结果:

5.4.使用场景:百度。

七.多线程通信

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

所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。

2.线程通信常见形式

(1)通过共享一个数据的方式实现。

(2)根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。

3.线程通信实际应用场景

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

要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。

4.Object类的等待和唤醒方法:

 

5.案例及代码实现

案例:小明的父亲、干爹、岳父负责向一个银行账户存钱,小明和小红从银行账户里面取钱,银行账户里面如果有钱,则小明的父亲、干爹、岳父都不能继续存钱,只有等小明或小红将钱取完后才能向账户里面存钱;反之,账户里面如果没钱,小明和小红必须等他人来存钱之后才能取钱。

代码实现:

准备账户对象

package cn.zcj.Thread.lockTest;

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

public class Account {

    private String CardId;
    //余额,关键信息
    private Double money;
    //final修饰后,锁对象唯一且不可替换,非常专业的写法
    private final Lock lock = new ReentrantLock();

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

    public String getCardId() {
        return CardId;
    }

    public void setCardId(String cardId) {
        CardId = cardId;
    }

    public Double getMoney() {
        return money;
    }

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

    /**
     * 存钱行为
     *
     * @param money
     */
    public synchronized void keepMoney(double money) {
        try {
            //1.获取当前线程是谁来存钱
            String name = Thread.currentThread().getName();
            //2.判断账户余额是否为0
            if (this.money == 0) {
                //没钱了,就存钱
                this.money += money;
                //4.取钱
                System.out.println(name + "来存钱成功,存入:" + money);
                System.out.println(name + "来存钱后,还剩:" + this.money);
                //有钱了,等人来取,唤醒别人,等待自己
                this.notifyAll();
                this.wait();
            } else {
                //5.有钱,不存钱
                System.out.println("账户余额不为0,暂时不能存钱!");
                //唤醒其他所有线程,唤醒别人,让自己进入等待状态
                this.notifyAll();
                //锁对象,让当前线程进入等待
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 取钱行为
     *
     * @param money
     */
    public synchronized void drawMoney(double money) {
        try {
            //1.获取当前线程是谁来取钱
            String name = Thread.currentThread().getName();
            //2.判断账户余额是否充足
            if (this.money >= money) {
                //3.线程安全时可以先更新余额,后取钱
                this.money -= money;
                //4.取钱
                System.out.println(name + "来取钱成功,取出:" + money);
                System.out.println(name + "取钱后,还剩:" + this.money);
                //没钱了
                this.notifyAll();
                this.wait();
            } else {
                //5.余额不足
                System.out.println(name + ",您的余额不足!");
                //唤醒其他所有线程,唤醒别人,让自己进入等待状态
                this.notifyAll();
                //锁对象,让当前线程进入等待
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

 准备存钱线程对象

package cn.zcj.Thread.lockTest;

public class KeepThread extends Thread{
    private Account account;

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

    @Override
    public void run() {
        while (true){
            //取钱的方法
            account.keepMoney(10000);
            try {
                Thread.sleep(3000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

准备取钱线程对象

package cn.zcj.Thread.lockTest;

public class DrawThread extends Thread{
    private Account account;

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

    @Override
    public void run() {
        while (true){
            //取钱的方法
            account.drawMoney(10000);
            try {
                Thread.sleep(3000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

测试

package cn.zcj.Thread.lockTest;

public class Test {
    public static void main(String[] args) {
        Account account = new Account("123456", 10000d);

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

        new KeepThread(account,"亲爹").start();
        new KeepThread(account,"干爹").start();
        new KeepThread(account,"岳父").start();
    }
}

测试结果:

八.线程池

1.线程池概述

问:不使用线程池有什么问题?

答:如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。

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

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

问:谁代表线程池?

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

2.1.如何得到线程池对象?

 

3.线程池处理Runnable任务

3.1.准备线程对象

package cn.zcj.Thread.ThreadPool;

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"输出"+i);
        }
        try {
            System.out.println(Thread.currentThread().getName()+"本线程已经休眠");
            Thread.sleep(1000000);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

 3.2.创建线程池并测试

package cn.zcj.Thread.ThreadPool;

import java.util.concurrent.*;

public class RunnablePoolTest {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,
                6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        MyRunnable myRunnable = new MyRunnable();
        pool.execute(myRunnable);
        pool.execute(myRunnable);
        pool.execute(myRunnable);
        pool.execute(myRunnable);
        pool.execute(myRunnable);
        pool.execute(myRunnable);
        pool.execute(myRunnable);
        pool.execute(myRunnable);
        pool.execute(myRunnable);
    }
}

3.3.输出结果:

3.4.参数解释

3.4.1参数解释1:

 

3.4.2.参数解释2:

 

3.5.补充2个问题:

(1)问:临时线程什么时候创建?

答:新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

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

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

4.线程池处理Callable任务

4.1.准备线程对象

package cn.zcj.Thread.ThreadPool02;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    private Integer number;
    public MyCallable(Integer number){
        this.number = number;
    }
    @Override
    public String call() throws Exception {
        Integer sum = 0;
        for (int i = 0;i<number;i++){
            sum += i;
        }
        return Thread.currentThread().getName()+"计算的1--"+number+"和为:"+sum;
    }
}

 4.2.创建线程池并测试

package cn.zcj.Thread.ThreadPool02;

import java.util.concurrent.*;

public class ThreadCallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5,
                6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory());
        Future<String> f1 = pool.submit(new MyCallable(5));
        Future<String> f2 = pool.submit(new MyCallable(10));
        Future<String> f3 = pool.submit(new MyCallable(15));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
    }
}

4.3.测试输出:

4.4.常用方法

 

5.Executors工具类实现线程池

5.1.准备实现线程对象

package cn.zcj.Thread.ThreadPool03;

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"输出"+i);
        }
        try {
            System.out.println(Thread.currentThread().getName()+"本线程已经休眠");
            Thread.sleep(1000000);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

 5.2.测试

package cn.zcj.Thread.ThreadPool03;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolTest {
    public static void main(String[] args) {
        //线程池提供固定数量的线程个数
        //弊端:由于底层代码没有规定排队线程的数量,因此这种方式创建出来的线程池允许无限排队,会内存溢出
        ExecutorService pool = Executors.newFixedThreadPool(3);
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        //由于线程池里面永远只有3个线程,所以第四个线程会阻塞
        pool.execute(new MyRunnable());
    }
}

5.3.常用方法

5.4.安全隐患

 

5.5.总结

(1)Executors工具类底层是基于什么方式实现的线程池对象?

答:线程池ExecutorService的实现类:ThreadPoolExecutor

(2)Executors是否适合做大型互联网场景的线程池方案?

答:不合适。建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。由于Executors底层代码没有规定排队线程的数量,因此这种方式创建出来的线程池允许无限排队,会内存溢出

九.定时器

1.概念

定义:定时器是一种控制任务延时调用,或者周期调用的技术。

应用场景:闹钟、定时邮件发送。

2.定时器的2种实现方式

(1)方式一:Timer

(2)方式二: ScheduledExecutorService

3.Timer定时器

3.1.代码案例:

package cn.zcj.Thread.dingshiqi;

import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo1 {
    public static void main(String[] args) {
        //1.创建定时器
        Timer timer = new Timer();
        //2.调用方法,处理定时任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"定时器执行了。");
            }
            //定时任务开启的5秒后执行第一次定时任务,之后每隔2秒执行一次定时任务
        },5000,2000);
    }
}

 3.2.测试输出:

3.3.方法解释

 

3.4.Timer定时器的特点和存在的问题

(1)Timer是单线程的,一次只能执行一个定时任务,在处理多个任务时按照顺序执行,存在延时与设置定时器的时间有出入----即可能会出现延迟。案例解释:假设程序中有定时器A和定时器B,定时器A是当定时任务开启后马上执行第一次定时任务,之后每隔2秒执行一次定时任务,定时器B也是如此。如果此时定时器A中进行了线程休眠5秒钟,那么此时即使定时器B是每隔2秒执行一次,定时器B也要等定时器A执行完后才能继续执行,那么定时器B本次任务举例上传任务的时间间隔就是5秒,造成时间紊乱。

代码:

package cn.zcj.Thread.dingshiqi;

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo1 {
    public static void main(String[] args) {
        //1.创建定时器
        Timer timer = new Timer();
        //2.调用方法,处理定时任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"定时器A执行了。"+new Date());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //定时任务开启的5秒后执行第一次定时任务,之后每隔2秒执行一次定时任务
        },0,2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"定时器B执行了。"+new Date());
            }
            //定时任务开启的5秒后执行第一次定时任务,之后每隔2秒执行一次定时任务
        },0,2000);
    }
}

  输出结果:

(2)如果有多个定时器,可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行,导致所有线程挂掉。

4.ScheduledExecutorService定时器

方法介绍:

 

4.1.代码案例:

package cn.zcj.Thread.dingshiqi;

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) {
        //1.创建ScheduledExecutorService线程池,做定时器
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
        //2.开启定时任务
        /**
         * 参数说明:
         * initialDelay:第一次定时任务的执行时间
         * period:定时任务执行的周期时间--每隔多长时间执行一次
         * unit:时间单位
         */
        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"线程执行AAA"+new Date());
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },0,2, TimeUnit.SECONDS);

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

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

 4.2.测试输出:

4.3.ScheduledExecutorService的优点

答:基于线程池,某个任务的执行情况不会影响其他定时任务的执行。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值