线程同步synchronized

转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/70670024
本文出自:【顾林海的博客】

前言

在编写多线程应用时,读写相同的数据,最有可能发生数据的错误或不一致,为了防止这些错误的发生,我们引入了临界区概念,临界区是一个用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程运行。为了更好的实现临界区,Java提供了同步机制,所谓的同步机制是指:当一个线程试图访问一个临界区时,它将使用一种同步机制来查看是不是已经有其他线程进入临界区,如果没有其他线程进入临界区,它就可以进入临界区,否则它就被挂起,直到进入的线程离开这个临界区。关于线程的基础知识可以查看《有关线程的相关知识(1)》和《有关线程的相关知识(2)


synchronized的使用

在Java中使用synchronized关键字来控制一个方法的并发访问。如果一个对象使用synchronized关键字声明,说明只有一个执行线程被允许访问它。如果synchronized关键字声明的是静态方法,同时只能够被一个执行线程访问,但是其他线程可以访问这个对象的非静态方法,当出现这种情况时就要注意两个方法是否改变了相同的数据,从而导致数据不一致。

接下来我们编写一个银行存取款应用,不用synchronized同步存取款方法,在存取款方法中每次进行操作时休眠50毫秒,在休眠50毫秒的这个时间段内,其他线程可能会执行这个方法,最终的账户余额会改变,从而引发错误。

/**
 * 用户账户类
 * 
 * <pre>
 * 含有新增余额和扣除余额操作
 * </pre>
 * 
 * @author gulinhai
 *
 */
public class Account {

    //账户余额
    private double balance;

    public double getBalance(){
        return balance;
    }

    public void setBalance(double balance){
        this.balance=balance;
    }

    /**
     * 增加余额
     * @param amount
     */
    public void addAmount(double amount){
        double tmp=balance;
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp+=amount;
        balance=tmp;
    }

    /**
     * 扣除余额
     * @param amount
     */
    public void subtractAmount(double amount){
        double tmp=balance;
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp-=amount;
        balance=tmp;
    }

}



创建一个新增余额的线程,通过for循环100次,每次增加1000:

public class AddAmount implements Runnable{

    private Account mAccount;

    public AddAmount(Account account){
        this.mAccount=account;
    }

    @Override
    public void run() {
        for(int i=0;i<100;i++){
            mAccount.addAmount(1000);
        }
    }

}



创建一个减少余额的线程,通过for循环100次,每次减少1000:

public class SubtractAmount implements Runnable{

    private Account mAccount;

    public SubtractAmount(Account account){
        this.mAccount=account;
    }

    @Override
    public void run() {
        for(int i=0;i<100;i++){
            mAccount.subtractAmount(1000);
        }
    }
}



创建银行类:

public class Bank {
    public static void main(String[] args) {
        Account mAccount=new Account();
        mAccount.setBalance(1000);

        AddAmount addAmount=new AddAmount(mAccount);
        Thread addThread=new Thread(addAmount);

        SubtractAmount subtractAmount=new SubtractAmount(mAccount);
        Thread subtractThread=new Thread(subtractAmount);

        System.out.println("余额:"+mAccount.getBalance());

                addThread.start();
        subtractThread.start();

        try {
            subtractThread.join();
            addThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("余额:"+mAccount.getBalance());

    }
}

在主类中创建Account对象mAccount,并进行初始化1000,随后创建新增余额和减少余额线程,并执行这两个线程,最后输出余额,按照日常生活中,新增100次余额,每次1000,随后扣除100次,每次扣1000,最后的结果应该是初始化金额1000,但我们运行上面的程序。

余额:1000.0
余额:-5000.0

可以看到每次运行的结果大部分是不想同的,也就是多个线程同时操作了同一个数据,造成数据不一致,这里我们用synchronized关键字来同步新增和扣除方法,使得每次只有一个线程执行新增或扣除操作:

/**
     * 增加余额
     * @param amount
     */
    public synchronized void addAmount(double amount){
        double tmp=balance;
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp+=amount;
        balance=tmp;
    }

    /**
     * 扣除余额
     * @param amount
     */
    public synchronized void subtractAmount(double amount){
        double tmp=balance;
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp-=amount;
        balance=tmp;
    }
运行的结果:
余额:1000.0
余额:1000.0

使用synchronized关键字,保证了在并发程序中对共享数据的正确访问。这里要注意了synchronized关键字会降低应用程序的性能,因此只能在并发场景中需要修改共享数据时使用,我们推荐这样使用synchronized:方法的其余部分保持在synchronized代码块之外,以获取更好的性能。这样使用就需要把对象引入作为传入参数。

接下来在编写一个程序,并在类中添加两个非依赖属性,用于多线程共享,同一时刻只允许一个线程访问一个属性变量,其他某个线程访问另一个变量。程序中创建两个数字,初始化各20,在两个线程中对这两个数字进行添加和减少操作。

public class Test {

    private int number1;
    private int number2;

    private final Object numberObject1;
    private final Object numberObject2;

    public Test(){
        numberObject1=new Object();
        numberObject2=new Object();

        number1=20;
        number2=20;
    }

    public int getNumber1(){
        return number1;
    }

    public int getNumber2(){
        return number2;
    }

    public void substractNmbuer1(int number){
        synchronized (numberObject1) {
            if(number<number1){
                number1-=number;
            }
        }
    }

    public void substractNmbuer2(int number){
        synchronized (numberObject2) {
            if(number<number2){
                number2-=number;
            }
        }
    }

    public void addNumber1(int number){
        synchronized (numberObject1) {
            number1+=number;
        }
    }

    public void addNumber2(int number){
        synchronized (numberObject2) {
            number1+=number;
        }
    }
}
public class Test1 implements Runnable{

    private Test mTest;

    public Test1(Test test){
        this.mTest=test;
    }

    @Override
    public void run() {
        mTest.substractNmbuer1(2);
        mTest.substractNmbuer1(5);
        mTest.addNumber1(3);
        mTest.substractNmbuer2(2);
        mTest.substractNmbuer2(4);
        mTest.substractNmbuer1(3);
    }

}
public class Test2 implements Runnable{

    private Test mTest;

    public Test2(Test test){
        this.mTest=test;
    }

    @Override
    public void run() {
        mTest.substractNmbuer2(2);
        mTest.substractNmbuer2(5);
        mTest.addNumber1(3);
        mTest.substractNmbuer2(2);
        mTest.substractNmbuer1(4);
        mTest.addNumber2(6);
        mTest.substractNmbuer1(3);
        mTest.substractNmbuer1(3);
    }

}
public class Client {

    public static void main(String[] args) {
        Test test=new Test();

        Test1 test1=new Test1(test);
        Thread thread1=new Thread(test1);

        Test2 test2=new Test2(test);
        Thread thread2=new Thread(test2);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("number1剩余:"+test.getNumber1()+"\nnumber2剩余:"+test.getNumber2());
    }

}
运行程序:
number1剩余:12
number2剩余:5

在并发编程中有一个典型的问题就是生产者-消费者问题,比如我们有一个数据缓冲区,一个或者多个数据生产者,一个或者多个数据消费者将从数据缓冲区中取走数据,这个缓冲区是一个共享结构,因此需要使用同步机制,但这里有个问题,如果使用synchronized关键字,如果缓冲区满了,生产者就不能再放入数据,如果缓冲区空了,消费者也不能读取数据。

为了解决这个问题,Java在Object类中提供了wait()、notify()和notifyAll()方法。我们可以在同步代码块中调用wait()方法,这时JVM将这个线程置入休眠,并且释放控制这个同步代码块的对象,同时允许其他线程执行这个对象控制的其他同步代码块,这时在这个对象控制的某个同步代码块中调用notify()或者notifyAll()方法,来唤醒这个线程。

接下来实现一个生产者-消费者问题。

/**
 * 创建数据存储类EventStorage,并保存一个最大值maxSize和数据集合
 * LinkedList<Date>来保存存入的日期。
 * @author gulinhai
 *
 */
public class EventStorage {

    private int maxSize;
    private LinkedList<Date> storage;

    public EventStorage(){
        this.maxSize=10;
        this.storage=new LinkedList<>();
    }

    public int size(){
        return storage.size();
    }

    /**
     * 同步方法set(),保存数据到存储列表storage。
     * 首先检查列表是不是满了,如果满了,就调用wait()方法挂起线程并等待空余空间的出现,
     * 最后调用notifyAll()方法唤醒所有因调用wait()方法进入休眠的线程。
     */
    public synchronized void set(){
        while(storage.size()==maxSize){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(new Date());
        notifyAll();
    }

    /**
     * 同步方法set(),从存储列表storage中获取数据。
     * 首先检查列表是不是空了,如果空了,就调用wait()方法挂起线程并等待列表中数据的出现,
     * 最后调用notifyAll()方法唤醒所有因调用wait()方法进入休眠的线程。
     */
    public synchronized void get(){
        while(storage.size()==0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.poll();
        notifyAll();
    }

}
/**
 * 生产者
 * @author gulinhai
 *
 */
public class Producer implements Runnable{

    private EventStorage storage;

    public Producer(EventStorage storage){
        this.storage=storage;
    }

    @Override
    public void run() {
        for(int i=0;i<100;i++){
            storage.set();
        }
    }

}
/**
 * 消费者
 * @author gulinhai
 *
 */
public class Consumber implements Runnable{

    private EventStorage storage;

    public Consumber(EventStorage storage){
        this.storage=storage;
    }

    @Override
    public void run() {
        for(int i=0;i<100;i++){
            storage.get();
        }
    }

}
public class Client {

    public static void main(String[] args) {
        EventStorage storage=new EventStorage();

        Producer producer=new Producer(storage);
        Thread producerThread=new Thread(producer);

        Consumber consumber=new Consumber(storage);
        Thread consumberThread=new Thread(consumber);

        producerThread.start();
        consumberThread.start();

        try {
            producerThread.join();
            consumberThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("缓冲区数据个数:"+storage.size());

    }

}
输出:
缓冲区数据个数:0

注意:必须在while循环中调用wait(),并且不断查询while的条件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值