Java多线程——Java教案(十一)

多线程

参考文章

Java多线程,程序运行的堆栈分析

1. 多线程概述

进程与线程的关系

**进程:**一个应用程序,进程与进程之间不能进行数据共享。一个程序可以启动多个线程。

**线程:**一个进程中的执行场景/执行单元,线程与线程之前可以进行数据共享。

进程与线程的关系

进程和线程之间的关系就好比餐厅和员工。一个餐厅A有多个员工,员工与员工之间可以共享这个餐厅A的资源。餐厅B是另一个餐厅,餐厅A和餐厅B之间不能进行数据共享。

QQ音乐,和穿越火线是两个进程,他们是独立的,不共享资源。

Java中的进程与线程:

对Java程序而言,但我们使用 java HellWorld运行Java文件时,会先启动JVM虚拟机,JVM就是一个进程,JVM再启动一个主线程调用main方法,同时启动一个垃圾回收线程负责看护,回收垃圾。

一个Java程序至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。

J**ava程序中,线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈。**如果有10个线程,就会有十个栈,每个栈之间互不干扰,各自执行各自的,这就是多线程并发。

火车站的卖票窗口就是一个多线程,你可以在任何一个窗口买票,在窗口A买票并不影响在窗口B买票的乘客。火车票是他们的共享资源。

Java引入多线程,主要是为了提高程序的处理效率。

堆,方法取,共享栈

Java程序中,线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈。如果有10个线程,就会有十个栈,每个栈之间互不干扰,各自执行各自的。

**问题:**使用了多线程后,main方法结束,程序是否就一定结束?

main方法结束只是主线程结束,主栈空了,但是其他的栈(线程)可能还在运行

image-20211204144459273

image-20211206101405825

多线程并发

单核CPU:

不能做到多线程并发,但是可以给人一种多线程并发的错觉。单核cpu在一个时间点上只能处理一件事情但是由于CPU处理极快,多个线程之间频繁切换,给人的感觉是多个事情同时在做。

线程A:播放音乐,线程B:运行魔兽游戏。

线程A与线程B的频繁切换执行,给人一种音乐和游戏同时运行,给我们并发的错觉。

多核CPU

多核CPU在同一个时间节点上,可以真正的多个进行并发执行。

多线程并发

线程a执行线程a的,线程b执行线程b的,二者不会相互影响。

image-20211204150153258

2. 创建线程

创建线程的方式

  1. 继承Thread类并重写run方法
  2. 实现Runnable接口并重写run方法(常用)
  3. 使用匿名内部类并重写run方法

启动线程

调用线程的.start()方法

继承Thread类

public class ThreadTest01 {
    public static void main(String[] args) {
        MyThread01 myThread = new MyThread01();
        myThread.start();
        for (int i = 0; i <1000 ; i++) {
            System.out.println("主线程------"+i);
        }
    }
}
class  MyThread01 extends Thread{
    //调用start方法后,JVM会默认调用run方法
    public void run(){
        for (int i = 0; i <1000 ; i++) {
            System.out.println("子线程------"+i);
        }
    }
}

实现Runable接口

public class ThreadTest02 {
    public static void main(String[] args) {
        MyThread02 myThread = new MyThread02();
        Thread thread = new Thread(myThread);
        thread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程------" + i);
        }
    }
}

class MyThread02 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("子线程------" + i);
        }
    }
}

使用匿名内部类

public class ThreadTest03 {
    public static void main(String[] args) {
        //写法一:
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("子线程1------" + i);
                }
            }
        }).start();
        //写法二:
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("子线程2------" + i);
            }
        }).start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程------" + i);
        }
    }
}

Runnable接口是一个函数式接口,因此我们可以使用Lambda表达式。

@FunctionalInterface
public interface Runnable {public abstract void run();}

课堂练习

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

        //创建子线程对象  方式一:继承Thread
        Thread myThread01 = new MyThread01();
        myThread01.start();
        //创建子线程对象  方式二:实现Runnable接口
        MyThread02 myThread = new MyThread02();
        Thread myThread02 = new Thread(myThread);
        myThread02.start();
        //创建子线程对象  方式三:通过匿名内部类创建
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程03" + "------" + i);
                }
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println("子线程04" + "------" + i);
            }
        }).start();

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

class MyThread01 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程01" + "------" + i);
        }
    }
}

class MyThread02 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程02" + "------" + i);
        }
    }
}

线程调用解析

start()方法
  1. 线程调用的时候,需要调用start()方法,start()方法调用后即停止。
  2. 作用:启动一个分支线程,在JVM中开辟一个新的栈空间,只要新的栈空间开辟出来,start()瞬间就结束了,线程就启动了。
public class ThreadTest01 {
    public static void main(String[] args) {
        MyThread01 myThread = new MyThread01();
       
        myThread.start();
        for (int i = 0; i <1000 ; i++) {
            System.out.println("主线程------"+i);
        }
    }
}
class  MyThread01 extends Thread{
    //调用start方法后,JVM会默认调用run方法
    public void run(){
        for (int i = 0; i <100 ; i++) {
            System.out.println("子线程------"+i);
        }
    }
}

结果:主线程和子线程交替输出

image-20211204205922822

问题:为什么主线程和子线程是交替输出的?

运行逻辑图

image-20211204205730809

run()方法
  1. 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈),类似于程序的主线程,main方法。
  2. run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main方法是平级的。
  3. 如果我们在主线程中直接使用run方法,不使用start方法,那么我们的程序是不会开辟新的子线程的,只会当做一个普通的方法处理。
public class ThreadTest01 {
    public static void main(String[] args) {
        MyThread01 myThread = new MyThread01();
        myThread.run();
        for (int i = 0; i <100 ; i++) {
            System.out.println("主线程------"+i);
        }
    }
}
class  MyThread01 extends Thread{
    //调用start方法后,JVM会默认调用run方法
    public void run(){
        for (int i = 0; i <100 ; i++) {
            System.out.println("子线程------"+i);
        }
    }
}

运行结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HORWD6gp-1639296073442)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20211204210031229.png)]

运行逻辑图

image-20211204205602995

线程的生命周期

线程的生命周期分为5个状态:

  1. 新建状态:创建线程对象
  2. 就绪状态:调用.start()方法,开辟新的线程栈
  3. 运行状态:线程抢住了CPU的时间片,执行run方法
  4. 阻塞状态:遇到阻塞时间,线程暂停,归还cpu时间片,阻塞结束后,进入就绪状态,需要再次抢夺CPU资源片
  5. 死亡状态:run方法结束

image-20211205092121087

3. 使用线程

常用方法

  1. getName():获取当前线程名称
  2. setName(String name):修改线程名称
  3. Thread.currentThread():获取当前线程对象
  4. Thread.sleep(long time):阻塞线程
  5. interrupt():中断线程睡眠
  6. setPriority(int i):设置线程优先级
  7. getPriority():获取线程优先级
  8. Thread.yield():线程让位
  9. void join();合并线程

获取当前线程对象

  1. 主线程的默认名称为main.

  2. 子线程的默认名称为Thread-0,随着子线程的增加,后面的编号也随之增加

public class ThreadTest02 {
    public static void main(String[] args) {
        System.out.println("主线程:" + Thread.currentThread().getName() + "开始------------");
        //获取当前线程名称
        Thread.currentThread().setName("main1");
        String name = Thread.currentThread().getName();
        System.out.println("主线程:" + name);
        Thread thread = new Thread(new MyThread02());
        System.out.println("子线程"+thread.getName());
        thread.start();
        System.out.println("主线程:" + Thread.currentThread().getName() + "结束------------");
    }
}

class MyThread02 implements Runnable {

    @Override
    public void run() {
        System.out.println("子线程:" + Thread.currentThread() + "开始------------");
        Thread.currentThread().setName("子线程01");
        System.out.println("子线程:" + Thread.currentThread() + "结束------------");
    }
}

阻塞线程

使用sleep来阻塞线程,此时线程的时间片会被CPU回收,当阻塞结束之后,线程会重新向CPU请求资源。

扩展:我们可以通过阻塞线程,来实现一些定时任务,比如整点的时候,我们向用户推送信息。或者实现倒计时功能。

注意:sleep是静态方法,Thread在哪里写,表示的是当前的线程对象。在main方法中,表示的是main,在子线程的run方法中写,表示的是子线程。

public class ThreadTest04 {
    public static void main(String[] args) {
        //创建子线程
        Thread thread = new Thread(new MyThreadTest04());
        thread.start();
        try {
            System.out.println("主线程:"+Thread.currentThread().getName()+"开始休眠");
            //此处虽然调用了thread子线程的sleep方法,但是sleep是静态方法,所以休眠的还是当前线程main
            thread.sleep(5000);
            System.out.println("主线程:"+Thread.currentThread().getName()+"休眠结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

class MyThreadTest04 implements Runnable {

    @Override
    public void run() {
        try {
            System.out.println("子线程" + Thread.currentThread().getName() + "开始休眠");
            //当前线程休眠5s
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("子线程" + Thread.currentThread().getName() + "休眠结束");
    }
}

唤醒线程

线程睡眠后,在睡眠中时,可以通过interrupt中断线程睡眠,让线程直接进入就绪状态.

interrupt会通过产生异常的方式来中断线程睡眠。

public class ThreadTest04 {
    public static void main(String[] args) {
        //创建子线程
        Thread thread = new Thread(new MyThreadTest04());
        thread.start();
        try {
            System.out.println("主线程:" + Thread.currentThread().getName() + "开始休眠");
            //此处虽然调用了thread子线程的sleep方法,但是sleep是静态方法,所以休眠的还是当前线程main
            thread.sleep(5000);
            System.out.println("主线程:" + Thread.currentThread().getName() + "休眠结束");
            System.out.println("中断子线程睡眠");
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

class MyThreadTest04 implements Runnable {

    @Override
    public void run() {
        try {
            System.out.println("子线程" + Thread.currentThread().getName() + "开始休眠");
            //当前线程休眠5s
            Thread.sleep(50000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("子线程" + Thread.currentThread().getName() + "休眠结束");
    }
}

杀死线程

方式一:使用stop(不推荐,已过时)

stop相当于直接结束程序,如果某些数据在内存中,需要保存,很可能没有保存就退出程序造成数据丢失。

public class ThreadTest04 {
    public static void main(String[] args) {
        //创建子线程
        Thread thread = new Thread(new MyThreadTest04());
        thread.start();
        try {
            System.out.println("主线程:" + Thread.currentThread().getName() + "开始休眠");
            //此处虽然调用了thread子线程的sleep方法,但是sleep是静态方法,所以休眠的还是当前线程main
            thread.sleep(5000);
            thread.stop();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

class MyThreadTest04 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println(i+"子线程" + Thread.currentThread().getName() + "开始休眠");
                //当前线程休眠5s
                Thread.sleep(2000);
                System.out.println(i+"子线程" + Thread.currentThread().getName() + "休眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
方式二:使用return(推荐)

在结束直接,可以进行一些业务处理,比如保存数据。

public class ThreadTest04 {
    public static void main(String[] args) {
        //创建子线程
        MyThreadTest04 threadChild = new MyThreadTest04();
        Thread thread = new Thread(threadChild);
        thread.start();
        try {
            System.out.println("主线程:" + Thread.currentThread().getName() + "开始休眠");
            //此处虽然调用了thread子线程的sleep方法,但是sleep是静态方法,所以休眠的还是当前线程main
            thread.sleep(5000);
            threadChild.flag=false;
            System.out.println("结束子线程");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

class MyThreadTest04 implements Runnable {
    boolean flag = true;

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                if (flag) {
                    System.out.println(i + "子线程" + Thread.currentThread().getName() + "开始休眠");
                    //当前线程休眠5s
                    Thread.sleep(2000);
                    System.out.println(i + "子线程" + Thread.currentThread().getName() + "休眠结束");
                }else {
                    System.out.println("子线程被外界终止,开始保存信息");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程调度

常见的线程调度模型:

  1. 抢占式调度模型:
    1. 哪个线程的优先级比较高,抢到的cpu时间片的概率就高一些/多一些。
    2. Java采用的是抢占式调度模型
  2. 均分式调度模型:
    1. 平均分配cpu时间片。每个线程占有的cpu时间片时间长度一样。平均分配,一切平等。
    2. 有一些编程语言,线程调度模型采用的是这种方式。
线程优先级
  1. setPriority(int i):设置线程优先级
  2. getPriority():获取线程优先级
  3. 枚举等级:
    1. 最低优先级Thread.MIN_PRIORITY:1
    2. 默认优先级Thread.NORM_PRIORITY:5
    3. 最高优先级MAX_PRIORITY:10

优先级高的线程获取CPU时间片可能会多一些(不完全是,大概率是)。指线程运行的时间相对多一些。

public class ThreadTest05 {
    public static void main(String[] args) {
        System.out.println("最高优先级" + Thread.MAX_PRIORITY);
        System.out.println("最低优先级" + Thread.MIN_PRIORITY);
        System.out.println("默认优先级" + Thread.NORM_PRIORITY);
        //获取主线程的优先级
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority());
        //创建子线程
        MyThread05 myThread05 = new MyThread05();
        Thread childThread = new Thread(myThread05);
        childThread.start();
        //修改子线程的优先级
        childThread.setPriority(10);
        for (int i = 0; i <100 ; i++) {
            System.out.println(Thread.currentThread().getName()+"----"+i);
        }
    }
}

class MyThread05 implements Runnable {

    @Override
    public void run() {
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority());
        for (int i = 0; i <100 ; i++) {
            System.out.println(Thread.currentThread().getName()+"----"+i);
        }
    }
}
线程让位

使用Thread.yield()方法,暂停当前执行的线程对象,并执行其他线程。让当前运行的线程从运行状态返回到就绪状态。

public class ThreadTest05 {
    public static void main(String[] args) {
        //创建子线程
        MyThread05 myThread05 = new MyThread05();
        Thread childThread = new Thread(myThread05);
        childThread.start();
        for (int i = 0; i <100 ; i++) {
            System.out.println(Thread.currentThread().getName()+"----"+i);
        }
    }
}

class MyThread05 implements Runnable {

    @Override
    public void run() {
        Thread currentThread = Thread.currentThread();

        //对5取余,看是否实现线程让位
        for (int i = 0; i <100 ; i++) {
            if (i%5==0){
                Thread.yield();
                System.out.println(Thread.currentThread().getName()+"----"+i);
            }
        }
    }
}
合并线程

使用join( )方法,在线程A中调用线程B的join方法,此时线程A进入阻塞状态,并释放CPU资源,当线程B内的方法执行完毕后,线程A的阻塞停止,进入就绪状态。

public class ThreadTest07 {
    public static void main(String[] args) {
        //创建子线程
        MyThread07 myThread07 = new MyThread07();
        Thread childThread = new Thread(myThread07);
        childThread.start();
        try {
            //合并线程,合并后childThread执行完才会执行主线程
            childThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "----" + i);
        }
    }
}

class MyThread07 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 5 == 0) {
            System.out.println(Thread.currentThread().getName() + "----" + i);
            }
        }
    }
}

4. 线程的安全问题

安全问题

在开发中,我们基本不同过手动创建线程的方式来实现多线程,我们的项目是部署在服务器上的,服务器已经将线程的定义,线程对象的创建,线程的启动等都已经实现,因此我们基本不需要手动创建线程。

也就是说,我们的项目是运行在一个多线程的环境下的,我们需要关注的是,在多线程条件下,我们的数据会不会出现问题,数据是否安全。

产生数据安全问题的条件

  1. 多线程并发
  2. 发生数据共享
  3. 数据有修改行为

满足上面三个条件后,就会存在线程安全问题。如在银行进行取钱操作

image-20211205095612243

解决方式

如果要解决多线程并发的安全问题,我们需要使用线程同步(这里同步指的是线程排队,数据同步。而非线程同时获取数据。)

同步编程模型

线程1和线程2,在线程1执行的时候,必须等待线程2执行结束,反之同理。两个线程之间发生了等待关系,即同步编程模型。

优点:数据同步。缺点:效率低。

异步编程模型

线程1和线程2各自执行各自的,互不影响,谁也不需要等待谁即异步编程模型。

优点:效率高。缺点:发生数据共享且修改数据时,大概率造成数据不同步。

模拟银行取钱

使用两个线程共享一个账户,来模拟取钱操作。

image-20211207103140978

//账户类 
class Account {
    //账号
    private String actno;
    //余额
    private double balance;

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }
    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public void withDraw(double money) {
        double before = this.getBalance();
        double after = before - money;
        //如果一条线程完成了after操作,但还没来的及更新,另一条线程又进来了,此时就会产生问题。
        //模拟网络延迟
         try {
            System.out.println(Thread.currentThread().getName()+"取钱中");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
        System.out.println(Thread.currentThread().getName() + "取了"+money+"剩余余额" + after);
    }
}
//用户类
class Person implements Runnable {
    Account account;

    public Person(Account account) {
        this.account = account;
    }
    @Override
    public void run() {
        account.withDraw(500);
    }
}
//银行
public class BankSystem {
    public static void main(String[] args) {
        //账户account存储在堆中,thread1和thread2二者为两个线程栈,共享account这一个对象。
        Account account = new Account("actno_1", 1000);
        Thread thread1 = new Thread(new Person(account));
        thread1.setName("张三");
        Thread thread2 = new Thread(new Person(account));
        thread2.setName("张三的妻子");
        thread1.start();
        thread2.start();

    }
}

image-20211205101405745

使用Sleep解决

张三取钱等待5s,张三妻子取钱,可以直接取,这样可以实现数据一致。

弊端:

  1. 线程多的情况下,不好划分。
  2. 无法获取准确的取钱时间,睡眠时间不能准确把握,影响效率。
  3. 如果睡眠时间小于取钱时间,可能还造成数据不一致的问题。

改造后的run方法

  @Override
    public void run() {
        if (Thread.currentThread().getName().equals("张三")) {
            System.out.println(Thread.currentThread().getName() + "开始取钱");
            try {
                Thread.sleep(10000);//张三取钱,睡眠10s
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.withDraw(500);
        } else {
            System.out.println(Thread.currentThread().getName() + "开始取钱");
            account.withDraw(500);
        }
    }

线程同步机制

线程同步块
synchronized(线程的共享对象){
    //要同步进行的内容
}

线程的共享对象:

  1. 这个是多个线程的共享的数据,这样才能达到多线程排队。

  2. 如果我们又1,2,3,4,5个线程,如果1,2,3需要排队,我们就需要在里面写1,2,3的共享对象,这个对象对于4,5来说是不共享的。

加锁相当于进行了阻塞。

执行原理

  1. 假设t1和t2线程并发,开始执行withDraw中的操作的时候,肯定有一个先,有一个后。
  2. 如果线程t1先执行,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块结束后,这把锁才会释放。
  3. 假设t1已占有这把锁,t2此时也遇到了synchronized这个关键字,也会去尝试占有这个锁,但是t1占有,t2只能等待t1的结束,直到t1把同步代码块执行结束了,t1归还这把锁,此时t2才能占有这把锁,进入同步代码块执行程序。
  4. 注意:共享对象一定是你需要排序执行的这些线程对象所共享的。

改编银行取钱操作,实现同步

    public void withDraw(double money) {
        //为当前操作加锁
        synchronized (this) {  //这里的this,指的是调用当前方法的account对象。
            double before = this.getBalance();
            double after = before - money;
            try {
                System.out.println(Thread.currentThread().getName() + "取钱中");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
            System.out.println(Thread.currentThread().getName() + "取了" + money + "剩余余额" + after);
        }
    }

image-20211205122554297

虽然我们实现了加锁,但是系统的执行效率也大大降低。

注意:线程同步块越小,效率越高

我们也可以在调用withDraw方法的地方添加synchronized,只是效率会降低。

class Person implements Runnable {
    Account account;
    public Person(Account account) { this.account = account; }
    @Override
    public void run() {
        synchronized (account){  //共享对象是当前账户
            account.withDraw(500);
        }
    }
}

改变共享对象

  1. 使用当前共享对象的成员变量作为共享对象
    1. 成功:因为当前对象的成员变量属于当前共享对象的一部分,可以实现共享操作。
  2. 使用局部变量作为共享对象
    1. 失败:局部变量是每次调用方法都会产生一个,因此并不是当前共享对象所特有的,不属于共享对象。
  3. 使用字符串作为共享对象
    1. 成功:因为字符串存在常量池中,所有的字符串共享这个字符串,属于共享对象。
    2. 但是,字符串并不是当前两个线程所特有的共享对象,也就是说其它线程调用的时候,也会出现等待的情况。

使用account的成员变量和字符串对象

创建第三个线程李四,单独一个账户

public static void main(String[] args) {
        Account account = new Account("actno_1", 1000);
        Thread thread1 = new Thread(new Person(account));
        thread1.setName("张三");
        Thread thread2 = new Thread(new Person(account));
        thread2.setName("张三的妻子");
        thread1.start();
        thread2.start();
        //创建李四,单独一个账户
        Account account2 = new Account("actno_2",5000);
        Thread thread3 = new Thread(new Person(account2));
        thread3.setName("李四");
        thread3.start();
    }

成员变量作为共享对象

   //object是Account类的一个成员变量
   Object object = new Object();
    public void withDraw(double money) {
        //为当前操作加锁
        synchronized (object) {
            double before = this.getBalance();
            double after = before - money;
            try {
                System.out.println(Thread.currentThread().getName() + "取钱中");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
            System.out.println(Thread.currentThread().getName() + "取了" + money + "剩余余额" + after);
        }
    }

结果:张三或者张三妻子的其中一人,和李四同时进行取钱。

张三和张三妻子共享一个对象,排序取。他们和李四不共享一个对象,可以其中一人和李四同时取。

image-20211205125112297

字符串作为共享对象

三个人共享一个“object”字符串共享对象

 public void withDraw(double money) {
        //为当前操作加锁
        synchronized ("object") {
            double before = this.getBalance();
            double after = before - money;
            try {
                System.out.println(Thread.currentThread().getName() + "取钱中");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
            System.out.println(Thread.currentThread().getName() + "取了" + money + "剩余余额" + after);
        }
    }

结果:张三或者张三妻子或者李四排队取钱。


image-20211205124954699

线程同步方法

即在实例方法或者静态方法上添加synchronized。表示整个方法体都需要同步。

缺点:扩大同步范围,效率降低,不常用。

优点:精简代码。

对象锁

  1. 添加到实例方法上,synchronized表示当前的共享对象是this。
  2. 一个对象一把锁,一百个对象一百个锁

类锁

  1. 添加到静态方法上,synchronized表示的共享对象是当前类。
  2. 类锁永远只有一把,因为同一个类,一个Java程序只有一个。
    //为当前方法加锁
    public synchronized void withDraw(double money) {
        double before = this.getBalance();
        double after = before - money;
        try {
            System.out.println(Thread.currentThread().getName() + "取钱中");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
        System.out.println(Thread.currentThread().getName() + "取了" + money + "剩余余额" + after);
    }

Java中的变量共享问题

变量类型:

  1. 实例变量:存在堆中
  2. 静态变量:存在方法区中
  3. 局部变量:存在栈中
  4. 常量:不可改变,不会有线程安全问题。

堆和方法区中的变量共享,每个线程一个栈,栈中的变量不会发生共享。

实例变量和静态变量是多线程共享的,局部变量不可能发生共享。

StringBuilder和StringBuffer

StringBuilder:线程不同步

StringBuffer:线程同步

使用字符串局部变量时,使用StringBuilder效率更高。因为局部变量没有线程安全问题。

如果使用StringBuffer,每次调用append方法,程序都会去锁池中寻找对应的锁,多了一个寻锁步骤,影响效率。

StringBuffer源码:

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

不同情况下的加锁分析

张三先取钱,张三妻子接着存钱

main方法

public class BankSystem {

    public static void main(String[] args) {
        Account account = new Account("actno_1", 1000);
        Thread thread1 = new Thread(new Person(account));
        thread1.setName("张三");
        thread1.start();
        //让线程张三先取钱
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread thread2 = new Thread(new Person(account));
        thread2.setName("张三的妻子");
        thread2.start();
    }
}

class Person implements Runnable {
    Account account;

    public Person(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("张三")) {
            account.withDraw(500);
        } else account.addDraw(500);
    }
}

存钱取钱操作

 //存钱
    public void withDraw(double money) {
        double before = this.getBalance();
        double after = before - money;
        try {
            System.out.println(Thread.currentThread().getName() + "取钱中");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
        System.out.println(Thread.currentThread().getName() + "取了" + money + "剩余余额" + after);
    }
    //取钱
    public void addDraw(double money) {
        double before = this.getBalance();
        double after = before + money;
        try {
            System.out.println(Thread.currentThread().getName() + "存钱中");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
        System.out.println(Thread.currentThread().getName() + "存了" + money + "剩余余额" + after);
    }
情况一:取钱,存钱方法都不加锁

结果:张三取钱的同时,他妻子可以同时存钱。

情况二:取钱方法加锁,存钱方法不加锁

结果:张三取钱的同时,他妻子可以同时存钱,但是不能同时取钱。

image-20211207133959748

情况三:取钱,存钱方法都加锁

结果:张三取钱的同时,他妻子不能同时存钱

注意:虽然我们是将两个方法加了锁,但是这两个方法使用的共享对象都是当前对象this,所以说他们使用的是一个共享对象,只有一个锁。

    //取钱操作
    public  synchronized    void withDraw(double money) {
        double before = this.getBalance();//0
        double after = before - money;//-1000
        try {
            System.out.println(Thread.currentThread().getName() + "取钱中");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
        System.out.println(Thread.currentThread().getName() + "取了" + money + "剩余余额" + this.balance);
    }
    //存钱操作
    public synchronized    void addDraw(double money) {
        double before = this.balance;
        double after = before + money;//1000
        try {
            System.out.println(Thread.currentThread().getName() + "存钱中");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);//1000
        System.out.println(Thread.currentThread().getName() + "存了" + money + "剩余余额" + this.balance);
    }

image-20211207134019986

情况四:取钱,存钱都作为静态方法,并且都加锁,并且把balance改为静态

结果:张三取钱的同时,他妻子不能同时存钱

   //取钱操作
    public  synchronized static   void withDraw(double money) {
        double before = Account.balance;//0
        double after = before - money;//-1000
        try {
            System.out.println(Thread.currentThread().getName() + "取钱中");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Account.balance=after;
        System.out.println(Thread.currentThread().getName() + "取了" + money + "剩余余额" +  Account.balance);
    }
    //存钱操作
    public synchronized static void addDraw(double money) {
        double before =Account.balance;
        double after = before + money;//1000
        try {
            System.out.println(Thread.currentThread().getName() + "存钱中");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Account.balance=after;
        System.out.println(Thread.currentThread().getName() + "存了" + money + "剩余余额" + Account.balance);
    }

排它锁与互斥锁

synchronized属于排它锁,一次只能有一个线程获取锁操作相关数据。

开发中如何解决线程安全问题

synchronized会让程序效率降低,用户体验不好,系统的用户吞吐量降级,用户体验差(原则上,一个接口让用户的等待时间最多不能超过5s)

  1. 尽量使用局部变量代替“实例变量和静态变量"。
  2. 如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)
  3. 如果不能使用局部变量,对象也不能创建多个,可以选择synchronized了,线程同步机制。
  4. 实际开发中,如果真的遇到线程共享的情况,通常我们借助redis在逻辑中进行处理。(通过加读写锁的形式实现)。

死锁

image-20211205200426927

线程1操作方法1时,需要获取锁o1,操作之后执行方法2,需要锁o2。线程2与之同理。

此时,线程1调用完方法1(o1)还没有释放,去调用方法二,而此时线程2刚执行完方法二,准备去执行方法一,但是o2没有释放。

此时,线程1获取不到锁o2,并且没有释放锁o1,而线程2获取不到锁o1,并且没有释放锁o2,则出现死锁。

出现死锁时,程序不会中断,且不容易排查,就停在那里等待。

案例分析

张三先进行存钱操作,再进行取钱操作。

张三的妻子先进行取钱操作,再进行存钱操作。

存钱操作和取钱操作使用不同的对象进行加锁。

class Account{
    //账号
    private  String actno;
    //余额
    private double balance;
    //表示操作:save 表示存储,take表示拿
    Object save;
    Object take;
     public Account(String actno, double balance,Object save,Object take) {
        this.actno = actno;
        this.balance = balance;
        this.save=save;
        this.take=take;
    }
        //存钱
    public  void withDraw(double money) {
        double before = this.getBalance();
        double after = before - money;
        this.setBalance(after);
        System.out.println(Thread.currentThread().getName() + "取了" + money + "剩余余额" + after);
    }

    //取钱
    public  void addDraw(double money) {
        double before = this.getBalance();
        double after = before + money;
        this.setBalance(after);
        System.out.println(Thread.currentThread().getName() + "存了" + money + "剩余余额" + after);
    }
}


class Person implements Runnable {
    Account account;


    public Person(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("张三")) {
            System.out.println(account);
            synchronized (account.save) {
                account.addDraw(500);
                try {
                    //阻塞线程
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (account.take) {
                    account.withDraw(500);
                }
            }
        } else {
            synchronized (account.take) {
                try {
                    //阻塞线程
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.addDraw(500);
                synchronized (account.save) {
                    account.withDraw(500);
                }
            }
        }
    }
}

public class BankSystem {

    public static void main(String[] args) {
        Account account = new Account("actno_1", 1000, new Object(), new Object());
        Thread thread1 = new Thread(new Person(account));
        thread1.setName("张三");
        thread1.start();
        Thread thread2 = new Thread(new Person(account));
        thread2.setName("张三的妻子");
        thread2.start();
    }
}

image-20211205203033853

程序永远地停在了这里。

5. 守护线程和定时器

守护线程

Java中的线程分类:

  1. 用户线程
    1. main方法,以及我们定义的一些没有设置守护线程的线程
  2. 守护线程(后台线程)
    1. 垃圾回收机制就是守护线程
    2. 特点:一般守护线程是一个死循环,所有的用户线程只要结束(mian方法结束),守护线程就自动结束。
    3. 使用场景:定时给用户推送一些服务,或者进行一些系统资源的备份。

案例

保安作为银行的守护者,当有顾客时,需要时刻关注周围的环境。当顾客离开后,则可以下班。

当不使用 thread3.setDaemon(true);的时候,说明该线程是一个用户线程,因为该线程是一个死循环,所以即使其他用户线程执行结束,该线程还在运行,程序并不会中断。

当时thread3.setDaemon(true);的时候,说明该线程是一个守护线程,当用户线程执行完,该线程完成使命,自动退出,程序结束。

线程

   public static void main(String[] args) {
        Account account = new Account("actno_1", 1000, new Object(), new Object());
        Thread thread1 = new Thread(new Person(account));
        thread1.setName("张三");
        thread1.start();
        Thread thread2 = new Thread(new Person(account));
        thread2.setName("张三的妻子");
        thread2.start();
        Thread thread3 = new Thread(new Safer());
        thread3.setName("保安");
        thread3.setDaemon(true);
        thread3.start();
    }

保安类

class Safer implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + "查看门口状况");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

不开启守护线程:

即使用户取钱完毕,保安还是会每个5s查看状况。

image-20211205211139168

开启守护线程:

用户取钱完毕,保安也不干了。

image-20211205211443917

定时器

作用: 间隔特定的时间,执行特定的程序。如:每周要进行银行账户的总账操作,每天要进行数据的备份操作。

定时器方式实现 :

  1. 可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较low)

  2. 使用定时器 java.util.Timer。不过,实际开发中也很少用,因为现在有很多高级框架都是支持定时任务的。

  3. 在实际的开发中,目前使用较多的是Spring框架中提供的springTask或者Quartz 框架。

使用Timer实现:

  1. 创建Timer对象
  2. 让需要执行的任务实现TimerTask这个抽象类
  3. 将任务作为参数传递给Timer对象,并设置执行时间和相应的延迟时间
  4. 可对Timer进行设置,可将其设置为守护线程

构造方法:

Timer() :创建一个新的计时器。
Timer(boolean isDaemon) :创建一个新的定时器的线程可以被指定 run as a daemon。(守护线程)
Timer(String name) :创建一个新的计时器,该计时器的关联线程具有指定的名称。
Timer(String name, boolean isDaemon) :创建一个新的定时器的相关线程指定名称,可以指定(守护线程) 。

常用方法:

void cancel() :终止此计时器,丢弃任何当前计划的任务。
int purge() :从这个计时器的任务队列中移除所有已取消的任务。
void schedule(TimerTask task, Date time) :在指定的时间计划执行指定的任务。
void schedule(TimerTask task, Date firstTime, long period): 计划重复固定延迟执行指定的任务,开始在指定的时间。
void schedule(TimerTask task, long delay) :指定在指定的延迟后执行指定的任务的时间
void schedule(TimerTask task, long delay, long period): 计划重复固定延迟执行指定的任务,在指定的延迟后开始。
void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) :计划重复固定利率执行指定的任务,开始在指定的时间。
void scheduleAtFixedRate(TimerTask task, long delay, long period): 计划重复固定利率执行指定的任务,在指定的延迟后开始。

案例:

通过定时任务实现保安每隔10s进行情况查看

线程

   public static void main(String[] args) {
        Account account = new Account("actno_1", 1000, new Object(), new Object());
        Thread thread1 = new Thread(new Person(account));
        thread1.setName("张三");
        thread1.start();
        Thread thread2 = new Thread(new Person(account));
        thread2.setName("张三的妻子");
        thread2.start();
        Timer time = new Timer("保安定时查看任务");
        time.schedule(new Safer(), new Date(), 1000 * 10L);
    }

附:匿名内部类写法

        Timer time = new Timer("保安定时查看任务");
//        time.schedule(new Safer(), new Date(), 1000 * 10L);
        time.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(new Date() + Thread.currentThread().getName() + "查看门口状况");
            }
        },new Date(), 1000 * 10L);

保安

class Safer extends TimerTask {
    @Override
    public void run() {
        System.out.println(new Date() + Thread.currentThread().getName() + "查看门口状况");
    }
}

image-20211205213522474

6. 实现线程的第三种方法

实现callable接口(jdk8的新特性JUC包下)这种方式实现的线程可以获取线程的返回值。

继承Thead和实现Runnable接口的方式是无法获取线程返回值的,因为run方法返回void。

应用场景:

系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,需要拿到这个结果。

**优点:**可以获取线程的执行结果

**缺点:**效率比较低,获取线程结果的时候,当前线程受阻塞,效率较低

Callable接口与Runnable接口的区别

  1. Callable有返回值,Runnable无返回值
  2. Callable可以抛出异常,Runnable不能抛出异常

我们可以通过FutureTask来进行相应的线程创建,FutureTask就是一个线程,并且实现了Runnable接口。

常用方法:

FutureTask(Callable callable) :创建一个 FutureTask会在运行,执行给定的 Callable。
FutureTask(Runnable runnable, V result) :创建一个 FutureTask会在运行,执行给定的 Runnable,并安排 get将给定的成功完成的结果返回。

实现方式:

  1. 创建FutureTask对象(该对象需要传入一个Callable接口的实现类)
  2. 创建Thread对象,并传入FutureTask,启动线程
  3. 重写call()方法,类似run()方法,但是该方法有返回值
  4. 通过get()方法来获取线程的返回值

案例

public class ThreadTest06 {

    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask(() -> {
            System.out.println("数据开始统计");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("数据开始完毕");
            return 100;
        });
        Thread task = new Thread(futureTask);
        task.start();

        for (int i = 0; i < 10; i++) {
            if (i== 5) {
                try {
                    Integer result = (Integer)futureTask.get();
                    System.out.println("数据统计结果"+result);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "正在执行" + i);
        }
    }
}

image-20211205225645248

课堂案例

public class FutrueTaskThead {
    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask(new Callable() {
            int a = 0;
            @Override
            public Object call() throws Exception {
                int a = 0;
                for (int i = 0; i < 10; i++) {
                    System.out.println("10086套餐了解一下");
                    a++;
                }
                return a;
            }
        });
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            Object o = futureTask.get();
            System.out.println(o);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

lambda写法

      FutureTask futureTask = new FutureTask(() -> {
            int a = 0;
            for (int i = 0; i < 10; i++) {
                System.out.println("10086套餐了解一下");
                a++;
            }
            return a;
        });

7. Object类的wait和notify方法

wait和notify方法

方法介绍

  1. wait和notify方法不是线程对象的方法,是普通java对象都有的方法。
  2. wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。
  3. wait方法作用: o.wait()让正在o对象上活动的线程t进入等待状态,无限期等待,并且释放掉t线程之前占有的o对象的锁。
  4. notify方法作用: o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。
    1. notifyAll:唤醒o对象处于等待的所有线程。

方法作用:

1、使用wait方法和notify方法实现“生产者和消费者模式”

生产线程负责生产,消费线程负责消费。生产线程和消费线程要达到均衡。

生产者和消费者模式一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。

生产者和消费者

模型

image-20211205222616284

案例一:仓库存储

需求:生产者生产苹果,消费者消费苹果。仓库一次只能存一个苹果。

public class ThreadTest08 {
    public static void main(String[] args) {
        List<Apple> appleList = new ArrayList<>();
        Thread consumer = new Thread(new Consumer(appleList));
        consumer.setName("消费者");
        consumer.start();
        Thread producer = new Thread(new Producer(appleList));
        producer.setName("生产者");
        producer.start();
    }
}

class Apple {}
class Consumer implements Runnable {
    List<Apple> appleList;

    public Consumer(List<Apple> appleList) {
        this.appleList = appleList;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (appleList) {
                if (appleList.size() == 0) {
                    try {
                        appleList.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Apple apple = appleList.remove(0);
                System.out.println(Thread.currentThread().getName() + "---消费了一个苹果" + apple);
                appleList.notify();
            }
        }
    }
}

class Producer implements Runnable {
    List<Apple> appleList;

    public Producer(List<Apple> appleList) {
        this.appleList = appleList;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (appleList) {
                if (appleList.size() > 0) {
                    try {
                        appleList.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Apple apple = new Apple();
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                appleList.add(apple);
                System.out.println(Thread.currentThread().getName() + "---生成了一个苹果" + apple);
                appleList.notify();
            }
        }

    }

}

image-20211206003212827

案例二:银行取钱

image-20211207155211719

public class BankSystem {
    public static void main(String[] args) {
        Account account = new Account("actno_1", 1000, new Object(), new Object());
        Thread thread1 = new Thread(new Person(account));
        thread1.setName("张三");
        thread1.start();
        Thread thread2 = new Thread(new Person(account));
        thread2.setName("张三的妻子");
        thread2.start();
        Thread thread3 = new Thread(new Person(account));
        thread3.setName("张三的儿子");
        thread3.start();
    }
}

class Person implements Runnable {
    Account account;


    public Person(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (account) {
                if (Thread.currentThread().getName().equals("张三")) {
                    account.addDraw(500);
                } else {
                    account.withDraw(500);
                }
            }
        }
    }
}
class Account {
    //账号
    private String actno;
    //余额
    private double balance;

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

   //取钱
    public void withDraw(double money) {
        double after = this.balance - money;
        if (after < 500) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            this.setBalance(this.balance - money);
            this.notify();
            System.out.println(Thread.currentThread().getName() + "qu了" + money + "剩余余额" + this.balance);
        }
    }


    //存钱
    public void addDraw(double money) {
        double after = this.balance + money;
        if (after > 2000) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            this.setBalance(this.balance + money);
            System.out.println(Thread.currentThread().getName() + "存了" + money + "剩余余额" + this.balance);
            this.notify();
        }
    }
}

image-20211207130112669

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

See you !

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值