【Java笔记】线程通信、线程池、volatile关键字、synchronized

三. 等待唤醒机制

1. 线程通信

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

为什么要处理线程间通信:

多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。


2. 等待唤醒机制

什么是等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify 就是线程间的一种协作机制。

等待唤醒中的方法

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
  2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

image-20200707161132583

3. 生产者与消费者问题

等待唤醒机制其实就是经典的“生产者与消费者”的问题。就拿存钱取钱案例来说等待唤醒机制如何有效利用资源。

案例

夫妻小明与小红有一个共同账户:共享资源。他们有三个爸爸(亲爹,干爹,岳父)给他们账户存钱。小明和小红去取钱,如果账户有钱就取出来,等待自己,然后等待自己等他们三个爸爸去存钱;爸爸们去存钱,如果发现有钱就不存,没钱就存钱,然后等待自己。

分析:

生产者线程:3个爸爸

消费者线程:小明,小红

共享资源:账户对象

账户类:

public class Account {
    
    
    private String cardId;
    private double money;

    public Account() {
    
    
    }

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

    public String getCardId() {
    
    
        return cardId;
    }

    public void setCardId(String cardId) {
    
    
        this.cardId = cardId;
    }

    public double getMoney() {
    
    
        return money;
    }

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

    public synchronized void drawMoney(int money) {
    
    
      try{
    
    
          String name=Thread.currentThread().getName();
          if(this.money>=money){
    
    
              this.money -=money;
              System.out.println(name+"来取钱:"+money+",余额:"+this.money);
              //等待自己唤醒别人,锁对象调用
              this.notifyAll();
              this.wait();
          }else {
    
    
              //余额不足,唤醒别人等待自己
              this.notifyAll();
              this.wait();
          }
      }catch (Exception e){
    
    
          e.printStackTrace();
      }
    }

    public synchronized void saveMoney(int money) {
    
    
        try{
    
    
            String name=Thread.currentThread().getName();
            if(this.money> 0){
    
    
                //有钱,不存,唤醒别人
                this.notifyAll();
                this.wait();
            }else {
    
    
                this.money +=money;
                System.out.println(name+"来存钱:"+money+",余额:"+this.money);
                this.notifyAll();
                this.wait();
            }
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
    }
}

存钱线程类:

public class SaveThread extends Thread{
    
    

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

        @Override
        public void run() {
    
    
            while (true) {
    
    
                try {
    
    
                    this.sleep(3000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                acc.saveMoney(100000);
            }
        }
    }

取钱线程类:

public class DrawThread extends Thread{
    
    

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

    @Override
    public void run() {
    
    
        while (true){
    
    
            try {
    
    
                Thread.sleep(3000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            //取钱
            acc.drawMoney(100000);
        }
    }
}

主类:

public class MainTest {
    
    
    public static void main(String[] args) {
    
    
        //创建一个共享账户
        Account acc = new Account();
        //创建两个取钱线程对象代表小明和小红
        new DrawThread(acc,"小明").start();
        new DrawThread(acc,"小红").start();

        //存钱线程
        new SaveThread(acc,"亲爹").start();
        new SaveThread(acc,"干爹").start();
        new SaveThread(acc,"岳父").start();
    }
}

结果:存取钱交替执行

image-20200707192328765
总结:

  • 线程通信一定多个线程操作一个资源才能通信
  • 线程通信必须包装线程安全,否则无意义

四. 线程状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中, 有几种状态呢?在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

image-20200707173125038
image-20200707171658786


五. 线程池

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

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

创建与常用方法
image-20200707175410857


使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

Runnable实现类代码:

public class ThreadPoolsTest {
    
    
    public static void main(String[] args) {
    
    
        //创建一个线程池
        ExecutorService pools = Executors.newFixedThreadPool(3);

        //提交线程任务让线程池处理
        Runnable target = new MyRunnable();
        pools.submit(target);//线程池创建新线程
        pools.submit(target);//线程池创建新线程
        pools.submit(target);//线程池创建新线程
        pools.submit(target);//复用之前的线程

        pools.shutdown();//等任务执行完了就会关闭线程池
//        pools.shutdownNow();//立即关闭线程池,无论是否执行完毕
    }
}
class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i=0;i<10;i++)
            System.out.println(Thread.currentThread().getName()+"=>"+i);
    }
}

六. 死锁

image-20200707180009356
设计一个必然死锁的案例:

public class Demo {
    
    
    //创建两个共享资源
    public static Object resource1=new Object();
    public static Object resource2=new Object();

    public static void main(String[] args) {
    
    
        //创建两个线程
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synchronized (resource1){
    
    
                    System.out.println("线程1占用资源1,请求资源2");
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    synchronized (resource2){
    
    
                        System.out.println("线程1占用资源2");
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synchronized (resource2){
    
    
                    System.out.println("线程2占用资源2,请求资源1");
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    synchronized (resource1){
    
    
                        System.out.println("线程2占用资源2");
                    }
                }
            }
        }).start();
    }
}

小结:死锁代码形式上通常需要进行锁的嵌套访问


七. volatile关键字

1. 并发编程下变量不可见性问题

public class Volatiledemo extends Thread {
    
    
    private boolean flag= false;
    @Override
    public void run() {
    
    
        try {
    
    
            Thread.sleep(1000);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        //线程中修改flag
        flag =true;
        System.out.println("flag =true");
    }

    public boolean isFlag() {
    
    
        return flag;
    }

    public void setFlag(boolean flag) {
    
    
        this.flag = flag;
    }
}

class Test{
    
    
    public static void main(String[] args) {
    
    
        Volatiledemo t = new Volatiledemo();
        t.start();

        while (true){
    
    
            if (t.isFlag())
                System.out.println("主线程进行");

        }
    }
}

运行结果:
image-20200708161116519
分析:
  多线程访问一个共享变量,会出现一个线程修改变量值后,其他线程看不到的情况

2. 变量不可见性的含义

image-20200708161551491

本地内层与主内存的关系

image-20200708161709266

问题分析:

image-20200708161950724

image-20200708162045088

不可见性原因:

  每个线程都有自己的工作内存,线程都是从主内层拷贝共享变量的副本值,每个线程都是在自己的工作内存中操作共享变量


3.变量不可见性解决方案

3.1 加锁

每次加锁会清空自己的工作内存,重新读取主内层中的最新值

//main方法
while (true){
    
    
    synchronized (Volatiledemo.class){
    
    
        if (t.isFlag())
            System.out.println("主线程进行");
    }
}

某一个线程进入synchronized代码块前后,执行过程如下:

image-20200708163342106


3.2 volatile关键字修饰

  使用volatile关键字修饰该变量,一旦一个线程修改了volatile关键字修饰的变量,另一个线程立即取到最新值

private volatile boolean flag;

工作原理:

image-20200708163856695

4.volatile与synchronized

image-20200708164459290


八. 原子性

image-20200708164826444
  原子性是指一批操作是一个整体,要么同时成功,要么同时失败,不能被干扰

1. volatile的原子性

volatile只能保证线程变量的可见性,不能保证变量操作的原子性

public class Demo {
    
    
    public static void main(String[] args) {
    
    
        Runnable target=new MyRunnable();
        for (int i=0;i<100;i++)
            new Thread(target).start();
    }
}
class MyRunnable implements Runnable{
    
    
    private volatile int count;
    @Override
    public void run() {
    
    
        for (int i=0;i<100;i++){
    
    
            count++;
            System.out.println("count="+count);
        }
            
    }
}

输出结果可能不是理想的结果(10000),结果小于10000,不具有原子性
image-20200708170813109image-20200708165919433


2. 保证原子性的操作

2.1 加锁

public class Demo2 {
    
    
    public static void main(String[] args) {
    
    
        Runnable target=new MyRunnable2();
        for (int i=0;i<100;i++)
            new Thread(target).start();
    }
}

class MyRunnable2 implements Runnable{
    
    
    private volatile int count;
    @Override
    public void run() {
    
    
        synchronized ("a"){
    
    //即保证可见性,有保证原子性
            for (int i=0;i<100;i++){
    
    
                count++;
                System.out.println("count="+count);
            }
        }
    }
}

2.2 原子类

加锁虽然可以保证原子性,但是性能较差,于是java提供了原子类,提高了性能
image-20200708172805019
AtomicInteger

原子型Inteager,可以实现原子更新操作
image-20200708172921836

public class Demo3 {
    
    
    public static void main(String[] args) {
    
    
        Runnable target=new MyRunnable3();
        for (int i=0;i<100;i++)
            new Thread(target).start();
    }
}

class MyRunnable3 implements Runnable{
    
    
    //创建一个Integer更新的原子类AtomicInteger,初始值是0
    private AtomicInteger count=new AtomicInteger();

    @Override
    public void run() {
    
    
        for (int i=0;i<100;i++){
    
    
            System.out.println("count="+count.incrementAndGet());
        }
    }
}

3.原子类CAS机制实现线程安全

3.1 概述

image-20200708174108409
QQ图片20200708174640

3.2 CAS与Synchronized:乐观锁,悲观锁

image-20200708174936423
image-20200708175132138

九. 并发包

猜你喜欢

转载自blog.csdn.net/Supreme7/article/details/107213180
今日推荐