java多线程高级

本文详细讲解了线程通信中的打印机打印实例,如何使用锁同步输入和输出任务,以及生产者消费者模式的实现和优化。重点讨论了synchronized、Lock与Condition的使用,避免了死锁并提升代码效率。
摘要由CSDN通过智能技术生成

线程通信

线程通信-打印机打印实例

线程通信基本实现

实例:打印机打印

实现功能:不断输⼊不断输出

总结:需要给输⼊任务和输出任务同时加⼀把锁,保证两个任务之间是同步的给两个任务加⼀把锁:可以是desc或者Object.class

不建议使⽤Object.class:由于Object的使⽤范围太⼤,可能造成不必要的错误.desc合适,因为他只被当前的两个任务共享.

注意:对于当前的情况只给⼀个线程加锁,⽆法实现两个线程的同步.

线程通信功能优化

实例:打印机打印
功能:对⼀次输⼊⼀次输出代码的改进
总结:进⾏了代码优化
⾯向对象的精髓:谁的活⼉谁⼲,不是你的活⼉不要⼲
将数据准备的活⼉从输⼊任务输出任务提出来,放⼊数据类Desc2

package thread02;


public class Demo1 {
    public static void main(String[] args) {
        Desc desc = new Desc();

        Input input = new Input(desc);
        Output output = new Output(desc);

        Thread t1 = new Thread(input);
        Thread t2 = new Thread(output);

        t1.start();
        t2.start();
    }
}

class Input implements Runnable{
    Desc desc;
    int i;
    public Input(Desc desc){
        this.desc = desc;
    }

    @Override
    public void run() {
        while (true){
            synchronized (desc){
                if (i==0){
                    desc.setData("zhangsan","男");
                }else {
                    desc.setData("lisi","女");
                }
                i++;
                i = i%2;
            }
        }
    }
}

class Output implements Runnable{
    Desc desc;
    public Output(Desc desc){
        this.desc = desc;
    }
    @Override
    public void run() {
        while (true){
            synchronized (desc){
                desc.getData();
            }
        }
    }
}

class Desc{
    String name;
    String sex;

    boolean flag = false;

    public synchronized void setData(String name,String sex){
            if (flag){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            this.name = name;
            this.sex = sex;

            System.out.println(name+"读入成功");
            flag = !flag;
            notify();
        }

    public synchronized void getData(){
            if (!flag){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("name:"+name+" sex:"+sex);

            flag = !flag;
            notify();
        }
}

⽣产者消费者模式

⽣产者消费者问题是研究多线程程序经典问题之⼀,它描述是有⼀块缓冲区作为仓库,⽣产者可以将产品放⼊仓库,消费者则可以从仓库中取⾛产品。在Java中⼀共有

四种⽅法⽀持同步,其中前三个是同步⽅法,⼀个是管道⽅法。

(1)Object的wait() / notify()⽅法 (2)Lock和Condition的await() / signal()⽅法

(3)BlockingQueue阻塞队列⽅法 (4)PipedInputStream /

PipedOutputStream

单生产者单消费者

单生产者单消费者问题的代码展示与上述打印机的案例类似,单生产者的模式在于一个生产者要先生产一个产品之后,才能由消费者去消费一个对应的产品,要注意控制线程的执行流程以及共用一个变量的原则。

多⽣产者多消费者

总结:将单⽣产者单消费者代码不做更改,直接再添加⼀个⽣产线程,⼀个消费线程,就形成了多⽣产者多消费者多消费者.
出现的错误1
错误描述:当有两个⽣产线程,两个消费线程同时存在的时候,有可能出现⽣产⼀次,消费多次或者⽣产多次消费⼀次的情况.
原因:当线程被重新唤醒之后,没有判断标记,直接执⾏了下⾯的代码
解决办法:将标记处的if改成while
出现的错误2
问题描述:继续运⾏程序,会出现死锁的情况(4个线程同时处于等待状态)
原因:唤醒的是本⽅的线程,最后导致所有的线程都处于等待状态.
解决办法:将notify改成notifyAll.保证将对⽅的线程唤醒

Lock锁

为什么使⽤Lock锁?

在我们使⽤synchronized进⾏同步的时候,锁对象是Object类的对象,使⽤的wait,notify⽅法都来⾃Object类,但是咱们知道并不是所有的对象都会⽤到同步,所以这样⽤法不太合理,⽽且锁相关的功能很多,Lock就是将锁⾯向对象的结果.不光是将锁⾯向对象了,同时将wait,notify等⽅法也做了⾯相对象处理.形成了Condition接⼝.当我们想实现多⽣产者多消费者模式时,可以使⽤Lock实现同步,同时配合Condition接⼝实现唤醒等待.

//创建锁对象
Lock lock = new ReentrantLock();
//⽤于⽣产任务的Condition
Condition proCon = lock.newCondition();
//⽤于消费任务的Condition
Condition conCon = lock.newCondition();

⽐较synchronized和Lock
1.synchronized:从jdk1.0就开始使⽤的同步⽅法-称为隐式同步
synchronized(锁对象){//获取锁 我们将锁还可以称为锁旗舰或者监听器
同步的代码
}//释放锁
2.Lock:从jdk1.5开始使⽤的同步⽅法-称为显示同步
原理:Lock本身是接⼝,要通过他的⼦类创建对象⼲活⼉
常⽤⼦类:ReentrantLock
使⽤过程:
⾸先调⽤lock()⽅法获取锁
进⾏同步的代码块⼉
使⽤unlock()⽅法释放锁
使⽤的场景:
当进⾏多⽣产者多消费者的功能时,使⽤Lock,其他的都使⽤synchronized
使⽤效率:Lock⾼于synchronized
⽐较Object多wait,notify和Condition的await,signal

在这里插入图片描述

示例代码

package morning;


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

public class Demo {
    public static void main(String[] args) {
        Product product = new Product();

        Produce produce = new Produce(product);
        Consume consume = new Consume(product);

        Thread thread1 = new Thread(produce);
        Thread thread2 = new Thread(produce);
        Thread thread3 = new Thread(consume);
        Thread thread4 = new Thread(consume);

        thread1.start();
        thread3.start();
        thread2.start();
        thread4.start();
    }
}

class Produce implements Runnable{

    private Product product;

    public Produce(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        while (true) {
            product.setProduce("旺旺碎冰冰", 5);
        }
    }
}

class Consume implements Runnable{

    private Product product;

    public Consume(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        while (true) {
            product.getConsume();
        }
    }
}



class Product{
    private String name;
    private double price;
    private int count;

    private boolean flag = false;

   Lock lock =  new ReentrantLock();
   Condition proCon = lock.newCondition();
   Condition conCon = lock.newCondition();

   public void setProduce(String name,double price){
       try{
           lock.lock();
           while (flag == true){
               try {
                   proCon.await();
               }catch (InterruptedException e){
                   e.printStackTrace();
               }
           }
           this.name = name;
           this.price = price;
           count++;
           try {
               Thread.sleep(200);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println(Thread.currentThread().getName()+" ⽣产 了:"+this.name+" 产品的数量:"+this.count+" 价 格:"+this.price);
           flag = !flag;

           conCon.signal();
       }finally {
            lock.unlock();
       }
   }

   public void getConsume(){
       try {
           lock.lock();
           while (flag == false){
               try {
                   conCon.await();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }

           try {
               Thread.sleep(200);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

           System.out.println(Thread.currentThread().getName()+" 消费 了:"+this.name+" 价格:"+this.price);
           //count--;
           flag = !flag;

           proCon.signal();
       }finally {
           lock.unlock();
       }
   }

    public Product() {
    }

    public Product(String name, double price, int count) {
        this.name = name;
        this.price = price;
        this.count = count;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + '\'' +
                ", price=" + price +
                ", count=" + count +
                '}';
    }
}

唤醒等待机制

⽅法简介

Object类中⼏个⽅法如下:

wait()

​ 等待,让当前的线程,释放⾃⼰持有的指定的锁标记,进⼊到等待队列。

​ 等待队列中的线程,不参与CPU时间⽚的争抢,也不参与锁标记的争抢。

notify()

​ 通知、唤醒。唤醒等待队列中,⼀个等待这个锁标记的随机的线程。

​ 被唤醒的线程,进⼊到锁池,开始争抢锁标记。

notifyAll()

​ 通知、唤醒。唤醒等待队列中,所有的等待这个锁标记的线程。

​ 被唤醒的线程,进⼊到锁池,开始争抢锁标记。

wait和sleep的区别

​ sleep()⽅法,在休眠时间结束后,会⾃动的被唤醒。 ⽽wait()进⼊到的阻塞态,需要被notify/notifyAll⼿动唤醒。

​ wait()会释放⾃⼰持有的指定的锁标记,进⼊到阻塞态。sleep()进⼊到阻塞态的时候,不会释放⾃⼰持有的锁标记。

注意事项

​ ⽆论是wait()⽅法,还是notity()/notifyAll()⽅法,在使⽤的时候要注意,⼀定要是⾃⼰持有的锁标记,才可以做这个操作。否则会出现IllegalMonitorStateException 异常。

为什么wait,notify⽅法要使⽤锁调⽤?

在这里插入图片描述

死锁

出现的情况有两种
所有的线程处于等待状态
⼤家都处于等待状态,没有⼈获取cpu使⽤
锁之间进⾏嵌套调⽤

​ 多个线程, 同时持有对⽅需要的锁标记, 等待对⽅释放⾃⼰需要的锁标记。此时就是出现死锁。 线程之间彼此持有对⽅需要的锁标记, ⽽不进⾏释放, 都在等待。

线程其他内容

线程的停⽌

/*
* 线程的停⽌:3种
* 1.通过⼀个标识结束线程
* 2.调⽤stop⽅法---因为有固有的安全问题,所以系统不建议使⽤.
* 3.调⽤interrupt⽅法----如果⽬标线程等待很⻓时间(例如基于⼀个条件变量),则应使⽤ interrupt ⽅法来中断该等待。
*/

线程的休眠

线程休眠, 就是让当前的线程休眠指定的时间。 休眠的线程进⼊到阻塞状态, 直到休眠结束。 阻塞的线程, 不参与CPU时间⽚的争抢。
注: 线程休眠的时间单位是毫秒。(Thread.sleep())

线程的合并

将⼀个线程中的任务, 合并⼊到另外⼀个线程中执⾏, 此时, 合并进来的线程有限执⾏。 类似于: 插队。
注意:优先级只⽐main线程的⾼.对其他的线程没有影响.

必须先开启线程在进行合并

比如:

// 先开启
vip.start();
// 再合并
try {
	vip.join();
} catch (InterruptedException e) {
}

线程的优先级设置

设置线程的优先级, 可以决定这个线程能够抢到CPU时间⽚的概率。 线程的优先级范围在 [1, 10], 默认的优先级是5。 数值越⾼, 优先级越⾼。 但是要注意, 并不是优先级⾼的线程⼀定能抢到CPU时间⽚, 也不是优先级的线程⼀定抢不到CPU时间
⽚。 线程的优先级只是决定了这个线程能够抢到CPU时间⽚的概率。 即便是优先级最低的线程, 依然可以抢到CPU时间⽚。

// 实例化两个线程, 处理的逻辑完全相同
Thread thread0 = new Thread(runnable, "t0");
Thread thread1 = new Thread(runnable, "t1");
// 设置线程的优先级, 必须在这个线程启动之前
thread0.setPriority(1);
thread1.setPriority(10);

守护线程

守护线程, ⼜叫后台线程。 是⼀个运⾏在后台, 并且会和前台线程争抢CPU时间⽚的线程。

​ 守护线程依然会和前台线程争抢CPU时间⽚, 实现并发的任务。

​ 在⼀个进程中, 如果所有的前台线程都结束了, 后台线程即便任务没有执⾏结束, 也会⾃动结束

// 将⼀个线程设置为守护线程
thread.setDaemon(true);
// 开启线程
thread.start();

线程池

线程池的简介

​ 线程池, 其实就是⼀个容器, ⾥⾯存储了若⼲个线程。
使⽤线程池, 最主要是解决线程复⽤的问题。 之前使⽤线程的时候, 当我们需要使⽤⼀个线程时, 实例化了⼀个新的线程。 当这个线程使⽤结束后, 对这个线程进⾏销毁。 对于需求实现来说是没有问题的, 但是如果频繁的进⾏线程的开辟和销毁,其实对于CPU来说, 是⼀种负荷, 所以要尽量的优化这⼀点。
​ 使⽤复⽤机制解决这个问题。 当我们需要使⽤到⼀个线程的时候, 不是直接实例化, ⽽是先去线程池中查找是否有闲置的线程可以使⽤。 如果有, 直接拿来使⽤; 如果没有, 再实例化⼀个新的线程。 并且, 当这个线程使⽤结束后, 并不是⻢上销毁, ⽽是将其放⼊到线程池中, 以便下次继续使⽤。

线程池的开辟

在Java中, 使⽤ThreadPoolExecutor类来描述线程池, 在这个类的对象实例化的时候, 有⼏个常⻅的参数:

在这里插入图片描述

BlockingQueue
ArrayBlockingQueue
LinkedBlockingQueue
SynchronouseQueue
RejectedExecutionHandler
ThreadPoolExecutor.AbortPolicy : 丢弃新的任务,并抛出异常
RejectedExecutionException
ThreadPoolExecutor.DiscardPolicy : 丢弃新的任务,但是不会抛出异常
ThreadPoolExecutor.DiscardOldestPolicy : 丢弃等待队列中最早的任务
ThreadPoolExecutor.CallerRunsPolicy : 不会开辟新的线程,由调⽤的线程来处理

线程池的⼯作原理

线程池中的所有线程, 可以分为两部分: 核⼼线程 和 临时线程
核⼼线程:
核⼼线程常驻于线程池中, 这些线程, 只要线程池存在, 他们不会被销毁。 只有当线程池需要被销毁的时候, 他们才会被销毁。

临时线程:
就是临时⼯。 当遇到了临时的⾼密度的线程需求时, 就会临时开辟⼀些线程, 处理⼀些任务。 这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务要处理, 就会闲置。 当闲置的时间到达了指定的时间之后, 这个临时线程就会被销
毁。
任务分配逻辑:

  1. 当需要处理并发任务的时候, 优先分配给核⼼线程处理。
  2. 当核⼼线程都已经分配了任务, ⼜有新的任务出现时,会将这个新的任务存⼊
    等待队列。
  3. 当等待队列被填满后, 再来新的任务时, 会从开辟⼀个临时线程,处理这个新
    的任务。
  4. 当临时线程加核⼼线程数量已经到达线程池的上限,再来新的任务的时候,就会
    触发拒绝访问策略。

线程池的常⽤⽅法

在这里插入图片描述

线程池的⼯具类

线程池的开辟, 除了可以使⽤构造⽅法进⾏实例化, 还可以通过Executors⼯具类进⾏获取。 实际应⽤中, ⼤部分的场景下, 可以不⽤前⾯的构造⽅法进⾏线程池的实例化, ⽽是⽤Executors⼯具类中的⽅法进⾏获取。

在这里插入图片描述

, 就会临时开辟⼀些线程, 处理⼀些任务。 这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务要处理, 就会闲置。 当闲置的时间到达了指定的时间之后, 这个临时线程就会被销
毁。
任务分配逻辑:

  1. 当需要处理并发任务的时候, 优先分配给核⼼线程处理。
  2. 当核⼼线程都已经分配了任务, ⼜有新的任务出现时,会将这个新的任务存⼊
    等待队列。
  3. 当等待队列被填满后, 再来新的任务时, 会从开辟⼀个临时线程,处理这个新
    的任务。
  4. 当临时线程加核⼼线程数量已经到达线程池的上限,再来新的任务的时候,就会
    触发拒绝访问策略。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值