• JAVA基础知识之多线程——线程同步


    线程安全问题

    多个线程同时访问同一资源的时候有可能会出现信息不一致的情况,这是线程安全问题,下面是一个例子,

    Account.class , 定义一个Account模型

     1 package threads.sync;
     2 
     3 
     4 public class Account {
     5     private String accountNo;
     6     private double balance;
     7     public Account() {}
     8     
     9     
    10     public Account(String accountNo, double balance) {
    11         this.accountNo = accountNo;
    12         this.balance = balance;
    13     }
    14 
    15     public String getAccountNo() {
    16         return accountNo;
    17     }
    18 
    19     public void setAccountNo(String accountNo) {
    20         this.accountNo = accountNo;
    21     }
    22 
    23     public double getBalance() {
    24         return balance;
    25     }
    26 
    27     public void setBalance(double balance) {
    28         this.balance = balance;
    29     }
    30     
    31     
    32     public int hashCode() {
    33         return accountNo.hashCode();
    34     }
    35     
    36     public boolean equals(Object obj) {
    37         if (this == obj) return true;
    38         if (obj != null && obj.getClass() == Account.class) {
    39             Account target = (Account)obj;
    40             return target.getAccountNo().equals(accountNo);
    41         }
    42         return false;
    43     }
    44 }

    DrawThread.class ,定义一个取钱类,用来操作Account

     1 package threads.sync;
     2 
     3 public class DrawThread extends Thread {
     4     private Account account;
     5     private double drawAmount;
     6     public DrawThread(String name, Account account, double drawAmount) {
     7         super(name);
     8         this.setAccount(account);
     9         this.setDrawAmount(drawAmount);
    10     }
    11     public Account getAccount() {
    12         return account;
    13     }
    14     public void setAccount(Account account) {
    15         this.account = account;
    16     }
    17     public double getDrawAmount() {
    18         return drawAmount;
    19     }
    20     public void setDrawAmount(double drawAmount) {
    21         this.drawAmount = drawAmount;
    22     }
    23     
    24     public void run() {
    25         if (account.getBalance() >=  drawAmount) {
    26             System.out.println(getName()+" draw money: "+drawAmount);
    27             try {
    28                 Thread.sleep(1);
    29             } catch (InterruptedException e) {
    30                 e.printStackTrace();
    31             }
    32             account.setBalance(account.getBalance() - drawAmount);
    33             System.out.println(getName()+" balance : "+account.getBalance());
    34         } else {
    35             System.out.println("failed for insufficient balance ");
    36         }
    37     }
    38 }

    DrawTest.class , 写一个测试类

    1 package threads.sync;
    2 
    3 public class DrawTest {
    4     public static void main(String[] args) {
    5         Account acc = new Account("123456",1000);
    6         new DrawThread("Thread-A",acc,800).start();
    7         new DrawThread("Thread-B",acc,800).start();
    8     }
    9 }

    执行结果,

    1 Thread-B draw money: 800.0
    2 Thread-A draw money: 800.0
    3 Thread-B balance : 200.0
    4 Thread-A balance : -600.0

    可见这里出现了逻辑错误,B线程取出800元后,账户里应该只剩下200元,但是接着A线程却也取出了800元,而且最终账户余额还成了负数,显然是不对的。

    造成上面错误的过程如下,当B线程执行到DreadThread类的第28行时,已经成功取出了800元,然后进入了sleep状态,没有继续下面的扣除余额的动作;

    此时JVM调度器将CPU切换到A线程执行,由于此时余额尚未扣除,A也能取出800元,之后A也进入sleep状态。

    接着B线程从sleep状态经历了1毫秒之后,进入了就绪状态,接着获取了CPU进入了运行状态,进行了后面的动作,余额变成了200元。

    最后A线程也醒来并获得继续运行机会,也做了一次扣款,结果余额变成了-600元(200-800)

    以上便是一个典型的线程安全问题。

    同步代码块

    解决上面线程安全问题的一种办法是同步代码块,使得一块代码同一时间只能在一个线程中执行,也就是常说的同步监视器原理。同步代码格式如下,

    synchronized(obj)

    {

    /*

    * 需要同步的代码块

    */

    }

    这表示JVM使用obj对象作为同步监视器(通常使用被并发访问的对象),线程执行这段代码之前,必须先获取对同步监视器的锁定。

    下面是一个用account对象作为同步监视器的例子,

    其他类用前面例子不变,唯一需要修改的是DrawThread.class

     1 package threads.sync;
     2 
     3 public class DrawThread extends Thread {
     4     private Account account;
     5     private double drawAmount;
     6     public DrawThread(String name, Account account, double drawAmount) {
     7         super(name);
     8         this.setAccount(account);
     9         this.setDrawAmount(drawAmount);
    10     }
    11     public Account getAccount() {
    12         return account;
    13     }
    14     public void setAccount(Account account) {
    15         this.account = account;
    16     }
    17     public double getDrawAmount() {
    18         return drawAmount;
    19     }
    20     public void setDrawAmount(double drawAmount) {
    21         this.drawAmount = drawAmount;
    22     }
    23     
    24     public void run() {
    25 
    26         synchronized(account) {
    27             if (account.getBalance() >=  drawAmount) {
    28                 System.out.println(getName()+" draw money: "+drawAmount);
    29                 try {
    30                     Thread.sleep(1);
    31                 } catch (InterruptedException e) {
    32                     e.printStackTrace();
    33                 }
    34                 account.setBalance(account.getBalance() - drawAmount);
    35                 System.out.println(getName()+" balance : "+account.getBalance());
    36             } else {
    37                 System.out.println("failed for insufficient balance ");
    38             }
    39         }
    40     }
    41 }

    可以看到只是在线程执行体中加了synchronized(account) { }来将一块代码锁定,保证同一时间这段代码只能被一个线程执行。

    执行结果,可以看到线程B去取款时已经没有足够余额了,所以失败,这与我们的设计初衷是相符的。

    1 Thread-A draw money: 800.0
    2 Thread-A balance : 200.0
    3 failed for insufficient balance 

    同步方法

    同步方法与同步代码块非常相似,只不过同步方法是将整个方法修饰为安全的线程访问方法,注意不能修饰static方法。

    同步方法的监视器是this,即调用该方法的对象。不需要显示地指定监视器。

    下面的是同步方法的例子,在Accont.class中,我们新加入一个同步方法draw,用来替代原来DrawThread中取款的线程执行体,

     1 package threads.sync;
     2 
     3 public class Account {
     4     private String accountNo;
     5     private double balance;
     6     public Account() {}
     7     
     8     
     9     public Account(String accountNo, double balance) {
    10         this.accountNo = accountNo;
    11         this.balance = balance;
    12     }
    13 
    14     public String getAccountNo() {
    15         return accountNo;
    16     }
    17 
    18     public void setAccountNo(String accountNo) {
    19         this.accountNo = accountNo;
    20     }
    21 
    22     public double getBalance() {
    23         return balance;
    24     }
    25 
    26     public void setBalance(double balance) {
    27         this.balance = balance;
    28     }
    29     
    30     
    31     public int hashCode() {
    32         return accountNo.hashCode();
    33     }
    34     
    35     public boolean equals(Object obj) {
    36         if (this == obj) return true;
    37         if (obj != null && obj.getClass() == Account.class) {
    38             Account target = (Account)obj;
    39             return target.getAccountNo().equals(accountNo);
    40         }
    41         return false;
    42     }
    43 
    44     public synchronized void draw(double drawAmount) {
    45         if ( balance >=  drawAmount) {
    46             System.out.println(Thread.currentThread().getName()+" draw money: "+drawAmount);
    47             try {
    48                 Thread.sleep(1);
    49             } catch (InterruptedException e) {
    50                 e.printStackTrace();
    51             }
    52             balance -= drawAmount;
    53             System.out.println(Thread.currentThread().getName()+" balance : "+balance);
    54         } else {
    55             System.out.println("failed for insufficient balance ");
    56         }
    57     }
    58 
    59 }

    修改DrawThread.class,我们直接在线程执行体中调用Account.class中的同步方法,调用同步方法的对象account将成为同步监视器被加锁,

     1 package threads.sync;
     2 
     3 public class DrawThread extends Thread {
     4     private Account account;
     5     private double drawAmount;
     6     public DrawThread(String name, Account account, double drawAmount) {
     7         super(name);
     8         this.setAccount(account);
     9         this.setDrawAmount(drawAmount);
    10     }
    11     public Account getAccount() {
    12         return account;
    13     }
    14     public void setAccount(Account account) {
    15         this.account = account;
    16     }
    17     public double getDrawAmount() {
    18         return drawAmount;
    19     }
    20     public void setDrawAmount(double drawAmount) {
    21         this.drawAmount = drawAmount;
    22     }
    23     
    24     public void run() {
    25                // account对象将作为同步监视器被加锁
    26         account.draw(drawAmount);
    27     }
    28 }

    执行结果,

    1 Thread-A draw money: 800.0
    2 Thread-A balance : 200.0
    3 failed for insufficient balance 

    释放同步监视器的锁定

    任何线程在进入同步代码块或同步方法之前,需要先获取同步监视器的锁定,最终会释放锁定(但不是显示地释放)。那么在什么情况下同步监视器锁定会被线程释放呢?

    • 当前线程的同步方法,同步代码块结束,当前线程释放同步监视器
    • 当前线程在同步方法,同步代码块中遇到break,return终止了执行的时候,当前线程会释放同步监视器
    • 当前线程在同步方法,同步代码块中出现了未处理的Error或Exception,导致无法继续执行下去,当前线程会释放同步监视器
    • 当前线程在执行同步方法,同步代码块时,程序执行了同步监视器对象的wait方法,则当前线程暂停,并释放同步监视器

    以下情况线程不会释放同步监视器,

    • 线程在执行同步方法,同步代码块时,程序调用sleep(), yield()方法来暂停当前线程时,当前线程不会释放同步监视器
    • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。

    同步锁

    同步锁可以显示地获取锁和释放锁,ReentrantLock是最常使用的同步锁。结合try .. finally {} 机制,可以确保同步锁在必要时得到释放。

    JAVA8中提供了一个StampedLock类,可为读写操作提供不同模式,例如Reading, Writing, ReadingOptimitic...

    下面是一个同步锁的例子,修改前面的Account.class,引入同步锁进行加锁和释放锁,其他类保持不变,

     1 package threads.sync;
     2 
     3 import java.util.concurrent.locks.ReentrantLock;
     4 
     5 public class Account {
     6     private final ReentrantLock lock = new ReentrantLock();
     7     private String accountNo;
     8     private double balance;
     9     public Account() {}
    10     
    11     
    12     public Account(String accountNo, double balance) {
    13         this.accountNo = accountNo;
    14         this.balance = balance;
    15     }
    16 
    17     public String getAccountNo() {
    18         return accountNo;
    19     }
    20 
    21     public void setAccountNo(String accountNo) {
    22         this.accountNo = accountNo;
    23     }
    24 
    25     public double getBalance() {
    26         return balance;
    27     }
    28 
    29     public void setBalance(double balance) {
    30         this.balance = balance;
    31     }
    32     
    33     
    34     public int hashCode() {
    35         return accountNo.hashCode();
    36     }
    37     
    38     public boolean equals(Object obj) {
    39         if (this == obj) return true;
    40         if (obj != null && obj.getClass() == Account.class) {
    41             Account target = (Account)obj;
    42             return target.getAccountNo().equals(accountNo);
    43         }
    44         return false;
    45     }
    46     
    47         public void draw(double drawAmount) {
    48         lock.lock();
    49         try {
    50             if ( balance >=  drawAmount) {
    51                 System.out.println(Thread.currentThread().getName()+" draw money: "+drawAmount);
    52                 try {
    53                     Thread.sleep(1);
    54                 } catch (InterruptedException e) {
    55                     e.printStackTrace();
    56                 }
    57                 balance -= drawAmount;
    58                 System.out.println(Thread.currentThread().getName()+" balance : "+balance);
    59             } else {
    60                 System.out.println("failed for insufficient balance ");
    61             }
    62         } finally {
    63             lock.unlock();
    64         }
    65     }
    66     
    67 }

    执行结果,在DrawThread.class中,通过调用account的draw方法,使用ReentrantLock的对象对取款操作进行同步锁操作,

    1 Thread-A draw money: 800.0
    2 Thread-A balance : 200.0
    3 failed for insufficient balance 

    死锁

    当两个线程互相等待对方释放同步监视器时就会发生死锁。

    死锁很容易发生,尤其在有多个同步监视器的时候,下面就是一个例子,

    A.class

     1 package threads.sync;
     2 
     3 public class A {
     4     public synchronized void foo(B b) {
     5         System.out.println(Thread.currentThread().getName()+": entered A.foo()");
     6         try {
     7             Thread.sleep(200);
     8         } catch (InterruptedException e) {
     9             e.printStackTrace();
    10         }
    11         System.out.println(Thread.currentThread().getName()+": trying to call B.last() ");
    12         b.last();
    13     }
    14     
    15     public synchronized void last() {
    16         System.out.println("A.last() executing");
    17     }
    18 }

    B.class

     1 package threads.sync;
     2 
     3 public class B {
     4     public synchronized void bar(A a) {
     5         System.out.println(Thread.currentThread().getName()+": entered B.bar()");
     6         try {
     7             Thread.sleep(200);
     8         } catch (InterruptedException e) {
     9             e.printStackTrace();
    10         }
    11         System.out.println(Thread.currentThread().getName()+": trying to call A.last() ");
    12         a.last();
    13     }
    14     public synchronized void last() {
    15         System.out.println("B.last() executing");
    16     }
    17 }

    A和B两个类中的方法都是同步方法,通过线程执行体调用的话会对调用对象加锁(将调用对象作为同步监视器),例如下面这样,

    DeadLock.class

     1 package threads.sync;
     2 
     3 public class DeadLock implements Runnable {
     4     A a = new A();
     5     B b = new B();
     6     public void init() {
     7         Thread.currentThread().setName("main Thread");
     8         a.foo(b);
     9         System.out.println("after entering main Thread");
    10     }
    11     
    12     @Override
    13     public void run() {
    14         Thread.currentThread().setName("sub Thread");
    15         b.bar(a);
    16         System.out.println("after entering sub Thread");
    17     }
    18     
    19     public static void main(String[] args) {
    20         DeadLock dl = new DeadLock();
    21         new Thread(dl).start();
    22         dl.init();
    23     }
    24 
    25 }

    执行结果,

    1 main Thread: entered A.foo()
    2 sub Thread: entered B.bar()
    3 sub Thread: trying to call A.last() 
    4 main Thread: trying to call B.last() 

    上面的执行结果到第4行的时候并没有结束,而是有两个线程处于阻塞状态,且这两个线程各自锁定一个同步监视器,同时又各自在请求对方的同步监视器,因此就陷入了死锁状态,具体过程如下,

    1. init()首先被执行(先后顺序随机),
    2. main 线程中调用a对象的foo方法,则main线程对a对象锁定,当main线程执行到foo方法中的第7行时,进入sleep状态(main线程不会释放同步监视器a),CPU切换到sub线程,
    3. sub线程中调用了b 对象的bar方法,于是sub 线程对b对象锁定,当sub线程执行到bar方法第7行时,也进入sleep状态(sub线程不会释放同步监视器b),
    4. main线程的由于先进入sleep所以会先醒来继续执行到foo方法第12行时,尝试调用b对象的同步方法last(),需要先锁定同步监视器b,
    5. 由于此时sub线程还处于sleep状态,并未释放同步监视器b,所以main线程将因此阻塞(依然不会释放同步监视器a),
    6. 当sub线程醒来之后,执行到bar方法第12行,尝试调用a对象的同步方法last(),需要先锁定同步监视器a,
    7. 由于此时main线程还处于阻塞状态并且锁定了同步监视器a,所以sub线程也会因此进入阻塞状态(依然不会释放同步监视器b),
    8. 至此,就形成了main线程持有同步监视器a,请求获取同步监视器b,而sub线程持有同步监视器b,请求获取同步监视器a的死锁局面

    对于线程同步,出于性能方面考虑,有如下原则,(参考自阿里巴巴Java开发手册)

    6. 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能

    锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

    7. 【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造

    成死锁。

    说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序

    也必须是A、B、C,否则可能出现死锁。

    8. 【强制】并发修改同一记录时,避免更新丢失,要么在应用层加锁,要么在缓存加锁,要么在

    数据库层使用乐观锁,使用version作为更新依据。

    说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次

    数不得小于3次。

  • 相关阅读:
    优化SQL Server数据库的经验总结
    #main和.main区别
    通过SQL把n条记录插入表中
    11月6日到8日参加微软Tech.ED2008大会
    NOIP2017 D2T1奶酪
    NOIP2017 D1T2时间复杂度
    NOIP2017 D2T2宝藏
    NOIP2017 D1T1小凯的疑惑
    NOIP2017 D1T3逛公园
    NOIP2017 D2T3列队
  • 原文地址:https://www.cnblogs.com/fysola/p/6071413.html
Copyright © 2020-2023  润新知