java并发之线程同步(synchronized和锁机制)

多个执行线程共享一个资源的情景,是并发编程中最常见的情景之一。多个线程读或者写相同的数据等情况时可能会导致数据不一致。为了解决这些问题,引入了临界区概念。临界区是一个用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程执行。

Java提供了同步机制。当一个线程试图访问一个临界区时,它将使用一种同步机制来查看是不是已有其他线程进入临界区。如果没有其他线程进入临界区,它就可以进入临界区;如果已有线程进入了临界区,它就被同步机制挂起,直到进入的线程离开这个临界区。如果在等待进入临界区的线程不止一个,JVM会 随机选择其中的一个,其余的将继续等待。
概念比较好理解,具体在java程序中是如何体现的呢?临界区对应的代码是怎么样的?

使用synchronized实现同步方法

每一个用synchronized关键字声明的方法都是临界区。在Java中, 同一个对象的临界区,在同一时间只有一个允许被访问。
注意:用synchronized关键字声明的静态方法,同时只能被一个执行线程访问,但是其他线程可以访问这个对象的非静态方法。即:两个线程可以同时访问一个对象的两个不同的synchronized方法,其中一个是静态方法,一个是非静态方法。
知道了synchronized关键字的作用,再来看一下synchronized关键字的使用方式。
  • 在方法声明中加入synchronized关键字
  • 1 public synchronized void addAmount(double amount) {
    2 }
  • 在代码块中使用synchronized关键字,obj一般可以使用this关键字表示本类对象
  • 1 synchronized(obj){
    2 }
需要注意的是:前面已经提到,引入synchronized关键字是为了声明临界区,解决在多线程环境下共享变量的数据更改安全问题。那么,一般用到synchronized关键字的地方也就是 在对共享数据 访问或者修改的地方。下面举一个例子,例子场景是这样:公司定时会给账户打款,银行对账户进行扣款。那么款项对于银行和公司来说就是一个共享数据。那么synchronized关键字就应该在修改账户的地方使用。

声明一个Account类:

public class Account {
    private double balance;
    public double getBalance() {
        return balance;
    }
    public void setBalance(double balance) {
        this.balance = balance;
    }
    public synchronized void addAmount(double amount) {
        double tmp=balance;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp+=amount;
        balance=tmp;
    }
    public synchronized void subtractAmount(double amount) {
        double tmp=balance;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp-=amount;
        balance=tmp;
    }
}

Bank类扣款:

public class Bank implements Runnable {
    private Account account;
    public Bank(Account account) {
        this.account=account;
    }
    public void run() {
        for (int i=0; i<100; i++){
            account.subtractAmount(1000);
        }
    }
}
Company类打款:
public class Company implements Runnable {
    private Account account;
    public Company(Account account) {
        this.account=account;
    }

    public void run() {
        for (int i=0; i<100; i++){
            account.addAmount(1000);
        }
    }
}
这里需要注意的就是:在Bank和Company的构造函数里面传递的参数是Account,就是一个共享数据。
Main函数:
public class Main {
    public static void main(String[] args) {
        Account    account=new Account();
        account.setBalance(1000);
        Company    company=new Company(account);
        Thread companyThread=new Thread(company);
        Bank bank=new Bank(account);
        Thread bankThread=new Thread(bank);

        companyThread.start();
        bankThread.start();
        try {
            companyThread.join();
            bankThread.join();
            System.out.printf("Account : Final Balance: %f\n",account.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
这个例子比较简单,但是可以说明问题。
补充:
1、synchronized关键字会降低应用程序的性能,因此只能在并发场景中修改共享数据的方法上使用它。
2、临界区的访问应该尽可能的短。方法的其余部分保持在synchronized代码块之外,以获取更好的性能

使用非依赖属性实现同步

非依赖属性:例如在一个类中有两个非依赖属性,Object obj1,Object obj2;他们被多个线程共享,那么同一时间只允许一个线程访问其中的一个属性变量,其他的 某个线程访问另一个属性变量。
举例如下:两个看电影的房间和两个售票口,一个售票处卖出的一张票,只能用于其中的一个电影院。不能同时作用于两个电影房间。
Cinema类:
public class Cinema {
    private long vacanciesCinema1;
    private long vacanciesCinema2;

    private final Object controlCinema1, controlCinema2;

    public Cinema(){
        controlCinema1=new Object();
        controlCinema2=new Object();
        vacanciesCinema1=20;
        vacanciesCinema2=20;
    }
    
    public boolean sellTickets1 (int number) {
        synchronized (controlCinema1) {
            if (number<vacanciesCinema1) {
                vacanciesCinema1-=number;
                return true;
            } else {
                return false;
            }
        }
    }
    
    public boolean sellTickets2 (int number){
        synchronized (controlCinema2) {
            if (number<vacanciesCinema2) {
                vacanciesCinema2-=number;
                return true;
            } else {
                return false;
            }
        }
    }
    
    public boolean returnTickets1 (int number) {
        synchronized (controlCinema1) {
            vacanciesCinema1+=number;
            return true;
        }
    }
    public boolean returnTickets2 (int number) {
        synchronized (controlCinema2) {
            vacanciesCinema2+=number;
            return true;
        }
    }
    public long getVacanciesCinema1() {
        return vacanciesCinema1;
    }
    public long getVacanciesCinema2() {
        return vacanciesCinema2;
    }
}
这样的话,vacanciescinema1和vacanciescinema2(剩余票数)是独立的,因为他们属于不同的对象。这种情况下,只允许一个同时有一个线程修改vacanciescinema1或者vacanciescinema2,但是允许有两个线程同时修改vacanciescinema1和vacanciescinema2。

在同步块中使用条件(wait(),notify(),notifyAll())

首先需要明确:
  1. 上述三个方法都是Object 类的方法。
  2. 上述三个方法都必须在同步代码块中使用。
当一个线程调用wait()方法时,JVM将这个线程置入休眠,并且释放控制这个同步代码块的对象,同时允许其他线程执行这个对象控制的其他同步代码块。为了唤醒这个线程,必须在这个对象控制的某个同步代码块中调用notify()或者notifyAll()方法。
上述一段话很重要!!!它说明了使用上述三个函数的方法以及方法的作用。
 
wait():将线程置入休眠状态,并且释放控制这个同步代码块的对象,释放了以后其他线程就可以执行这个对象控制的其他代码块。也就是可以进入了。这个和Thread.sleep(millions)方法不同,sleep()方法是睡眠指定时间后自动唤醒。
notify()/notifyAll():使用wait()方法休眠的线程需要在该对象控制的某个同步代码块中 调用notify或者notifyAll()方法去唤醒,才能进入就绪状态等待JVM的调用。否则一致处于休眠状态。
难点:线程休眠和唤醒的时机,就是说什么时候调用notify()或者notifyAll()方法???
拿生产者和消费者的例子来说:生产者往队列中塞数据,消费者从队列中取数据,所以这个队列是共享数据
数据存储类 EventStorage
塞数据方法和取数据方法:set()、get()
public synchronized void set(){
            while (storage.size()==maxSize){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            storage.add(new Date());
            System.out.printf("Set: %d\n", storage.size());
            notify();
    }    
   public synchronized void get(){
            while (storage.size()==0){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.printf("Get: %d: %s\n",storage.size(),((LinkedList<?>)storage).poll());
            notify();
    }
分析上面这个简单的程序:
1、方法使用synchronized关键字声明同步代码块。所以这个函数里面可以使用同步条件。
2、首先判断队列是否已经满了,这里要使用while而不是if。为什么呢?while是一致查询是否已经满了,而if是判断一次就完事了。
3、如果满了,调用wait()方法释放该对象,那么其他方法(例如get())就可以使用这个对象了。get()方法进入后取出一个数据,然后唤醒上一个被休眠的线程。
4、虽然线程被唤醒了, 但是由于get()方法线程占用对象锁,所以set()方法处于阻塞状态。直到get()方法取出所有的数据满足休眠条件以后,set()方法重新执行
5、重复以上步骤

使用锁实现同步

Java提供了同步代码块的另一种机制,它比synchronized关键字更强大也更加灵活。这种机制基于Lock接口及其实现类(例如:ReentrantLock)
它比synchronized关键字好的地方:
1、提供了更多的功能。tryLock()方法的实现,这个方法试图获取锁,如果锁已经被其他线程占用,它将返回false并继续往下执行代码。
2、Lock接口允许分离读和写操作,允许多个线程读和只有一个写线程。ReentrantReadWriteLock
3、具有更好的性能
一个锁的使用实例:
public class PrintQueue {
    private final Lock queueLock=new ReentrantLock();

    public void printJob(Object document){
        queueLock.lock();
        
        try {
            Long duration=(long)(Math.random()*10000);
            System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",Thread.currentThread().getName(),(duration/1000));
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
    }
}
声明一把锁,其中ReentrantLock(可重入的互斥锁)是Lock接口的一个实现
1 private final Lock queueLock=new ReentrantLock();
然后在函数里面调用lock()方法声明同步代码块(临界区)
1 queueLock.lock();
最后在finally块中释放锁,重要!!!
1 queueLock.unlock();

使用读写锁实现同步数据访问

锁机制最大的改进之一就是ReadWriteLock接口和他的唯一实现类ReentrantReadWriteLock.这个类有两个锁,一个是读操作锁,一个是写操作锁。 使用读操作锁时可以允许多个线程同时访问,使用写操作锁时只允许一个线程进行。在一个线程执行写操作时,其他线程不能够执行读操作。
 
在调用写操作锁时,使用一个线程。
写操作锁的用法:
public void setPrices(double price1, double price2) {
        lock.writeLock().lock();
        this.price1=price1;
        this.price2=price2;
        lock.writeLock().unlock();
    }
读操作锁:
public double getPrice1() {
        lock.readLock().lock();
        double value=price1;
        lock.readLock().unlock();
        return value;
    }
    public double getPrice2() {
        lock.readLock().lock();
        double value=price2;
        lock.readLock().unlock();
        return value;
    }

修改锁的公平性

ReentrantLock和ReetrantReadWriteLock构造函数都含有一个布尔参数fair。默认fair为false,即非公平模式。
公平模式:当有很多线程在等待锁时,锁将选择一个等待时间最长的线程进入临界区。
非公平模式:当有很多线程在等待锁时,锁将随机选择一个等待区(就绪状态)的线程进入临界区。
这两种模式只适用于lock()和unlock()方。而Lock接口的tryLock()方法没有将线程置于休眠,fair属性并不影响这个方法。

在锁中使用多条件(Multri Condition)

锁条件可以和synchronized关键字声明的临界区的方法(wait(),notify(),notifyAll())做类比。锁条件通过Conditon接口声明。Condition提供了挂起线程和唤醒线程的机制。
使用方法:
private Condition lines;
    private Condition space;
     */
    public void insert(String line) {
        lock.lock();
        try {
            while (buffer.size() == maxSize) {
                space.await();
            }
            buffer.offer(line);
            System.out.printf("%s: Inserted Line: %d\n", Thread.currentThread()
                    .getName(), buffer.size());
            lines.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
public String get() {
        String line=null;
        lock.lock();        
        try {
            while ((buffer.size() == 0) &&(hasPendingLines())) {
                lines.await();
            }
            
            if (hasPendingLines()) {
                line = buffer.poll();
                System.out.printf("%s: Line Readed: %d\n",Thread.currentThread().getName(),buffer.size());
                space.signalAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return line;
    }

转自:http://www.cnblogs.com/uodut/p/6775419.html

猜你喜欢

转载自blog.csdn.net/w20228396/article/details/79565807