线程同步基础之使用synchronized实现同步方法

Java的最基本的同步方式,即使用synchronized关键字来控制一个方法的并发访问。 每一个用synchronized关键字声明的方法都是临界区。在Java中,同一个对象的临界区,在同一时间只有一个允许被访问。

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

先来看第一个示例,在java中,同一个对象的临界区,在同一时间只有一个允许被访问(都是非静态的synchronized方法):

package concurrency;

public class Main8 {
    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);
        System.out.printf("Account : Initial Balance:  %f\n", account.getBalance());
        companyThread.start();
        bankThread.start();
        try {
            //join()方法等待这两个线程运行完成
            companyThread.join();
            bankThread.join();
            System.out.printf("Account : Final Balance:  %f\n", account.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/*帐户*/
class Account{
    private double balance;
    /*将传入的数据加到余额balance中*/
    public synchronized void addAmount(double amount){
        double tmp = balance;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp += amount;
        balance = tmp;
    }
    /*将传入的数据从余额balance中扣除*/
    public synchronized void subtractAmount(double amount){
        double tmp = balance;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp -= amount;
        balance = tmp;
    }
    public double getBalance() {
        return balance;
    }
    public void setBalance(double balance) {
        this.balance = balance;
    }
}

/*银行*/
class Bank implements Runnable{
    private Account account;
    public Bank(Account account){
        this.account = account;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            account.subtractAmount(1000);
        }
    }
}

/*公司*/
class Company implements Runnable{
    private Account account;
    public Company(Account account){
        this.account = account;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            account.addAmount(1000);
        }
    }
}

你已经开发了一个银行账户的模拟应用,它能够对余额进行充值和扣除。这个程序通过调用100次addAmount()方法对帐户进行充值,每次存入1000;然后通过调用100次subtractAmount()方法对帐户余额进行扣除,每次扣除1000;我们期望帐户的最终余额与起初余额相等,我们通过synchronized关键字实现了。

如果想查看共享数据的并发访问问题,只需要将addAmount()和subtractAmount()方法声明中的synchronized关键字删除即可。在没有synchronized关键字的情况下,打印出来的余额值并不一致。如果多次运行这个程序,你将获取不同的结果。因为JVM并不保证线程的执行顺序,所以每次运行的时候,线程将以不同的顺序读取并且修改帐户的余额,造成最终结果也是不一样的。

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

示例2,演示同一个对象上的静态synchronized方法与非静态synchronized方法可以在同一时间点被多个线程访问的问题,验证一下。

package concurrency;

public class Main8 {
    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);
        System.out.printf("Account : Initial Balance:  %f\n", account.getBalance());
        companyThread.start();
        bankThread.start();
        try {
            //join()方法等待这两个线程运行完成
            companyThread.join();
            bankThread.join();
            System.out.printf("Account : Final Balance:  %f\n", account.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/*帐户*/
class Account{
    /*这里改为静态变量*/
    private static double balance = 0;
    /*将传入的数据加到余额balance中,注意是用static修饰过的*/
    public static synchronized void addAmount(double amount){
        double tmp = balance;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp += amount;
        balance = tmp;
    }
    /*将传入的数据从余额balance中扣除*/
    public synchronized void subtractAmount(double amount){
        double tmp = balance;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tmp -= amount;
        balance = tmp;
    }
    public double getBalance() {
        return balance;
    }
    public void setBalance(double balance) {
        this.balance = balance;
    }
}

/*银行*/
class Bank implements Runnable{
    private Account account;
    public Bank(Account account){
        this.account = account;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            account.subtractAmount(1000);
        }
    }
}

/*公司*/
class Company implements Runnable{
    private Account account;
    public Company(Account account){
        this.account = account;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            account.addAmount(1000);
        }
    }
}

我只是把上个例子中的,balance加了static关键字修改,addAmount()方法也可以static关键字修饰。执行结果大家可以自己测试一下,每次执行都是不一样的结果!

synchronized关键字会降低应用程序的性能,因此只能在并发情景中需要修改共享数据的方法上使用它。如果多个线程访问同一个synchronized方法,则只有一个线程可以访问,其他线程将等待。如果方法声明没有使用synchronized关键字,所有的线程都能在同一时间执行这个方法,因而总运行时间降低。如果已知一个方法不会被一个以上线程调用,则无需使用synchronized关键字声明之。

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

我们可以通过synchronized关键字来保护代码块(而不是整个方法)的访问。应该这样利用synchronized关键字:方法的其余部分保持在synchronized代码块之外,以获取更好的性能。临界区(即同一时间只能被一个线程访问的代码块)的访问应该尽可能的短。例如在获取一幢楼人数的操作中,我们只使用synchronized关键字来保护对人数更新的指令,并让其他操作不使用共享数据。当这样使用synchronized关键字时,必须把对象引用作为传入参数。同一时间只有一个线程被允许访问这个synchronized代码。通常来说,我们使用this关键字来引用正在执行的方法所属的对象:

synchronized(this){
    //Java code
}
时间: 2024-10-10 08:39:55

线程同步基础之使用synchronized实现同步方法的相关文章

第二章线程同步基础

Java 7 并发编程实战手册目录 代码下载(https://github.com/Wang-Jun-Chao/java-concurrency) 第二章线程同步基础 2.1简介 多个执行线程共享一个资源的情景,是最常见的并发编程情景之一.在并发应用中常常遇到这样的情景:多个线程读或者写相同的数据,或者访问相同的文件或数据库连接. 为了防止这些共享资源可能出现的错误或数据不一致,我们必须实现一些机制来防止这些错误的发生. 为了解决这些问题,引入了临界区(Critical Section)概念,临

C# 多线程(二) 线程同步基础

本系列的第一篇简单介绍了线程的概念以及对线程的一些简单的操作,从这一篇开始讲解线程同步,线程同步是多线程技术的难点.线程同步基础由以下几个部分内容组成 1.同步要领(Synchronization Essentials) 2.锁(Locking) 3.线程安全(Thread Safety) 4.事件等待句柄(Signaling with Event Wait Handles) 5.同步上下文(Synchronization Contexts) 同步要领(Synchronization Essen

C# 多线程(二) 线程同步基础(上)

本系列的第一篇简单介绍了线程的概念以及对线程的一些简单的操作,从这一篇开始讲解线程同步,线程同步是多线程技术的难点.线程同步基础由以下几个部分内容组成 1.同步要领(Synchronization Essentials) 2.锁(Locking) 3.线程安全(Thread Safety) 4.事件等待句柄(Signaling with Event Wait Handles) 5.同步上下文(Synchronization Contexts) 同步要领(Synchronization Essen

【java7并发编程实战】—–线程同步基础:synchronized

在我们的实际应用当中可能经常会遇到这样一个场景:多个线程读或者.写相同的数据,访问相同的文件等等.对于这种情况如果我们不加以控制,是非常容易导致错误的.在java中,为了解决这个问题,引入临界区概念.所谓临界区是指一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问. 在java中为了实现临界区提供了同步机制.当一个线程试图访问一个临界区时,他将使用一种同步机制来查看是不是已经有其他线程进入临界区.如果没有则他就可以进入临界区,否则他就会被同步机制挂起,指定进入的线程离开这个临界区

(二)线程同步基础

1.使用 synchronized实现同步方法 如果一个对象已经用synchronized声明,那么只允许一个执行线程访问它,如果其他线程试图访问这个对象的其他方法,它将被挂起,直到第一个线程执行完正在运行的方法.被synchronized声明的方法就是临界区. 对于非静态的方法被synchronized修饰后,同一时间内只能有一个线程访问这个对象的synchronized方法.即,每一个synchronized方法都控制着类成员变量的访问,要想访问被synchronized声明的方法,必须先获

.net中的线程同步基础(搬运自CLR via C#)

线程安全 此类型的所有公共静态(Visual Basic 中为 Shared)成员对多线程操作而言都是安全的.但不保证任何实例成员是线程安全的. 在MSDN上经常会看到这样一句话.表示如果程序中有n个线程调用这个方法,那么这n个线程都是安全的, 但是实例成员就不能保证了. 比如Math.Max方法,不管有多少个线程调用,都不会出现线程不安全的情况. 列举一个由于多线程引起的数据不安全. static void Main(string[] args) { Stopwatch watch = new

C#中的线程(二) 线程同步基础

1.同步要领 下面的表格列展了.NET对协调或同步线程动作的可用的工具:                       简易阻止方法 构成 目的 Sleep 阻止给定的时间周期 Join 等待另一个线程完成                       锁系统 构成 目的 跨进程? 速度 lock 确保只有一个线程访问某个资源或某段代码. 否 快 Mutex 确保只有一个线程访问某个资源或某段代码.可被用于防止一个程序的多个实例同时运行. 是 中等 Semaphore 确保不超过指定数目的线程访问某

线程同步基础之简介

多个执行线程共享一个资源的情景,是最常见的并发编程情景之一.在并发应用中常常遇到这样的情景:多个线程读或者写相同的数据,或者访问相同的文件或数据库连接.为了防止这些共享资源可能出现的错误或数据不一致,我们必须实现一些机制来防止这些错误的发生. 为了解决这些问题,人们引入了临界区(Critical Section)概念.临界区是一个用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程执行. 为了帮助编程人员实现这个临界区,Java提供了同步机制.当一个线程试图访问一个临界区时,它将使用一

线程同步基础之使用非依赖属性实现同步

当使用synchronized关键字来保护代码块时,必须把对象引用作为传入参数.通常情况下,使用this关键字来引用执行方法所属的对象,也可以使用其他的对象对其进行引用.一般来说,这些对象就是为这个目的而创建的.例如,在类中有两个非依赖属性,它们被多个线程共享,你必须同步每一个变量的访问,但是同一时刻只允许一个线程访问一个属性变量,其他某个线程访问另一个属性变量. 这里我们演示电影院售票场景.这个范例模拟了有两个屏幕和两个售票处的电影院.一个集票处卖出的一张票,只能用于其中一个屏幕,不能同时用于