多线程(一)线程同步

时间:2022-11-10 21:09:28

 

一,线程的同步有以下方法

  1,使用synchronized实现同步方法;

  2,使用非依赖属性实现同步;

  3,在同步代码块中使用条件;

  4,使用锁实现同步;

  5,使用读写同步数据访问;

  6,修改锁的公平性;

  7,在锁中使用多条件;

  多个执行线程共享一个资源的情况,是最常见的并发编程情况之一。在并发应用中常常遇到这样的情景:多个线程读写相同的数据,或者访问相同的文件或数据库连接。为了防止这些共享资源可能出现的错误或者数据不一致,我们必须实现一些机制来防止这些错误的发生。

  为了解决这些问题,人们引入了临界区(Critical Section)概念。临界区是一个用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程执行。

  Java提供了同步机制,当一个线程试图访问一个临界区时,它将使用一种同步机制来查看是不是已经有其他线程进入了临界区。如果没有其他线程进入临界区,它就被同步机制挂起,直到进入的线程离开这个临界区。如果在等待进入临界区的线程不止一个,JVM会选择其中一个,其余的将继续等待。

二,使用synchronized实现同步方法

  每一个用synchronized关键字声明的方法都是临界区。在Java中,同一个对象的临界区,在同一时间只有一个允许被访问。

  静态方法则有不同的行为。用synchronized关键字声明的静态方法,同时只能够被一个执行线程访问,但是其他线程可以访问这个对象的非静态方法。必须非常谨慎这一点,因为两个线程可以同时访问一个对象的两个不同的synchronized方法,即其中一个是静态方法,另一个是非静态方法。如果两个方法都改变了相同的数据,将会出现数据不一致的错误。

案例:

多线程(一)线程同步多线程(一)线程同步
 1 //创建名为Account的账号类,它是银行账户模型,只有一个double类型的属性balance
2 public class Account {
3
4 private double balance;
5
6 // 实现getBalance(),setBalance()方法来写入和读取余额balance 的值。
7 public double getBalance()
8 {
9 return balance;
10 }
11
12 public void setBalance(double balance)
13 {
14 this.balance = balance;
15 }
16
17 /**
18 * 实现addAmount()方法。它会将传入的数量加入到余额balance中,
19 * 并且在同一时间只允许一个线程改变这个值,
20 * 所以我们使用synchronized关键字将这个方法标记成临界区。
21 * @param amount
22 */
23 public synchronized void addAmount(double amount)
24 {
25 double tmp = balance;
26 try {
27 TimeUnit.MILLISECONDS.sleep(10);
28 } catch (InterruptedException e) {
29 e.printStackTrace();
30 }
31 tmp += amount;
32 balance = tmp;
33 }
34 /**
35 * 实现subtranctAmount()方法。它会将传入的数量从余额中扣除,
36 * 并且在同一时间只允许一个线程改变这个值,
37 * 所以我们使用synchronized关键字将这个方法标记成临界区。
38 * @param amount
39 */
40 public synchronized void subtranctAmount(double amount)
41 {
42 double tmp = balance;
43 try {
44 TimeUnit.MILLISECONDS.sleep(10);
45 } catch (InterruptedException e) {
46 e.printStackTrace();
47 }
48 tmp -= amount;
49 balance = tmp;
50 }
51 }
View Code

 

实现一个ATM模拟类BANK。它使用subtractAmount()方法对账户余额进行扣除。这个类实现Runnable接口以作为线程执行。

1 public class Bank implements Runnable{
2 }

 

为这个类增加账户类Account对象,用构造器初始化这个对象。

1 private Account account;
2
3 public Bank(Account account) {
4 this.account = account;
5 }

实现run()方法。它将调用subtranctAmount()方法对账户余额进行扣除,并循环执行100次。

1 public void run()
2 {
3 for (int i = 0; i < 100; i++) {
4 account.subtranctAmount(100);
5 }
6
7 }
8

实现公司模拟类Company。它使用addAmount() 对账户的余额进行充值。这个类实现Runnable接口以作为线程运行。

 1 public class Company implements Runnable {} 

为Company类增加账户类Account 对象,用构造器初始化这个对象。

1 private Account account;
2
3 public Company(Account account) {
4 this.account = account;
5 }

实现run()方法。它将调用addAmount()方法对账户余额进行充值,并循环执行100次。

1 public void run()
2 {
3 for (int i = 0; i < 100; i++) {
4 account.addAmount(100);
5 }
6 }

范例主程序

 1 public static void main(String[] args) throws InterruptedException
2 {
3 //创建账户类初始值1000
4 Account account=new Account();
5 account.setBalance(1000);
6
7 //创建公司类
8 Company company=new Company(account);
9 Thread companyThread=new Thread(company);
10
11 //创建ATM模拟类
12 Bank bank=new Bank(account);
13 Thread bankThread=new Thread(bank);
14
15 //显示初始余额
16 System.out.println("Account: initial balance:"+account.getBalance());
17
18 //启动两个线程
19 companyThread.start();
20 bankThread.start();
21
22 //使用join()等待线程运行完成
23 companyThread.join();
24 bankThread.join();
25
26 //显示最终结果
27 System.out.println("Account: final balance:"+account.getBalance());
28 }

  在这个案例中通过使用tmp来临时存储账户余额,已经制造了一个错误的情景:这个临时变量先获取余额,然后进行数额累加,之后把最终结果更新为账户余额。此外,案例中还通过sleep()方法增加了延时,使得正在执行这个方法的线程休眠10ms,而此时其他线程也可能会执行这个方法,因此可能会改变余额,引发错误。而synchroniized关键字机制避免了这类错误的发生。

  我们先把addAmount()和subtractAmount()方法的synchroniized关键字去掉看下运行结果:

  多线程(一)线程同步

  如果多次运行这个程序,会有不同的结果。因为JVM并不保证线程执行的顺序。

  在把synchroniized关键字加上运行结果:

  多线程(一)线程同步

  synchroniized关键字的使用,保证了再并发程序中对共享数据的正确访问。

  一个对象的方法采用synchroniized关键字进行声明,只能被一个线程访问。如果线程A正在执行一个同步方法syncMethodA(),线程B要执行这个对象的其他同步方法syncMethodB(),线程B将被阻塞直到线程A访问完。但如果线程B访问的是这个类的不同对象,那么两个线程都不会被阻塞。

更多信息

   synchroniized关键字会降低系统的性能,因此只能在并发情景中需要修改共享数据的方法上使用它。

  可以使用递归调用被synchroniized关键字声明的方法。当线程访问一个对象的同步方法时,它还可以调用这个对象的其他同步方法,也包含正在执行的方法,而不必再次去获取这个方法的使用权。

  我们可以通过synchroniized关键字来保护代码块的访问。临界区的访问应尽可能的短。