Java基础(九)——线程同步、线程安全的类、生产者消费者

206 阅读10分钟

目录

线程同步

同步代码块

解决上一次卖票问题的同步线程同步方法:

同步的好处和弊端

同步方法

线程安全的类

StringBuffer

Vector

Hashtable

Lock锁

生产者消费者

模式概述

生产者消费者案例

代码


 

线程同步

同步代码块

锁多条语句操作共享数据,可以使用同步代码块实现
格式:

synchronized(任意对象) {
    多条语句操作共享数据的代码
}

synchronized(任意对象): 就相当于给代码加锁了,任意对象就可以看成是一把锁

解决上一次卖票问题的同步线程同步方法:

package cn.itcast.day8.买票;

public class SellTicket implements Runnable {
    private int tickets = 100;
    //定义同一把锁
    private Object obj = new Object();

    @Override
    public void run() {
//        A:判断票数大于0,就卖票,并告知是哪个窗口卖的
//        B:卖了票之后,总票数要减1
//        C:票没有了,也可能有人来问,所以这里用死循环让卖票的动作一直执行
        //问题:相同的票出现了多次  ---原因:线程执行的随机性导致的
        boolean flag = true;
        while (flag == true){
            synchronized (obj){
                if (tickets > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //t1抢到了CPU的执行权,在控制台输出,窗口1出售1张票
                    //假设t1继续拥有CPU执行器,就会执行tickets--;操作
                    System.out.println(Thread.currentThread().getName()+"正在出售第"+tickets+"张票");
                    tickets--;
                }
                else {
                    System.out.println("票已经卖完了");
                    flag = false;
                }
            }
        }
    }
}

结果:

同步的好处和弊端

好处: 解决了多线程的数据安全问题

弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

同步方法

同步方法:就是把synchronized关键字加到方法上
格式:
修饰符 synchronized 返回值类型 方法名(方法参数) { }

同步方法的锁对象是:this

静态同步方法的锁对象是自解码对象:类名.class

例子:使用this.sell();调用

public synchronized void sell(){
            //t1进来后,就会把这段代码锁起来
            if (tickets > 0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //t1抢到了CPU的执行权,在控制台输出,窗口1出售1张票
                //假设t1继续拥有CPU执行器,就会执行tickets--;操作
                System.out.println(Thread.currentThread().getName()+"正在出售第"+tickets+"张票");
                tickets--;
            }
            else {
                System.out.println("票已经卖完了");
                flag = false;
            }
    }

线程安全的类

StringBuffer

  • public final class StringBuffer
    extends Object
    implements Serializable, CharSequence
    

线程安全,可变的字符序列。 字符串缓冲区就像一个String ,但可以修改。 在任何时间点,它包含一些特定的字符序列,但可以通过某些方法调用来更改序列的长度和内容。

字符串缓冲区可以安全地被多个线程使用。 这些方法在必要时进行同步,以便任何特定实例上的所有操作都按照与所涉及的各个线程所执行的方法调用顺序一致的顺序发生。

StringBuffer的主要StringBufferappendinsert方法,它们被重载以便接受任何类型的数据。 每个都有效地将给定的数据转换为字符串,然后将该字符串的字符附加或插入到字符串缓冲区。 append方法总是在缓冲区的末尾添加这些字符; insert方法将insert添加到指定点。

线程安全,可变的字符序列
从版本JDK 5开始,被StringBuilder 替代。 通常应该使用StringBuilder类, 因为它支持所有相同的操作,但它更快,因为它不执行同步

 

Vector

Vector类实现了可扩展的对象数组。 像数组一样,它包含可以使用整数索引访问的组件。 但是, Vector的大小可以根据需要增长或缩小,以适应在创建Vector之后添加和删除项目。

从Java 2平台v1.2开始,该类改进了List接[ ],使其成为Java Collections Framework的成员。与新的集合实现不同,Vector被同步。 如果不需要线程安全的实现,建议使用ArrayList代替Vector

 

Hashtable

public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, Serializable

该类实现了一个哈希表,它将键映射到值。 任何非null对象都可以用作键值或值。

为了从散列表成功存储和检索对象,用作键的对象必须实现hashCode方法和equals方法。

Hashtable一个实例有两个影响其性能的参数: 初始容量负载因子容量是哈希表中的数, 初始容量只是创建哈希表时的容量。 请注意,哈希表是打开的 :在“哈希冲突”的情况下,单个存储桶存储多个条目,必须依次搜索。 负载因子是在容量自动增加之前允许哈希表得到满足的度量。 初始容量和负载因子参数仅仅是实现的暗示。 关于何时以及是否调用rehash方法的具体细节是依赖于实现的。

如果不需要线程安全的实现,建议使用HashMap代替Hashtable

如果需要线程安全的并发实现,那么建议使用ConcurrentHashMap代替Hashtable

Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁, JDK5I以后提供了一 个新的锁对象Lock

 

方法类型方法名和功能
voidlock()获得锁。
voidlockInterruptibly()获取锁定,除非当前线程是 interrupted
ConditionnewCondition()返回一个新Condition绑定到该实例Lock实例。
booleantryLock()只有在调用时才可以获得锁。
booleantryLock(long time, TimeUnit unit)如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁。
voidunlock()释放锁。

Lock加锁代码:

public class SellLock implements Runnable {
    private int tickets = 100;
    //定义同一把锁
    private Lock lock = new ReentrantLock();
    boolean flag = true;
    @Override
    public void run() {
        while (flag == true){
            try{
                //加锁
                lock.lock();
                if (tickets > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在出售第"+tickets+"张票");
                    tickets--;
                }
                else {
                    System.out.println("票已经卖完了");
                    flag = false;
                }
            }finally {
                //释放锁
                lock.unlock();
            }
        }
    }
}

 

生产者消费者

模式概述

生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻
所谓生产者消费者问题,实际上主要是包含了两类线程:

  • 一类是生产者线程用于生产数据
  • 一类是消费者线程用于消费数据

为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

  • 生产者生产数据之后直接放置在共享数据区中, 并不需要关心消费者的行为
  • 消费者只需要从共享数据区中去获取数据, 并不需要关心生产者的行为

 

方法类型方法名和功能
voidnotify()唤醒正在等待对象监视器的单个线程。
voidnotifyAll()唤醒正在等待对象监视器的所有线程。
StringtoString()返回对象的字符串表示形式。
voidwait()导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。
voidwait(long timeout)导致当前线程等待,直到另一个线程调用 notify()方法或该对象的 notifyAll()方法,或者指定的时间已过。
voidwait(long timeout, int nanos)导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法,或者某些其他线程中断当前线程,或一定量的实时时间。

生产者消费者案例

生产者消费者案例中包含的类:
●奶箱类(Box): 定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
生产者类(Producer):实现Runnable接口,重写run()方法, 调用存储牛奶的操作
●消费者类(Customer): 实现Runnable接口,重写run()方法,调用获取牛奶的操作
●测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下

  • ①创建奶箱对象,这是共享数据区域
  • ②创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
  • ③创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
  • ④创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
  • ⑤启动线程

代码

主程序代码:

package cn.itcast.day8.Box;

public class BoxDemo {
    public static void main(String[] args) {
//        创建奶箱对象,这是共享数据区域
        Box b = new Box();

        //创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
        Producer p = new Producer(b);
        //创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
        Customer c = new Customer(b);

//        创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);

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

奶箱代码:

package cn.itcast.day8.Box;

public class Box {
//    定义一个成员变量,表示第x瓶奶
    private int milk;
    //定义一个成员变量,表示奶箱的状态
    private boolean state = false;

    //提供存储牛奶和获取牛奶的操作
    public synchronized void put(int milk) throws InterruptedException {
        //如果有牛奶等待消费
        if(state){
            wait();
        }
        //如果没有牛奶就生产牛奶
        this.milk = milk;
        System.out.println("送奶工将第" + this.milk + "瓶奶放入奶箱");
        //生产完毕之后,改变奶箱状态
        state = true;
        //唤醒其他等待线程
        notify();
    }
    public synchronized void get() throws InterruptedException {
        if(!state){
            wait();
        }
        //如果有牛奶就消费牛奶
        System.out.println("用户拿到第"+this.milk+"瓶奶");
        //消费完改变奶箱状态
        state = false;
        //唤醒其他等待线程
        notify();
    }

}

生产者(送奶工)代码:

package cn.itcast.day8.Box;

//实现Runnable接口
public class Producer implements Runnable{
    private Box b;
    
    public Producer(Box b) {
        this.b = b;
    }

    //重写run()方法, 调用存储牛奶的操作
    @Override
    public void run() {
        for (int i = 1; i <= 5 ; i++) {
            try {
                b.put(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者代码:

package cn.itcast.day8.Box;

public class Customer implements Runnable {
    private Box b;
    public Customer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        //重写run()方法,调用获取牛奶的操作
        while (true){
            try {
                b.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

程序运行效果:

 

 

 

 

一起学习,一起进步 -.- ,如有错误,可以发评论