多线程(二、线程安全,线程通讯,线程池)

多线程(二、线程安全,线程通讯,线程池)

经过前面的学习,我们发现使用多线程中会有很多问题,这就涉及到了每个线程之间的配合问题。所以我们继续学习线程同步、线程通讯和线程池。

线程安全问题

线程安全问题是多线程的一个重点问题。

经典的卖票问题:

public class Test7 {
    public static void main(String[] args) {
        Station station = new Station();
        Thread thread1 = new Thread(station,"窗口一");
        Thread thread2 = new Thread(station,"窗口二");
        Thread thread3 = new Thread(station,"窗口三");
        Thread thread4 = new Thread(station,"窗口四");

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

class Station implements Runnable{
    private int number = 1;
    private boolean flag = true;
    @Override
    public void run() {
        while (flag) {
            buy();
        }
    }

    public void buy(){
        if (number<=20){
            System.out.println(Thread.currentThread().getName() + "卖出一张票," +  number + "号");
            number++;
        }else {
            flag = false;
        }
    }
}

运行上面代码就可以发现,不同的窗口卖出了同一张车票的情况,这就是线程不安全的情况。

线程同步

线程同步也就是线程安全,多线程只要牵扯到操作成员变量的情况,就可能发生线程安全问题

首先,操作共享数据的代码这些代码同时只让一个线程进行,其他线程运行到这行代码就让等待,只有线程把这些代码执行完毕,才会让给下一个线程执行。

Java中引入了同步监视器来解决这个问题,同步监视器也有两种使用方式,同步代码块和同步方法,

(JDK5中新增了lock锁方法,会在后面学习)

同步代码块
synchronized(同步监视器对象){
    //同步代码···
}

线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,换句话说没有获得同步监视器的锁定,就不能进入同步代码块的执行,线程就会进入阻塞状态,直到对方释放了对同步监视器的锁定

  • 任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程会自动释放对同步监视器对象的锁定

  • Java程序运行使用任何对象来作为同步监视器对象,只要保证共享资源的这几个线程锁的是同一个同步监视器对象即可

使用this作为同步监视器对象

如果我们使用Runnable接口实现线程,那么如果两个线程共用一个Runnable接口实现类对象作为target的话,那么我们把我们的同步监视器对象换成this也可以,锁住这一个实现类,也可以达到线程安全的效果

(如果线程是继承Thread类实现的,同步监视器对象就不能用this,因为此时this代表的是他们自己new的线程)

    public void buy(){
        ==synchronized (this) {==
            if (number <= 20) {
                System.out.println(Thread.currentThread().getName() + "卖出一张票," + number + "号");
                number++;
            } else {
                flag = false;
            }
        ==}==
    }
使用共享资源作为同步监视器对象

这时,我们使用继承Thread类来创建线程,那么就要锁住这个共同使用的station对象

public class Test3 {
    public static void main(String[] args) {
        Station station = new Station();
        SThread sThread1 = new SThread(station);
        SThread sThread2 = new SThread(station);

        sThread1.start();
        sThread2.start();
    }
}

class Station{
    private int number=1;
    private boolean flag = true;

    public void buy(){
        while (flag) {
            if (number <= 10) {
                System.out.println(Thread.currentThread().getName() + "卖出一张票," + number + "号");
                number++;
            } else {
                flag = false;
            }
        }
    }
}

class SThread extends Thread{
    Station station;
    public SThread(Station station){
        this.station=station;
    }
    @Override
    public void run() {
        //锁住共享的对象
        synchronized (station) {
            station.buy();
        }
    }
}
同步方法

同步方法就是使用synchronized关键字来修饰的方法。

对于同步方法而言,无须显示指定同步监视器:

  • 静态方法的同步监视器对象是当前类的Class对象,
  • 非静态方法的同步监视器对象是调当前方法的this对象。
//把这里的买票的方法使用synchronized修饰,那么它就是同步方法了
public ==synchronized== void buy(){
        while (flag) {
            if (number <= 10) {
                System.out.println(Thread.currentThread().getName() + "卖出一张票," + number + "号");
                number++;
            } else {
                flag = false;
            }
        }
    }
释放同步监视器的锁定

会释放锁的操作:

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步方法、同步代码块中遇到了break、return终止了该代码块或方法的执行
  • 当前线程在同步方法、同步代码块中出现了未处理的Exception或Error,导致当前线程异常结束
  • 当前线程在同步方法、同步代码块中执行了锁对象的wait()方法,当前线程被阻塞

不会释放锁对象的操作:

  • 当线程线程执行同步方法、同步代码块时,程序调用了Thread.sleep()、Thread.yield()方法暂停当前线程的执行,
  • 当线程线程执行同步方法、同步代码块时,其他程序调用了该线程的suspend()方法,该线程挂起。(应该尽量避免使用suspend()和resume()这样已经过时的方法来操作线程)
死锁

同步可以保证资源共享操作的正确性,但是过多的同步也会产生问题。

当两个线程,同时拿到对方需要的同步监视器对象,并且等待拿到自己需要的同步监视器对象时,就形成了死锁。发生死锁后,程序不会发生异常,也不会有任何提示,两个所有线程处于阻塞状态,都无法继续。

我们这里可以用买卖商品来重现这个问题,当卖家有货时,他要拿到钱才给货,小明想要买货,但是他想拿到货再给钱,他们两个谁都不让步,就僵持在这里了。

public class Test1 {
    public static void main(String[] args) {
        Object goods = new Object();
        Object money = new Object();
        Thread selle =  new Thread(new Selle(goods,money),"卖家");
        Thread xiaoming = new Thread(new XiaoMing(goods,money),"小明");

        selle.start();
        xiaoming.start();
    }
}

class Selle implements Runnable{
    Object goods;
    Object money;
    public Selle(Object goods,Object money){
        this.goods=goods;
        this.money=money;

    }
    @Override
    public void run() {
        synchronized (goods) {
            System.out.println(Thread.currentThread().getName()+":我有货,先给钱");
            synchronized (money){
                System.out.println("得到钱,给货。。");
            }
        }
    }
}

class XiaoMing implements Runnable{
    Object goods;
    Object money;
    public XiaoMing(Object goods,Object money){
        this.goods=goods;
        this.money=money;

    }
    @Override
    public void run() {
        synchronized (money) {
            System.out.println(Thread.currentThread().getName()+":我有钱,先给货");
            synchronized (goods){
                System.out.println("得到货,给钱。。");
            }
        }
    }
}

线程通信

常用方法:Object类中

返回值方法名说明
voidwait()使当前线程等待,直到有其他方法调用notify或者notifyAll
voidwait(long timeout)使当前线程等待,直到有其他方法调用notify或者notifyAll,或者指定的等待时间已到
voidnotify()唤醒正在等待的一个线程
voidnotifyAll()唤醒正在等待的所有线程

!这三个方法只能用在同步代码块或者同步方法中

生产者消费者模式

生产者和消费者模式(Producer-consumer problem),也称为有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例

该问题描述了两个(多个)共享固定大小缓冲区的线程(即生产者和消费者)实际运行时会发生的问题,生产者产生一定量的数据放到缓冲区中,然后一直重复这个过程;同时 ,消费者在缓冲区中消耗这些数据,该问题就是要保证,生产者在缓冲区满时不会继续生产,消费者也不会在缓冲区为空时消耗数据,

public class Test9 {
    public static void main(String[] args) {
        Workbench workbench = new Workbench();
        Cook cook1 = new Cook(workbench,"C一号");
        Water water1 = new Water(workbench,"F一号");
        Cook cook2 = new Cook(workbench,"C二号");
        Water water2 = new Water(workbench,"F二号");

        cook1.start();
        water1.start();
        cook2.start();
        water2.start();
    }
}

class Workbench {
    public static final int MAX_NUM = 10;
    public int num;

    //往工作台上放快餐
    public synchronized void put(){
        //当工作台满了,就让调用这个方法的厨师线程等待一下
        while (num>=MAX_NUM){
            try {
                System.out.println("工作台放满了,厨师等待···");
                this.wait();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"厨师制作了一份快餐,此时工作台上有"+num+"份");
        this.notifyAll();
    }

    //从工作台取餐
    public synchronized void take(){
        //当工作台没有快餐时,让调用这个方法的服务员等待一下
        while (num<=0){
            try {
                System.out.println("没有快餐,服务员等待···");
                this.wait();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"服务员取走了一份快餐,此时工作台上有"+num+"份");
        this.notifyAll();
    }
}

class Cook extends Thread{
    private Workbench workbench;
    public Cook(Workbench workbench,String name){
        super(name);
        this.workbench=workbench;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            workbench.put();
        }
    }
}

class Water extends Thread{
    private Workbench workbench;
    public Water(Workbench workbench,String name){
        super(name);
        this.workbench=workbench;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            workbench.take();
        }
    }
}

线程池

前面的学习中,我们每完成一个线程任务就创建一个线程,这个线程执行完毕后,就被销毁了。下次执行其他任务还要继续创建线程,在实际使用中,创建和销毁一个线程的开销是很大的,每一个活动的线程都会消耗一定的系统资源。

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

(线程池中有很多操作都是与优化资源相关的)

合理使用线程池的好处:

  • 降低资源的消耗,减少了创建和销毁线程的次数
  • 提高响应速度,当任务到达时,任务可以不需要等待线程线程创建就可以立即执行
  • 提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线程的数目,防止同时创建多条线程消耗过多内存导致服务器宕机

应用:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * ExecutorService 线程池对象
 * Executors中的方法可以返回各种各样的线程池对象
 */
public class Test12 {
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);
        //创建Runnable实现类对象
        MYRunnable r1 = new MYRunnable();
        //从线程池中获取一个线程来执行run中的任务
        service.submit(r1);
        service.submit(r1);
        service.submit(r1);
        service.submit(r1);
        //关闭线程池
        service.shutdown();
    }
}

class MYRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"执行线程任务中··");
    }
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值