Java中的synchronized关键字

来源:互联网 发布:sqlserver 认证 编辑:程序博客网 时间:2024/06/06 02:52

Java中的多线程同步:

讨论synchronized之前先看简单看一些java中的多线程同步。

当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。比如,有个初始银行账户Account、其初始值为0,有两个小伙伴AB去银行的两个窗口同时往Account这个账户存钱100¥,这个时候需要先获取账户原来的值,然后在原始值的基础上加上当前要存入的金额,再写回记账本。AB拿到的初始值都是0,再加上要存入的钱后总额为100元,再写回记账本,因此,最终记账本的显示的账户金额是100,这和实际情况就有出入了。

没有进行线程同步的实例代码:

public class Test {
   
private static int value = 0;
    public static int
getValue() {
       
return value;
   
}

   
public void add(){
       
int tmp = value;
        try
{
            Thread.sleep(
10);
       
} catch (InterruptedException e) {
            e.printStackTrace()
;
       
}
       
value = tmp + 100;
   
}
}

 

public class ThreadMain implements Runnable {    private int id;    private Test test;    public ThreadMain(int id, Test test) {        this.id = id;        this.test = test;    }    public void run() {        test.add();//d对数据的操作没有进行线程同步,不安全,最终结果不可预测        System.out.println("线程" + this.id + "完成 add操作");    }    public static void main(String[] args) {        Thread[] threads = new Thread[5];        Test test = new Test();        for (int i = 0; i < 5; i++) {            Thread thread = new Thread(new ThreadMain(i, test));            threads[i] = thread;            thread.start();        }        for(int i = 0; i < 5; i++){            Thread thread = threads[i];            try {                thread.join();            }catch (Exception e){            }        }        System.out.print("value= " + Test.getValue());    }}

运行结果:

线程1完成 add操作

线程4完成 add操作

线程3完成 add操作

线程0完成 add操作

线程2完成 add操作

value= 100

 

可以看到,在没有进行线程同步的情况下,多个线程会同时操作同一个数据、对象,导致结果不可预知。

我们需要的逻辑应该是这样的:当A在进行获取账户初始、进行账户增加、最终写回记账本这个存钱过程中,B不能同时进行操作,B等待A完成存钱动作后,B可以开始存钱的动作;因此需要一种机制,确保在任何时候,只有一个人在进行存钱的动作。在java中,使用synchronized关键字是实现该功能的一种方法(包括volatile关键字、Lock方式也可以实现)

Synchronized关键字:

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种: 
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象; 
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象; 
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象; 
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

我们使用同步代码块的方式,来看看上述存钱这件事用线程同步的方式应该怎么做:

public class Test {    private static int value = 0;    public static int getValue() {        return value;    }    public  void addWithSyn(){        synchronized(this) {            int tmp = value;            try {                Thread.sleep(10);            } catch (InterruptedException e) {                e.printStackTrace();            }            value = tmp + 100;        }    }}
public class ThreadMain implements Runnable {    private int id;    private Test test;    public ThreadMain(int id, Test test) {        this.id = id;        this.test = test;    }    public void run() {        test.addWithSyn();//        System.out.println("线程" + this.id + "完成 add操作");    }    public static void main(String[] args) {        Thread[] threads = new Thread[5];        Test test = new Test();        for (int i = 0; i < 5; i++) {            Thread thread = new Thread(new ThreadMain(i, test));            threads[i] = thread;            thread.start();        }        for(int i = 0; i < 5; i++){            Thread thread = threads[i];            try {                thread.join();            }catch (Exception e){            }        }        System.out.print("value= " + Test.getValue());    }}

运行结果:

线程1完成 add操作

线程0完成 add操作

线程4完成 add操作

线程3完成 add操作

线程2完成 add操作

value= 500

 

当然,也可以对方法同步、对类对象同步,由于某个线程进入同步的代码域后,其它线程要想进入同步的代码域,就要进入阻塞状态,会降低程序的性能。因此,应根据需要进行线程同步。

被synchronized修饰的代码称作临界区,当一个线程a进入该对象的临界区A时,其它试图进入临界区A或者该对象的其它临界区时,将会进入阻塞状态,直至线程a访问完临界区,其它线程才有机会进入临界区。

在使用临界区保护代码块时,必须将对象的引用作为传入参数,通常使用this来引用执行方法所属的这个对象,如上述代码所示。但是,当对象中有两个非依赖属性时,

我们需要同步每一个变量的访问,即同一时刻只允许一个线程访问属性A,但是其他线程仍然可以访问属性B。

举个例子:

有一个对象Bank,管理用户两张银行卡账户A和B,同一个时刻,对A账户只能执行存钱或者取钱中的一种行为,但是

对A账户操作的同时,还必须允许能够同时操作B:

银行账户类Bank:

public class Bank {
   
private double accountA;
    private final
Object accountControlA;
    private double
accountB;
    private final
Object accountControlB;

    public
Bank(double accountA, double accountB) {
       
this.accountA = accountA;
        this
.accountB = accountB;
       
accountControlA = new Object();
       
accountControlB = new Object();
   
}

   
public void addAccountA(double money) {
       
synchronized (accountControlA) {
           
double tmp = accountA;
            try
{
                Thread.sleep(
10);
           
} catch (InterruptedException e) {
                e.printStackTrace()
;
           
}
           
accountA = tmp + money;
       
}
    }

   
public void addAccountB(double money) {
       
synchronized (accountControlB) {
           
double tmp = accountB;
            try
{
                Thread.sleep(
10);
           
} catch (InterruptedException e) {
                e.printStackTrace()
;
           
}
           
accountB = tmp + money;
       
}
    }

   
public double getAccountA() {
       
return accountA;
   
}

   
public double getAccountB() {
       
return accountB;
   
}

}

两个线程类,分别对账户accountA和accountB进行操作:

public class ThreadObjA implements Runnable {
   
private int id;
    private
Bank bank;

    public
ThreadObjA(int id, Bank bank) {
       
this.id = id;
        this
.bank = bank;
   
}

   
public void run() {
       
bank.addAccountA(100.0);//
       
System.out.println("线程ThreadObjA" + this.id + "完成 add操作");
   
}
}

public class ThreadObjB implements Runnable {
   
private int id;
    private
Bank bank;

    public
ThreadObjB(int id, Bank bank) {
       
this.id = id;
        this
.bank = bank;
   
}

   
public void run() {
       
bank.addAccountB(100.0);//
       
System.out.println("线程ThreadObjB" + this.id + "完成 add操作");
   
}
}

各创建五个线程来对这两个账户进行操作:

public static void main(String[]args){
    Bank bank =
new Bank(0.0,100.0);
   
Thread[] threads = new Thread[10];
    for
(int i = 0; i < 5; i++) {
        Thread thread =
new Thread(new ThreadObjA(i, bank));
       
threads[i] = thread;
       
thread.start();
   
}
   
for (int i = 0; i < 5; i++) {
        Thread thread =
new Thread(new ThreadObjB(i, bank));
       
threads[i+5] = thread;
       
thread.start();
   
}
   
for(int i = 0; i < 10; i++){
        Thread thread = threads[i]
;
        try
{
            thread.join()
;
       
}catch (Exception e){

        }
    }
    System.
out.print("bank accountA=" + bank.getAccountA() + "\n");
   
System.out.print("bank accountB=" + bank.getAccountB() + "\n");
}

由于synchronized传入的对象不同,JVM保证同一时间只有一个线程能够访问这个对象的代码保护块(临界区),因此ThreadObjA类的线程和ThreadObjB类的线程是可以同时运行访问各自的代码块的。但是,同一个时刻ThreadObjA类的多个线程线程只允许有一个访问对象Bank中accountControlA对象的代码保护块。

Java中多线程同步的其他几种方式:

使用特殊域变量(Volatile)实现线程同步。

使用JDK 5中提供的java.util.concurrent.lock包中的Lock对象。

这两种方式都一定的限制,其中Lock对象需要使用者手动释放锁。官方推荐使用synchronized来实现多线程同步,因此本人学习重点还是放在synchronized关键字上。

参考博客:JavaSynchronized的用法

 

2 0
原创粉丝点击