zoukankan      html  css  js  c++  java
  • 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     }
    复制代码
  • 相关阅读:
    LeetCode OJ 112. Path Sum
    LeetCode OJ 226. Invert Binary Tree
    LeetCode OJ 100. Same Tree
    LeetCode OJ 104. Maximum Depth of Binary Tree
    LeetCode OJ 111. Minimum Depth of Binary Tree
    LeetCode OJ 110. Balanced Binary Tree
    apache-jmeter-3.1的简单压力测试使用方法(下载和安装)
    JMeter入门教程
    CentOS6(CentOS7)设置静态IP 并且 能够上网
    分享好文:分享我在阿里8年,是如何一步一步走向架构师的
  • 原文地址:https://www.cnblogs.com/firstdream/p/7797946.html
Copyright © 2011-2022 走看看