Java多线程02:线程的同步和死锁+综合实战

Java高级编程02

本文基于 阿里云大学:Java高级编程 整理记录,仅用于个人学习/交流使用。

三、线程的同步与死锁

同步问题引出

在多线程的处理之中,可以利用Runnable描述多个线程操作的资源,而Thread 描述每一个线程对象,于是当多个线程访问同一资源的时候如果处理不当就会产生数据的错误操作。

此时的程序将创建三个线程对象,并且这三个线程对象将进行5张票的出售。

public class MyThread  implements Runnable{
    private int ticket=10;
    @Override
    public void run() {
        while (true){
            if (this.ticket>0){
                System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--);
            }else {
                System.out.println("***票已经卖光了***");
                break;
            }
        }
    }
}


class ThreadDemo13 {
    public static void main(String[] args)  {
       MyThread myThread=new MyThread();
       new Thread(myThread,"票贩子A").start();
       new Thread(myThread,"票贩子B").start();
       new Thread(myThread,"票贩子C").start();
    }
}

似乎没有什么的问题(但这只是假象),下面可以模拟一下卖票中的延迟操作。.

public class MyThread  implements Runnable{
    private int ticket=10;
    @Override
    public void run() {
        while (true){
            if (this.ticket>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--);
            }else {
                System.out.println("***票已经卖光了***");
                break;
            }
        }
    }
}


class ThreadDemo13 {
    public static void main(String[] args)  {
       MyThread myThread=new MyThread();
       new Thread(myThread,"票贩子A").start();
       new Thread(myThread,"票贩子B").start();
       new Thread(myThread,"票贩子C").start();
    }
}

/*
......
票贩子B卖票,ticket=1
***票已经卖光了***
票贩子A卖票,ticket=-1
***票已经卖光了***
票贩子C卖票,ticket=0
***票已经卖光了***
*/

这个时候追加了延迟问题就暴露出来了,而实际上这个问题一直都在。

image-20210310124013015

线程同步处理

经过分析之后已经可以确认同步问题所产生的主要原因了,那么下面就需要进行同步问题的解决,但是解决同步问题的关键是锁,指的是当某一个线程执行操作的时候,其它线程外面等待。

image-20210310124937789

如果要想在程序之中实现这把锁的功能,就可以使用synchronized关键字来实现,利用此关键字可以定义同步代码块或同步方法,在同步代码块的操作里面的代码只允许一个线程执行。

1、利用同步代码块进行处理

synchronized(同步对象){
同步代码操作 ;
}.

一般要进行同步对象处理的时候可以采用当前对象this进行同步

范例:利用同步代码块解决数据同步访问问题

public class MyThread  implements Runnable{
    private int ticket=10000;
    @Override
    public void run() {
        while (true){
            synchronized (this){
                if (this.ticket>0){
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--);
                }else {
                    System.out.println("***票已经卖光了***");
                    break;
                }
            }
        }
    }
}


class ThreadDemo13 {
    public static void main(String[] args)  {
       MyThread myThread=new MyThread();
       new Thread(myThread,"票贩子A").start();
       new Thread(myThread,"票贩子B").start();
       new Thread(myThread,"票贩子C").start();
    }
}

加入同步处理之后,程序的整体的性能下降了。同步实际上会造成性能的降低。

2、利用同步方法解决

只需要在方法定义上使用synchronized关键字即可。

范例:利用同步方法解决数据同步访问问题


public class MyThread  implements Runnable{
    private int ticket=10000;

    public boolean sale(){
            if (this.ticket>0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"卖票,ticket="+this.ticket--);
                return true;
            }else {
                System.out.println("***票已经卖光了***");
                return false;
            }

    }

    @Override
    public void run() {
        while (this.sale()){

        }
    }
}


class ThreadDemo13 {
    public static void main(String[] args)  {
       MyThread myThread=new MyThread();
       new Thread(myThread,"票贩子A").start();
       new Thread(myThread,"票贩子B").start();
       new Thread(myThread,"票贩子C").start();
    }
}

在日后学习Java类库的时候会发现,系统中许多的类上使用的同步处理采用的都是同步方法

线程死锁

死锁是在进行多线程同步的处理之中有可能产生的一种问题,所谓的死锁指的是若干个线程彼此互相等待的状态。

一个简单的代码来观察一下死锁的表现形式,但是对于此代码不作为重点。.

范例:死锁的展示

…省略…

现在死锁造成的主要原因是因为彼此都在互相等待着,等待着对方先让出资源。

死锁实际上是一种开发中出现的不确定的状态,有的时候代码如果处理不当则会不定期出现死锁,这是属于正常开发中的调试问题。

若干个线程访问同一资源时一定要进行同步处理,而过多的同步会造成死锁。

四、综合实战

生产者-消费者模型

在多线程的开发过程之中最为著名的案例就是生产者与消费者操作

该操作的主要流程如下:

  • 生产者负责信息内容的生产;
  • 每当生产者生产完成一项完整的信息之后消费者要从这里面取走信息;
  • 如果生产者没有生产者则消费者要等待它生产完成,如果消费者还没有对信息进行消费,则生产者应该等待完成后再继续生产。

image-20210311165933761

生产者与消费者基本程序模型

可以将生产者与消费者定义为两个独立的线程类对象,但是对于现在生产的数据,可以使用如下的

  • 数据一: name=张三、content =法外狂徒;
  • 数据二: name=李四、content =合法公民;

既然生产者与消费者是两个独立的线程,那么这两个独立的线程之间就需要有一个数据的保存集中点,那么可以单独个Message类实现数据的保存。

package com.lut.JavaPlus;

public class MyThread {
    public static void main(String[] args) {
        Message message=new Message();
        new Thread(new Producer(message)).start();//启动生产者线程
        new Thread(new Consumer(message)).start();//启动消费者线程
    }
}

class Producer implements Runnable{
    private Message message;
    public Producer(Message message) {
        this.message = message;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i%2==0){
                this.message.setName("张三");
                this.message.setContent("法外狂徒");
            }else {
                this.message.setName("李四");
                this.message.setContent("合法公民");
            }
        }
    }
}

class Consumer implements Runnable{
    private Message message;
    public Consumer(Message message) {
        this.message = message;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.message.getName()+"---"+this.message.getContent());
        }
    }
}

class Message{
    String name;
    String content;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
}

通过整个代码的执行你会发现此时有两个主要问题:

  • 问题一:数据不同步了;

  • 问题二:生产一个取走一个,但是发现有了重复生产和重复取出问题;

解决生产者-消费者同步问题

如果要解决问题,首先解决的就是数据同步的处理问题,如果要想解决数据同步最简单的做法是使用synchronized同步代码块或同步方法,于是这个时候对于同步的处理就可以直接在Message类中完成。

在进行同步处理的时候肯定需要有一个同步的处理对象,那么此时肯定要将同步操作交由Message类处理是最合适的

package com.lut.JavaPlus;

public class MyThread {
    public static void main(String[] args) {
        Message message=new Message();
        new Thread(new Producer(message)).start();//启动生产者线程
        new Thread(new Consumer(message)).start();//启动消费者线程
    }
}

class Producer implements Runnable{
    private Message message;
    public Producer(Message message) {
        this.message = message;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i%2==0){
               message.set("张三","法外狂徒");
            }else { 
                message.set("李四","合法公民");
            }
        }
    }
}

class Consumer implements Runnable{
    private Message message;
    public Consumer(Message message) {
        this.message = message;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(message.get());
        }
    }
}

class Message{
    String name;
    String content;
    public synchronized void set(String name,String content){
        this.name=name;
        this.content=content;
    }
    public synchronized String get(){
        return this.name+"--"+this.content;
    }
}

这时候发现数据已经可以正常的保持一致了,但是对于重复操作的问题依然存在。

利用Object类解决重复操作

如果说现在要想解决生产者与消费者的问题,那么最好的解决方案就是使用等待与唤醒机制。

而对于等待与唤醒的操作主要依靠的是 Object类中提供的方法处理的: 。

  • 等待机制:

    • 死等: public final void wait() throws InterruptedException;

    • 设置等待时间: public final void wait(long timeout) throws InterruptedException;

    • 设置等待时间: public final void wait(iong timeout, int nanos) throwsInterruptedException;

  • 唤醒机制:

    • 唤醒第一个等待线程: public final void notify();

    • 唤醒全部等待线程: public final void notifyAll();

    • 如果此时有若干个等待线程的话,那么notify()表示的是唤醒第一个等待的,而其它的线程继续等待,而notifyAll()唤醒所有等待的线程,那个线程的优先级高就有可能先执行。

对于当前的问题主要的解决应该通过Message类完成处理。

范例:修改Message类

package com.lut.JavaPlus;



public class MyThread {
    public static void main(String[] args) {
        Message message=new Message();
        new Thread(new Producer(message)).start();//启动生产者线程
        new Thread(new Consumer(message)).start();//启动消费者线程
    }

}

class Producer implements Runnable{

    private Message message;

    public Producer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i%2==0){
               message.set("张三","法外狂徒");
            }else {
                message.set("李四","合法公民");
            }
        }
    }
}

class Consumer implements Runnable{
    private Message message;
    public Consumer(Message message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(message.get());
        }
    }
}

class Message{
    private String name;
    private String content;
    private boolean flag=true;//表示生产或消费的形式
    //flag=true 允许生产,不予许消费
    //flag=false 允许消费,不允许生产

    public synchronized void set(String name,String content){
        if (this.flag==false){ //无法进行生产,应该等待被消费
            try {
                super.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.name=name;
        this.content=content;
        this.flag=false;//已经生产过了
        super.notify();//唤醒等待的线程
    }

    public synchronized String get(){

        if (this.flag==true){ //还未生产,需要等待
            try {
                super.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {

            }
        }

        try {
            return this.name+"--"+this.content;
        }finally {//不管如何都要执行
            this.flag=true;//继续生产
            super.notify();//唤醒等待线程
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值