Java_22_多线程02

多线程

线程通信

  1. 线程通信:多个线程因为在同一个进程中,所以互相通信比较容易的。

  2. 线程通信的经典模型:生产者与消费者问题。
    生产者负责生成商品,消费者负责消费商品。
    生产不能过剩,消费不能没有。(即时生产,即时消费)

  3. 模拟一个案例:
    小明和小红有一个共同账户:共享资源
    他们有3个爸爸(亲爸,岳父,干爹)给他们存钱。

  4. 模型:小明和小红去取钱,如果有钱就取出,然后等待自己,唤醒他们3个爸爸们来存钱
    他们的爸爸们来存钱,如果发现有钱就不存,没钱就存钱,然后等待自己,唤醒孩子们来取钱。
    做整存整取:10000元。

  5. 分析:
    生产者线程:亲爸,岳父,干爹
    消费者线程:小明,小红
    共享资源:账户对象。

  6. 注意:线程通信一定是多个线程在操作同一个资源才需要进行通信。
    线程通信必须先保证线程安全,否则毫无意义,代码也会报错!

  7. 线程通信的核心方法:
    public void wait(): 让当前线程进入到等待状态 此方法必须锁对象调用.
    public void notify() : 唤醒当前锁对象上等待状态的某个线程 此方法必须锁对象调用
    public void notifyAll() : 唤醒当前锁对象上等待状态的全部线程 此方法必须锁对象调用
    小结:
    是一种等待唤醒机制。
    必须是在同一个共享资源才需要通信,而且必须保证线程安全

主函数

public class ThreadConnection {
    public static void main(String[] args){
        //1.创建共享账户
        Account acc = new Account();
        //2.创建线程对象
        Runnable Little_Ming = new DrawThread(acc,0);
        new Thread(Little_Ming,"Little_Ming").start();
        Runnable Little_Hong = new DrawThread(acc,0);
        new Thread(Little_Hong,"Little_Hong").start();

        Runnable TrueDad = new SaveThread(acc);
        new Thread(TrueDad,"True_Dad").start();
        Runnable Gandad = new SaveThread(acc);
        new Thread(TrueDad,"Gan_Dad").start();
        Runnable GrandDad = new SaveThread(acc);
        new Thread(TrueDad,"Grand_Dad").start();
    }
}
/**
 * 取钱线程类
 */
public class DrawThread implements Runnable{
    private Account acc;
    private double money;
    public DrawThread(Account acc,double money){
        this.acc = acc;
        this.money = money;
    }
    @Override
    public void run() {
        //小明 小红取钱
        while (true) {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            acc.drawMoney(10000);
        }
    }
}

/**
 * 存钱的线程类
 */
public class SaveThread implements Runnable{
    private Account acc ; // 定义了一个账户类型的成员变量接收取款的账户对象!
    public SaveThread(Account acc){
        this.acc = acc ;
    }
    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(4000);
                acc.saveMoney(10000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

//账户类
public class Account {
    private String cardID;
    private double money;

    public Account() {
    }

    public synchronized void drawMoney(double money) {
        try {
            //谁取
            String name = Thread.currentThread().getName();
            //余额是否足够
            if(this.money >= money){
                //2.开始拿钱
                this.money -= money;
                System.out.println(name + "取走了" + money + ",剩余" + this.money);
                //3.取钱后没钱,等待自己唤醒别人
                this.notifyAll();//唤醒别人
                this.wait(); //自己等待
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public synchronized void saveMoney(double money) {
        try {
            //谁来存钱
            String name = Thread.currentThread().getName();
            //1.判断余额
            if(this.money <= 0){
                //没钱
                this.money += money;
                System.out.println(name + "来存了" + money);
            }
            //存完钱或者有钱,唤醒别人等待自己
            this.notifyAll();
            this.wait();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

利用this.notifyAll();this.wait();轮流唤醒其他用户操作,达到线程通信的目的!!

线程状态

在这里插入图片描述

在这里插入图片描述

线性池

  1. 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复的使用,
    省去了频繁创建和销毁线程对象的操作,无需反复创建线程而消耗过多资源。

  2. 为什么要用线程池:
    合理利用线程池能够带来三个好处

    1. 降低资源消耗。
      – 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

    2. 提高响应速度
      – 不需要频繁的创建线程,如果有线程可以直接用,不会出现系统僵死!

    3. 提高线程的可管理性(线程池可以约束系统最多只能有多少个线程,不会因为线程过多而死机)

  3. 线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务。

在这里插入图片描述

线程池的创建

  1. 线程池在Java中的代表类:ExecutorService(接口)。

  2. Java在Executors类下提供了一个静态方法得到一个线程池的对象:

    1. public static ExecutorService newFixedThreadPool(int nThreads): 创建一个线程池返回。

    2. ExecutorService提交线程任务对象执行的方法:

      1. Future<?> submit(Runnable task):提交一个Runnable的任务对象给线程池执行。
  3. 小结:
    pools.shutdown(); 等待任务执行完毕以后才会关闭线程池
    pools.shutdownNow(); 立即关闭线程池的代码,无论任务是否执行完毕!
    线程池中的线程可以被复用,线程用完以后可以继续去执行其他任务。

Runnable接口做线程池
public class ThreadPoolsDemo01 {
    public static void main(String[] args){
        //1.创建线程池,指定线程数量固定为3
        ExecutorService pools = Executors.newFixedThreadPool(3);
        //添加线程任务让线程处理
        Runnable tar = new MyRunnable();

        pools.submit(tar);//第一次提交任务,此时线程池创建新线程
        pools.submit(tar);//第二次提交任务,此时线程池创建新线程
        pools.submit(tar);//第三次提交任务,此时线程池创建新线程
        pools.submit(tar);//第四次提交任务,复用之前的线程

        pools.shutdown(); //等待任务执行完毕后关闭线程池
        //pools.shutdownNow(); //立即关闭线程池代码,无论任务是否执行完毕!
    }
}
class MyRunnable implements Runnable{
    @Override
    public void run(){
        for(int i = 0;i < 5;i ++) {
            System.out.println(Thread.currentThread().getName() + "==>" + i);
        }
    }
}
Callable接口做线程池
  1. 线程池在Java中的代表类:ExecutorService(接口)。

  2. Java在Executors类下提供了一个静态方法得到一个线程池的对象:
    1.public static ExecutorService newFixedThreadPool(int nThreads):创建一个线程池返回。

  3. ExecutorService提交线程任务对象执行的方法:
    1.Future<?> submit(Runnable task):提交一个Runnable的任务对象给线程池执行。 1.Future<?> submit(Callable task):提交一个Runnable的任务对象给线程池执行。

  4. 小结:
    Callable做线程池的任务,可以得到它执行的结果!!

public class ThreadPoolsDemo02 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pools = Executors.newFixedThreadPool(3);

        //2.提交Callable的任务对象后返回一个未来任务对象!
        Future<String> t1 = pools.submit(new MyCollable(100));
        Future<String> t2 = pools.submit(new MyCollable(200));
        Future<String> t3 = pools.submit(new MyCollable(300));
        Future<String> t4 = pools.submit(new MyCollable(400));

        //3.获取线程池执行的任务的结果
        try{
            String rs1 = t1.get();
            String rs2 = t2.get();
            String rs3 = t3.get();
            String rs4 = t4.get();
            
            System.out.println(rs1);
            System.out.println(rs2);
            System.out.println(rs3);
            System.out.println(rs4);
        }catch (Exception e){
            e.printStackTrace();
        }


    }
}
class MyCollable implements Callable<String>{
    //需求:使用线程池,计算1-100,1-200,1-300的和返回
    private int n;
    public MyCollable(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;
    }
}

死锁

  1. 死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
    由于线程被无限期地阻塞,因此程序不可能正常终止。

  2. 客户(占用资金,等待经销商的货品资源) 经销商(占用货品资源,等待客户的资金)

  3. java 死锁产生的四个必要条件:

    1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
    2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
    3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
    4. 循环等待,即存在一个等待循环队列:p1要p2的资源,p2要p1的资源。这样就形成了一个等待环路

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,
便可让死锁消失

  1. 小结:
    死锁是多个线程满足上述四个条件才会形成,死锁需要尽量避免。
    死锁一般存在资源的嵌套请求
public class ThreadDeadDemo01 {
    //定义资源对象
    public static Object resources01 = new Object();
    public static Object resources02 = new Object();
    public static void main(String[] args){
        //死锁至少两个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resources01){
                    System.out.println("线程1占用资源1,请求资源2");
                    try{
                        Thread.sleep(1000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    synchronized (resources02){
                        System.out.println("线程1成功占用资源2");
                    }
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resources02){
                    System.out.println("线程2占用资源2,请求资源1");
                    try{
                        Thread.sleep(1000);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    synchronized (resources01){
                        System.out.println("线程2成功占用资源1");
                    }
                }
            }
        }).start();
    }
}

volatile关键字

解决:并发编程下变量不可见性问题

  1. 引入:

    1. 问题:线程修改了某个成员变量的值,但是在主线程中读取到的还是之前的值
      修改后的值无法读取到。
    2. 原因:按照JMM模型,所有的成员变量和静态变量都存在于主内存中,主内存中的变量可以被多个线程共享。
      每个线程都存在一个专属于自己的工作内存,工作内存一开始存储的是成员变量的副本。
      所以线程很多时候都是直接访问自己工作内存中的该变量,其他线程对主内存变量值的修改将不可见!!
  2. 解决此问题:
    希望所有线程对于主内存的成员变量修改,其他线程是可见的。

    1. 加锁:可以实现其他线程对变量修改的可见性。
      某一个线程进入synchronized代码块前后,执行过程入如下:

      1. 线程获得锁
      2. 清空工作内存
      3. 从主内存拷贝共享变量最新的值到工作内存成为副本
    2. 可以给成员变量加上一个volatile关键字,立即就实现了成员变量多线程修改的可见性。

  3. 小结:

    1. 可以给成员变量加上一个volatile关键字,当一个线程修改了这个成员变量的值,其他线程可以立即看到修改后的值并使用!
    2. volatile与synchronized的区别。
      - volatile只能修饰实例变量和静态变量,而synchronized可以修饰方法,以及代码块。
      - volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);
      而synchronized是一种排他(互斥)的机制,可保证原子性(线程安全)
public class VolatileDemo01 extends Thread {
    private boolean flag = false;
    @Override
    public void run(){

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //线程中修改变量
        flag = true;
        System.out.println("flag = " + flag);
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
class VisibilityDemo{
    public static void main(String[] args){
        //1.启动子线程,修改flag的变量成true
        VolatileDemo01 var = new VolatileDemo01();
        var.start();
        //2.主线程
        while(true){
            if(var.isFlag()) System.out.println("主线程进入执行~~");
        }
    }
}

运行结果:

flag = true
子线程修改了值,主线程没用读到!!

在这里插入图片描述

不可见性解决方案

加锁

可以实现其他线程对变量修改的可见性。
某一个线程进入synchronized代码块前后,执行过程入如下:
1. 线程获得锁
2. 清空工作内存
3. 从主内存拷贝共享变量最新的值到工作内存成为副本

class VisibilityDemo01{
    public static void main(String[] args){
        //1.启动子线程,修改flag为true
        VolatileDemo02 var = new VolatileDemo02();
        var.start();
        //2.主线程
        while(true){
            //加锁会清空工作内存,读取主内存中的最新值到工作内存中来
            synchronized (VisibilityDemo01.class){
                if(var.isFlag()) System.out.println("主线程进入执行~~");
            }
        }
    }
}
Volatile关键字修饰

工作原理:一旦修改,主内存通知工作内存变量已修改,原值已失效!!再去主内存加载最新值。
在这里插入图片描述

private volatile boolean flag = false;

Volatile修饰变量的原子性研究

  1. 概述:所谓的原子性是指在一次操作或者多次操作中,所有的操作全部都得到了执行并且不会受到任何因素的干扰。最终结果要保证线程安全。

  2. 小结:在多线程环境下,volatile关键字可以保证共享数据的可见性,
    但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。

  3. volatile的使用场景

    1. 开关控制
      利用可见性特点,控制某一段代码执行或者关闭。
    2. 多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读。此时加上更好,其他线程可以立即读取到最新值。
      volatile不能保证变量操作的原子性(安全性)。
public class VoatileDemo03 {
    public static void main(String[] args){
        Runnable tar = new MyRunnable();
        for(int i = 1;i <= 100;i ++)
            //启动100次线程,执行100次任务
            new Thread(tar).start();
    }

}
class MyRunnable implements Runnable{
    private volatile int count = 0;
    @Override
    public void run(){
        for(int i = 1;i <=100;i ++) {
            count ++;
            System.out.println("Count ==>" + count);
        }
    }
}

运行结果:有时不准确

加锁实现线程安全

加锁机制性能很差

class MyRunnable01 implements Runnable{
    private int icount = 0;
    @Override
    public void run() {
        synchronized ("Safty") {
            for (int i = 1; i <= 100; i++) {
                icount++;
                System.out.println("iCount ==>" + icount);
            }
        }
    }
}
原子类保证原子性操作
  1. 如何保证变量访问的原子性呢?

    1. 加锁实现线程安全。
      – 虽然安全性得到了保证,但是性能不好!!
    2. 基于CAS方式的原子类。
      1. Java已经提供了一些本身即可实现原子性(线程安全)的类。
      2. 概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单 ,性能高效,线程安全地更新一个变量的方式。
      3. 操作整型的原子类
        public AtomicInteger(): 初始化一个默认值为0的原子型Integer
        public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
        int get(): 获取值
        int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
        int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
        int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
        int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。
  2. CAS与Synchronized总结:

    1. CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?
      1. Synchronized是从悲观的角度出发:
        总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
        共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差!!
      2. CAS是从乐观的角度出发:
        总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
        CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!
public class VolatileDemo05 {
    public static void main(String[] args){
        Runnable var = new MyRunnable();
        for(int i = 1;i <= 100;i ++){
            new Thread(var).start();
        }
    }
}
class MyRunnable02 implements Runnable{
    //创建一个Integer更新的原子类AtomicInteger.初始值为0 取代int count
    private AtomicInteger atomicInteger = new AtomicInteger();
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
        //atomicInteger.incrementAndGet() 先加再取
            System.out.println("iCount ==>" + atomicInteger.incrementAndGet());
        }
    }
}
原子类CAS机制

CAS:Compare And Swap(比较再交换)
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

沐鑫本鑫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值