• 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类:
    复制代码
     1 public class Account {
     2     private double balance;
     3     public double getBalance() {
     4         return balance;
     5     }
     6     public void setBalance(double balance) {
     7         this.balance = balance;
     8     }
     9     public synchronized void addAmount(double amount) {
    10         double tmp=balance;
    11         try {
    12             Thread.sleep(10);
    13         } catch (InterruptedException e) {
    14             e.printStackTrace();
    15         }
    16         tmp+=amount;
    17         balance=tmp;
    18     }
    19     public synchronized void subtractAmount(double amount) {
    20         double tmp=balance;
    21         try {
    22             Thread.sleep(10);
    23         } catch (InterruptedException e) {
    24             e.printStackTrace();
    25         }
    26         tmp-=amount;
    27         balance=tmp;
    28     }
    29 }
    复制代码
    Bank类扣款:
    复制代码
     1 public class Bank implements Runnable {
     2     private Account account;
     3     public Bank(Account account) {
     4         this.account=account;
     5     }
     6     public void run() {
     7         for (int i=0; i<100; i++){
     8             account.subtractAmount(1000);
     9         }
    10     }
    11 }
    复制代码
    Company类打款:
    复制代码
     1 public class Company implements Runnable {
     2     private Account account;
     3     public Company(Account account) {
     4         this.account=account;
     5     }
     6 
     7     public void run() {
     8         for (int i=0; i<100; i++){
     9             account.addAmount(1000);
    10         }
    11     }
    12 }
    复制代码
    这里需要注意的就是:在Bank和Company的构造函数里面传递的参数是Account,就是一个共享数据。
    Main函数:
    复制代码
     1 public class Main {
     2     public static void main(String[] args) {
     3         Account    account=new Account();
     4         account.setBalance(1000);
     5         Company    company=new Company(account);
     6         Thread companyThread=new Thread(company);
     7         Bank bank=new Bank(account);
     8         Thread bankThread=new Thread(bank);
     9 
    10         companyThread.start();
    11         bankThread.start();
    12         try {
    13             companyThread.join();
    14             bankThread.join();
    15             System.out.printf("Account : Final Balance: %f
    ",account.getBalance());
    16         } catch (InterruptedException e) {
    17             e.printStackTrace();
    18         }
    19     }
    20 }
    复制代码
    这个例子比较简单,但是可以说明问题。
    补充:
    1、synchronized关键字会降低应用程序的性能,因此只能在并发场景中修改共享数据的方法上使用它。
    2、临界区的访问应该尽可能的短。方法的其余部分保持在synchronized代码块之外,以获取更好的性能

    使用非依赖属性实现同步

    非依赖属性:例如在一个类中有两个非依赖属性,Object obj1,Object obj2;他们被多个线程共享,那么同一时间只允许一个线程访问其中的一个属性变量,其他的某个线程访问另一个属性变量。
    举例如下:两个看电影的房间和两个售票口,一个售票处卖出的一张票,只能用于其中的一个电影院。不能同时作用于两个电影房间。
    Cinema类:
    复制代码
     1 public class Cinema {
     2     private long vacanciesCinema1;
     3     private long vacanciesCinema2;
     4 
     5     private final Object controlCinema1, controlCinema2;
     6 
     7     public Cinema(){
     8         controlCinema1=new Object();
     9         controlCinema2=new Object();
    10         vacanciesCinema1=20;
    11         vacanciesCinema2=20;
    12     }
    13     
    14     public boolean sellTickets1 (int number) {
    15         synchronized (controlCinema1) {
    16             if (number<vacanciesCinema1) {
    17                 vacanciesCinema1-=number;
    18                 return true;
    19             } else {
    20                 return false;
    21             }
    22         }
    23     }
    24     
    25     public boolean sellTickets2 (int number){
    26         synchronized (controlCinema2) {
    27             if (number<vacanciesCinema2) {
    28                 vacanciesCinema2-=number;
    29                 return true;
    30             } else {
    31                 return false;
    32             }
    33         }
    34     }
    35     
    36     public boolean returnTickets1 (int number) {
    37         synchronized (controlCinema1) {
    38             vacanciesCinema1+=number;
    39             return true;
    40         }
    41     }
    42     public boolean returnTickets2 (int number) {
    43         synchronized (controlCinema2) {
    44             vacanciesCinema2+=number;
    45             return true;
    46         }
    47     }
    48     public long getVacanciesCinema1() {
    49         return vacanciesCinema1;
    50     }
    51     public long getVacanciesCinema2() {
    52         return vacanciesCinema2;
    53     }
    54 }
    复制代码
    这样的话,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()
    复制代码
     1 public synchronized void set(){
     2             while (storage.size()==maxSize){
     3                 try {
     4                     wait();
     5                 } catch (InterruptedException e) {
     6                     e.printStackTrace();
     7                 }
     8             }
     9             storage.add(new Date());
    10             System.out.printf("Set: %d
    ", storage.size());
    11             notify();
    12     }    
    13    public synchronized void get(){
    14             while (storage.size()==0){
    15                 try {
    16                     wait();
    17                 } catch (InterruptedException e) {
    18                     e.printStackTrace();
    19                 }
    20             }
    21             System.out.printf("Get: %d: %s
    ",storage.size(),((LinkedList<?>)storage).poll());
    22             notify();
    23     }
    复制代码
     
    分析上面这个简单的程序:
    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、具有更好的性能
    一个锁的使用实例:
    复制代码
     1 public class PrintQueue {
     2     private final Lock queueLock=new ReentrantLock();
     3 
     4     public void printJob(Object document){
     5         queueLock.lock();
     6         
     7         try {
     8             Long duration=(long)(Math.random()*10000);
     9             System.out.printf("%s: PrintQueue: Printing a Job during %d seconds
    ",Thread.currentThread().getName(),(duration/1000));
    10             Thread.sleep(duration);
    11         } catch (InterruptedException e) {
    12             e.printStackTrace();
    13         } finally {
    14             queueLock.unlock();
    15         }
    16     }
    17 }
    复制代码
    声明一把锁,其中ReentrantLock(可重入的互斥锁)是Lock接口的一个实现
    1 private final Lock queueLock=new ReentrantLock();
    然后在函数里面调用lock()方法声明同步代码块(临界区)
    1 queueLock.lock();
    最后在finally块中释放锁,重要!!!
    1 queueLock.unlock();

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

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

    修改锁的公平性

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

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

    锁条件可以和synchronized关键字声明的临界区的方法(wait(),notify(),notifyAll())做类比。锁条件通过Conditon接口声明。Condition提供了挂起线程和唤醒线程的机制。
    使用方法:
    复制代码
     1 private Condition lines;
     2     private Condition space;
     3      */
     4     public void insert(String line) {
     5         lock.lock();
     6         try {
     7             while (buffer.size() == maxSize) {
     8                 space.await();
     9             }
    10             buffer.offer(line);
    11             System.out.printf("%s: Inserted Line: %d
    ", Thread.currentThread()
    12                     .getName(), buffer.size());
    13             lines.signalAll();
    14         } catch (InterruptedException e) {
    15             e.printStackTrace();
    16         } finally {
    17             lock.unlock();
    18         }
    19     }
    20 public String get() {
    21         String line=null;
    22         lock.lock();        
    23         try {
    24             while ((buffer.size() == 0) &&(hasPendingLines())) {
    25                 lines.await();
    26             }
    27             
    28             if (hasPendingLines()) {
    29                 line = buffer.poll();
    30                 System.out.printf("%s: Line Readed: %d
    ",Thread.currentThread().getName(),buffer.size());
    31                 space.signalAll();
    32             }
    33         } catch (InterruptedException e) {
    34             e.printStackTrace();
    35         } finally {
    36             lock.unlock();
    37         }
    38         return line;
    39     }
  • 相关阅读:
    CSS清除浮动常用方法小结
    json格式的javascript对象用法分析
    jQuery on()方法绑定动态元素的点击事件无响应的解决办法
    一道思考题(二进制枚举的应用的想法)切金条
    对于excel中,无法把字符型转成数字型解决办法
    对于excel中,无法把字符型转成数字型解决办法
    python文本挖掘输出权重,词频等信息,画出3d权重图
    python文本挖掘输出权重,词频等信息,画出3d权重图
    python使用scikit-learn计算TF-IDF
    python使用scikit-learn计算TF-IDF
  • 原文地址:https://www.cnblogs.com/luckypo/p/7657098.html
Copyright © 2020-2023  润新知