转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/70670024
本文出自:【顾林海的博客】
前言
在编写多线程应用时,读写相同的数据,最有可能发生数据的错误或不一致,为了防止这些错误的发生,我们引入了临界区概念,临界区是一个用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程运行。为了更好的实现临界区,Java提供了同步机制,所谓的同步机制是指:当一个线程试图访问一个临界区时,它将使用一种同步机制来查看是不是已经有其他线程进入临界区,如果没有其他线程进入临界区,它就可以进入临界区,否则它就被挂起,直到进入的线程离开这个临界区。关于线程的基础知识可以查看《有关线程的相关知识(1)》和《有关线程的相关知识(2)》
synchronized的使用
在Java中使用synchronized关键字来控制一个方法的并发访问。如果一个对象使用synchronized关键字声明,说明只有一个执行线程被允许访问它。如果synchronized关键字声明的是静态方法,同时只能够被一个执行线程访问,但是其他线程可以访问这个对象的非静态方法,当出现这种情况时就要注意两个方法是否改变了相同的数据,从而导致数据不一致。
接下来我们编写一个银行存取款应用,不用synchronized同步存取款方法,在存取款方法中每次进行操作时休眠50毫秒,在休眠50毫秒的这个时间段内,其他线程可能会执行这个方法,最终的账户余额会改变,从而引发错误。
/**
* 用户账户类
*
* <pre>
* 含有新增余额和扣除余额操作
* </pre>
*
* @author gulinhai
*
*/
public class Account {
//账户余额
private double balance;
public double getBalance(){
return balance;
}
public void setBalance(double balance){
this.balance=balance;
}
/**
* 增加余额
* @param amount
*/
public void addAmount(double amount){
double tmp=balance;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp+=amount;
balance=tmp;
}
/**
* 扣除余额
* @param amount
*/
public void subtractAmount(double amount){
double tmp=balance;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp-=amount;
balance=tmp;
}
}
创建一个新增余额的线程,通过for循环100次,每次增加1000:
public class AddAmount implements Runnable{
private Account mAccount;
public AddAmount(Account account){
this.mAccount=account;
}
@Override
public void run() {
for(int i=0;i<100;i++){
mAccount.addAmount(1000);
}
}
}
创建一个减少余额的线程,通过for循环100次,每次减少1000:
public class SubtractAmount implements Runnable{
private Account mAccount;
public SubtractAmount(Account account){
this.mAccount=account;
}
@Override
public void run() {
for(int i=0;i<100;i++){
mAccount.subtractAmount(1000);
}
}
}
创建银行类:
public class Bank {
public static void main(String[] args) {
Account mAccount=new Account();
mAccount.setBalance(1000);
AddAmount addAmount=new AddAmount(mAccount);
Thread addThread=new Thread(addAmount);
SubtractAmount subtractAmount=new SubtractAmount(mAccount);
Thread subtractThread=new Thread(subtractAmount);
System.out.println("余额:"+mAccount.getBalance());
addThread.start();
subtractThread.start();
try {
subtractThread.join();
addThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("余额:"+mAccount.getBalance());
}
}
在主类中创建Account对象mAccount,并进行初始化1000,随后创建新增余额和减少余额线程,并执行这两个线程,最后输出余额,按照日常生活中,新增100次余额,每次1000,随后扣除100次,每次扣1000,最后的结果应该是初始化金额1000,但我们运行上面的程序。
余额:1000.0
余额:-5000.0
可以看到每次运行的结果大部分是不想同的,也就是多个线程同时操作了同一个数据,造成数据不一致,这里我们用synchronized关键字来同步新增和扣除方法,使得每次只有一个线程执行新增或扣除操作:
/**
* 增加余额
* @param amount
*/
public synchronized void addAmount(double amount){
double tmp=balance;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp+=amount;
balance=tmp;
}
/**
* 扣除余额
* @param amount
*/
public synchronized void subtractAmount(double amount){
double tmp=balance;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp-=amount;
balance=tmp;
}
运行的结果:
余额:1000.0
余额:1000.0
使用synchronized关键字,保证了在并发程序中对共享数据的正确访问。这里要注意了synchronized关键字会降低应用程序的性能,因此只能在并发场景中需要修改共享数据时使用,我们推荐这样使用synchronized:方法的其余部分保持在synchronized代码块之外,以获取更好的性能。这样使用就需要把对象引入作为传入参数。
接下来在编写一个程序,并在类中添加两个非依赖属性,用于多线程共享,同一时刻只允许一个线程访问一个属性变量,其他某个线程访问另一个变量。程序中创建两个数字,初始化各20,在两个线程中对这两个数字进行添加和减少操作。
public class Test {
private int number1;
private int number2;
private final Object numberObject1;
private final Object numberObject2;
public Test(){
numberObject1=new Object();
numberObject2=new Object();
number1=20;
number2=20;
}
public int getNumber1(){
return number1;
}
public int getNumber2(){
return number2;
}
public void substractNmbuer1(int number){
synchronized (numberObject1) {
if(number<number1){
number1-=number;
}
}
}
public void substractNmbuer2(int number){
synchronized (numberObject2) {
if(number<number2){
number2-=number;
}
}
}
public void addNumber1(int number){
synchronized (numberObject1) {
number1+=number;
}
}
public void addNumber2(int number){
synchronized (numberObject2) {
number1+=number;
}
}
}
public class Test1 implements Runnable{
private Test mTest;
public Test1(Test test){
this.mTest=test;
}
@Override
public void run() {
mTest.substractNmbuer1(2);
mTest.substractNmbuer1(5);
mTest.addNumber1(3);
mTest.substractNmbuer2(2);
mTest.substractNmbuer2(4);
mTest.substractNmbuer1(3);
}
}
public class Test2 implements Runnable{
private Test mTest;
public Test2(Test test){
this.mTest=test;
}
@Override
public void run() {
mTest.substractNmbuer2(2);
mTest.substractNmbuer2(5);
mTest.addNumber1(3);
mTest.substractNmbuer2(2);
mTest.substractNmbuer1(4);
mTest.addNumber2(6);
mTest.substractNmbuer1(3);
mTest.substractNmbuer1(3);
}
}
public class Client {
public static void main(String[] args) {
Test test=new Test();
Test1 test1=new Test1(test);
Thread thread1=new Thread(test1);
Test2 test2=new Test2(test);
Thread thread2=new Thread(test2);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("number1剩余:"+test.getNumber1()+"\nnumber2剩余:"+test.getNumber2());
}
}
运行程序:
number1剩余:12
number2剩余:5
在并发编程中有一个典型的问题就是生产者-消费者问题,比如我们有一个数据缓冲区,一个或者多个数据生产者,一个或者多个数据消费者将从数据缓冲区中取走数据,这个缓冲区是一个共享结构,因此需要使用同步机制,但这里有个问题,如果使用synchronized关键字,如果缓冲区满了,生产者就不能再放入数据,如果缓冲区空了,消费者也不能读取数据。
为了解决这个问题,Java在Object类中提供了wait()、notify()和notifyAll()方法。我们可以在同步代码块中调用wait()方法,这时JVM将这个线程置入休眠,并且释放控制这个同步代码块的对象,同时允许其他线程执行这个对象控制的其他同步代码块,这时在这个对象控制的某个同步代码块中调用notify()或者notifyAll()方法,来唤醒这个线程。
接下来实现一个生产者-消费者问题。
/**
* 创建数据存储类EventStorage,并保存一个最大值maxSize和数据集合
* LinkedList<Date>来保存存入的日期。
* @author gulinhai
*
*/
public class EventStorage {
private int maxSize;
private LinkedList<Date> storage;
public EventStorage(){
this.maxSize=10;
this.storage=new LinkedList<>();
}
public int size(){
return storage.size();
}
/**
* 同步方法set(),保存数据到存储列表storage。
* 首先检查列表是不是满了,如果满了,就调用wait()方法挂起线程并等待空余空间的出现,
* 最后调用notifyAll()方法唤醒所有因调用wait()方法进入休眠的线程。
*/
public synchronized void set(){
while(storage.size()==maxSize){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
notifyAll();
}
/**
* 同步方法set(),从存储列表storage中获取数据。
* 首先检查列表是不是空了,如果空了,就调用wait()方法挂起线程并等待列表中数据的出现,
* 最后调用notifyAll()方法唤醒所有因调用wait()方法进入休眠的线程。
*/
public synchronized void get(){
while(storage.size()==0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.poll();
notifyAll();
}
}
/**
* 生产者
* @author gulinhai
*
*/
public class Producer implements Runnable{
private EventStorage storage;
public Producer(EventStorage storage){
this.storage=storage;
}
@Override
public void run() {
for(int i=0;i<100;i++){
storage.set();
}
}
}
/**
* 消费者
* @author gulinhai
*
*/
public class Consumber implements Runnable{
private EventStorage storage;
public Consumber(EventStorage storage){
this.storage=storage;
}
@Override
public void run() {
for(int i=0;i<100;i++){
storage.get();
}
}
}
public class Client {
public static void main(String[] args) {
EventStorage storage=new EventStorage();
Producer producer=new Producer(storage);
Thread producerThread=new Thread(producer);
Consumber consumber=new Consumber(storage);
Thread consumberThread=new Thread(consumber);
producerThread.start();
consumberThread.start();
try {
producerThread.join();
consumberThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("缓冲区数据个数:"+storage.size());
}
}
输出:
缓冲区数据个数:0
注意:必须在while循环中调用wait(),并且不断查询while的条件。