Java中的多线程同步:
讨论synchronized之前先看简单看一些java中的多线程同步。
当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。比如,有个初始银行账户Account、其初始值为0,有两个小伙伴A、B去银行的两个窗口同时往Account这个账户存钱100¥,这个时候需要先获取账户原来的值,然后在原始值的基础上加上当前要存入的金额,再写回记账本。A和B拿到的初始值都是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关键字上。
参考博客:Java中Synchronized的用法