java中synchronized关键字的使用

写在前面:在做徐boss布置的并行计算作业中,遇到了一些坑,同时也有几个同学问我类似的问题,于是想写篇东西记一下。

一、Synchronized关键字的用法

synchronized关键字有多种用法,这里介绍用于同步方法以及同步代码块。

1.同步方法

       synchronized关键字可用于方法的声明中,用于同步某个方法,加了synchronized关键字的方法相当于一个临界区,同一个对象同一个synchronized方法同一时间只允许一个线程访问。

public synchronized void method(){}

        由于徐boss说我们是唯一一届有并行计算课程的(具体忘了,反正就是那个么意思)那我就用徐boss布置的作业作为例子使用一下啦,不用担心被学弟学妹们看到我的辣鸡代码。思考了一下,徐boss作业好像不能用这种方式实现。。算了强行实现一波吧
      下面用一个例子来演示synchronized同步方法的使用:

/**
 * Created by Charlie Mu on 2018/5/16.
 */
public class AnotherSaleTicket {
    public static void main(String[] args)
    {
        int flag=0;
        for(int j=0;j<100;j++)//为什么加循环?因为由于是随机的买票,那么如何验证程序是否正确呢 那就多运行几次看看会不会出错咯
        {
            Ticket ticket=new Ticket();
            AnotherBoxOffice windows1=new AnotherBoxOffice(1,ticket);
            AnotherBoxOffice windows2=new AnotherBoxOffice(2,ticket);
            AnotherBoxOffice windows3=new AnotherBoxOffice(3,ticket);
            AnotherBoxOffice windows4=new AnotherBoxOffice(4,ticket);
            windows1.start();
            windows2.start();
            windows3.start();
            windows4.start();
            try
            {
                windows1.join();
                windows2.join();
                windows3.join();
                windows4.join();
            }
            catch (InterruptedException e)
            {
                System.out.println("Interrupt...");
            }
            if(ticket.counter==30)
            {
                flag=1;
            }
            else
            {
                flag=0;
                break;
            }
        }
        if(flag==1)
        {
            System.out.println("Perfect!");
        }
        else
        {
            System.out.println("There is something wrong...");
        }
    }
}
class Ticket
{
    private int []ticket;
    public int counter=0;//用于测试的一个统计值
    public Ticket()
    {
        ticket=new int[30];
        for(int i=0;i<30;i++)//0代表票还没卖出去
        {
            ticket[i]=0;
        }
    }
    public synchronized void saleTicket(int i,int threadNumber)//同步saleTicket方法
    {
        if(ticket[i]==0)
        {
            ticket[i]=threadNumber;
            //System.out.println("Window "+threadNumber+" Sale the ticket :"+i+". Thread: "+Thread.currentThread().getName());//用于输出卖票情况
            counter++;//统计值加一
        }
    }
    public boolean saleout()//判断票是否已经卖完
    {
        for(int i=0;i<30;i++)
        {
            if(ticket[i]==0)
            {
                return false;
            }
        }
        return true;
    }
}
class AnotherBoxOffice extends Thread//窗口线程
{
    private int num;
    private Ticket ticket;
    public AnotherBoxOffice(int num,Ticket ticket)
    {
        this.num=num;
        this.ticket=ticket;
    }
    public void run()
    {
        while(true)
        {
            int currticket=(int)(Math.random()*10000)%30;//顾客随机买票
            ticket.saleTicket(currticket,num);
            Thread.yield();
            if(ticket.saleout())
            {
                break;
            }
        }
    }
}

这个程序实现多窗口卖票,顾客随机买票,为了验证这样设计的合理性,通过一个循环,不断运行程序,判断是否出现一票多卖的情况,按理说是不会出现的,因为,我直接把saleTicket方法同步了,也就意味着,每次只有一个线程能够调用saleTicket方法,似乎也就是同一个时刻只能有一个窗口在卖票?好像不太合理,所以我说这个问题不太适合用这种实现方式,我只是强行实现一下。

2.同步代码块

        同步代码块,也就是让一段代码成为临界区而不是整个方法。对于同一个Object(对象)代码块只能被一个线程访问。
synchronized(Object){
//Java code
}

        同样是徐boss作业的例子....
        下面给是一个同步代码块的例子。程序中可能存在一些冗余的语句或者变量,解决的还是上面那个问题,而且解决方式也并不是太好,但实在是不想再改了。
import java.util.Objects;

/**
 * Created by Charlie Mu on 2018/5/7.
 */

public class SaleTicket {
    public static final int TICKETNUM=100;
    public static Integer[] ticket;
    public static int counter=0;
    public static void main(String[] args)
    {
        ticket=new Integer[TICKETNUM];
        int flag=0;
        int j;
        for(j=0;j<10;j++)//用counter记录每次run方法中输出的总条数,刚好等于三十条则说明没有一票多卖的情况,因为每次运行结果随机,多循环几次来验证程序是否争取。
        {
            counter=0;
            for(int i=0;i<TICKETNUM;i++)
            {
                ticket[i]=0;
            }
            Object [] ob=new Object[TICKETNUM];
            for(int i=0;i<TICKETNUM;i++)//初始化object 之前因为没有初始化导致报错 NonePointer
            {
                ob[i]=new Object();
            }
            //System.out.println("Enter main Thread...");
            BoxOffice b1=new BoxOffice(1,ob);
            BoxOffice b2=new BoxOffice(2,ob);
            BoxOffice b3=new BoxOffice(3,ob);
            BoxOffice b4=new BoxOffice(4,ob);
            b1.start();
            b2.start();
            b3.start();
            b4.start();
            try
            {
                b1.join();
                b2.join();
                b3.join();
                b4.join();
            }
            catch (InterruptedException e)
            {
                System.out.println("Interrupt...");
            }
            //System.out.println("Counter :"+counter);
            if(counter>TICKETNUM)
            {
                flag=1;
                break;
            }
            System.out.println();
        }
        if(flag==1)
        {
            System.out.println("Find Error!!!");
            System.out.println("do while time : "+(j+1));
        }
        else
        {
            System.out.println("Perfect!!!");
        }

    }
}

class BoxOffice extends Thread
{
    private int window;
    private Object ticket[];
    BoxOffice(int window, Object [] ticket)
    {
        this.window=window;
        this.ticket=ticket;
    }
    public void start()
    {
        //System.out.println("Start Thread  "+window);
       super.start();
    }
    public void run()
    {
        //for(int currticket=0;currticket<30;currticket++)
        while(true)
        {
            int currticket=(int)(Math.random()*10000)%SaleTicket.TICKETNUM;
            synchronized (ticket[currticket])
            {

                if(SaleTicket.ticket[currticket]==0)
                {
                    System.out.print(System.currentTimeMillis()+"The windows(Threads) "+window+"write:"+currticket+" Thread: "+this.getName()+" ");
                    long saletime=System.currentTimeMillis();
                    System.out.println(saletime);
                    SaleTicket.ticket[currticket]=window;
                    SaleTicket.counter++;

                }
                //ticket[currticket].notify();
            }
            try{
                Thread.sleep(1);
            Thread.yield();
            }
            catch(Exception e)
            {
                ;
            }
            if(allsaled())
            {
                break;
            }
        }
    }
    boolean allsaled()//判断票是否卖完
    {
    for(int i=0;i<SaleTicket.TICKETNUM;i++)
        {
            if(SaleTicket.ticket[i]==0)
            {
                return false;
            }
        }
        return true;
    }
}

二、synchronized同步的是对象

        说了一大堆,那么我遇到了什么坑呢?这还得从徐boss的另外一个题目说起。题目是两个人共用一个账户,一个人在ATM取钱,一个人在柜台取钱。一开始以为,那我直接把存款定义成Java封装好的类型Integer,到时候直接synchronized它不就好了嘛。。。嗯,事实证明,我想的太简单了。
        首先,先大概说一下synchronized关键字,有一点非常重要的是,要理解synchronized关键字同步是对象,而不是代码。举个例子,还是之前那个问题,按照第二个解决方案的思路,能不能在run方法里面synchronized(this)
            synchronized (this)
            {

                if(SaleTicket.ticket[currticket]==0)
                {
                    System.out.print(System.currentTimeMillis()+"The windows(Threads) "+window+"write:"+currticket+" Thread: "+this.getName()+" ");
                    long saletime=System.currentTimeMillis();
                    System.out.println(saletime);
                    SaleTicket.ticket[currticket]=window;
                    SaleTicket.counter++;

                }
                //ticket[currticket].notify();
            }

以保证每次只有一个线程能够访问这段代码?答案显然是不能。因为虽然这里的this确实指的是当前线程对象的引用,但是synchronized同步的是对象,最终是防止多个线程调用同一个对象的方法或者代码段,可是每个线程实际上都是一个对象。或许我表达的不是特别好。
        那我是怎么意识到这些问题的呢,说回徐boss的题目,我就按照一开始的思路实现了一遍。噢,我扩展了一下,一线程个存,一个线程取,随机存取十次。

/**
 * Created by Charlie Mu on 2018/5/13.
 */
public class Bank {
    public static Integer money=new Integer(1000);
    public static Object object=new Object();
    public static void main(String [] args)
    {
        Account Zhangsan=new Account(200,"Zhangsan");
        Register Lisi=new Register(200,"Lisi");
        Zhangsan.start();
        Lisi.start();
    }
}

class Account implements Runnable
{
    private Thread t;
    private String ThreadName;
    private int depositeMoney;
    public Account(int money,String name)
    {
        depositeMoney=money;
        ThreadName=name;
    }

   public void start()
   {
       System.out.println(ThreadName+"Start!");
       if(t==null)
       {
           t=new Thread(this,ThreadName);
       }
       t.start();
   }
   public void run() {
       for(int i=0;i<10;i++)
       {
           synchronized(Bank.money)//更改Integer对象的值 实际上是创建一个新的Integer对象所以不能直接synchronized Bank.money
           {

               depositeMoney=(int)(Math.random()*1000)%500;
               System.out.println("Current left money : "+Bank.money+" "+ThreadName+" deposite "+depositeMoney);
               Bank.money = Bank.money - depositeMoney;
               System.out.println("After "+ThreadName+" left : "+Bank.money);
           }
           Thread.yield();
       }
   }
}

class Register implements Runnable
{
    private Thread t;
    private String ThreadName;
    private int registeMoney;
    public Register(int money,String name)
    {
        registeMoney=money;
        ThreadName=name;
    }

    public void start()
    {
        System.out.println(ThreadName+"Start!");
        if(t==null)
        {
            t=new Thread(this,ThreadName);
        }
        t.start();
    }
    public void run() {
        for(int i=0;i<10;i++)
        {
            synchronized(Bank.object)
            {
                registeMoney=(int)(Math.random()*1000)%500;
                System.out.println("Current left money : "+Bank.money+" "+ThreadName+" registe "+registeMoney);
                Bank.money = Bank.money + registeMoney;
                System.out.println("After "+ThreadName+" left : "+Bank.money);
            }
            Thread.yield();
        }

    }
}

运行结果:

ZhangsanStart!
LisiStart!
Current left money : 1000 Lisi registe 455
Current left money : 1000 Zhangsan deposite 59
After Lisi left : 1455
After Zhangsan left : 1396
Current left money : 1396 Lisi registe 60
Current left money : 1396 Zhangsan deposite 153
After Lisi left : 1456
After Zhangsan left : 1303
Current left money : 1303 Lisi registe 453
Current left money : 1303 Zhangsan deposite 239
After Lisi left : 1756
After Zhangsan left : 1517
Current left money : 1517 Lisi registe 43
After Lisi left : 1560
Current left money : 1560 Zhangsan deposite 97
After Zhangsan left : 1463
Current left money : 1463 Lisi registe 433
After Lisi left : 1896
Current left money : 1896 Zhangsan deposite 223
Current left money : 1896 Lisi registe 271
After Zhangsan left : 1673
After Lisi left : 1944
Current left money : 1944 Zhangsan deposite 128
After Zhangsan left : 1816
Current left money : 1816 Lisi registe 0
After Lisi left : 1816
Current left money : 1816 Zhangsan deposite 497
Current left money : 1816 Lisi registe 183
After Zhangsan left : 1319
After Lisi left : 1502
Current left money : 1502 Zhangsan deposite 394
Current left money : 1502 Lisi registe 456
After Zhangsan left : 1108
After Lisi left : 1564
Current left money : 1564 Zhangsan deposite 99
Current left money : 1564 Lisi registe 458
After Zhangsan left : 1465
After Lisi left : 1923
Current left money : 1923 Zhangsan deposite 11
After Zhangsan left : 1912

        发现问题了吗?嗯,一开始两行输出就有问题了,如果它真的同步了那一段代码块的话,输出的结果应该是像是串行化工作的执行结果才对,为什么会出现两个人一开始看见的余额一样呢?当然,我一开始是用一个很简单的例子发现这个问题的,这里就不重现那个简单的例子的。
        把代码中的synchronized(Bank.money)改成synchronized(Bank.object)就能得到正确的结果,正确结果应该形式是这样的(请忽略存款存在负值):


ZhangsanStart!
LisiStart!
Current left money : 1000 Zhangsan deposite 195
After Zhangsan left : 805
Current left money : 805 Lisi registe 484
After Lisi left : 1289
Current left money : 1289 Lisi registe 486
After Lisi left : 1775
Current left money : 1775 Zhangsan deposite 452
After Zhangsan left : 1323
Current left money : 1323 Lisi registe 95
After Lisi left : 1418
Current left money : 1418 Zhangsan deposite 485
After Zhangsan left : 933
Current left money : 933 Lisi registe 156
After Lisi left : 1089
Current left money : 1089 Zhangsan deposite 265
After Zhangsan left : 824
Current left money : 824 Lisi registe 168
After Lisi left : 992
Current left money : 992 Zhangsan deposite 310
After Zhangsan left : 682
Current left money : 682 Lisi registe 382
After Lisi left : 1064
Current left money : 1064 Zhangsan deposite 47
After Zhangsan left : 1017
Current left money : 1017 Lisi registe 148
After Lisi left : 1165
Current left money : 1165 Zhangsan deposite 250
After Zhangsan left : 915
Current left money : 915 Zhangsan deposite 172
After Zhangsan left : 743
Current left money : 743 Zhangsan deposite 453
After Zhangsan left : 290
Current left money : 290 Zhangsan deposite 480
After Zhangsan left : -190
Current left money : -190 Lisi registe 202
After Lisi left : 12
Current left money : 12 Lisi registe 429
After Lisi left : 441
Current left money : 441 Lisi registe 219
After Lisi left : 660

        发现这个问题后,我查阅各种资料了解了一下synchronized关键字,原来它同步的是对象,而不是代码。而且在我的代码里,Bank.money这个对象,实际上是一个Integer类型的对象。有趣的是,在Java中,它封装Integer类型的时候,将数据成员定义成了static final int,也就是说,实际上每个Integer对象创建后都是不可变的,我们眼中的改变Integer对象的值实际上是创建了新的Integer对象?而恰巧synchronized是根据对象来同步的,在我的同步块里面,改变了Integer对象的值,于是,另外一个线程运行到这里时,有可能面对的刚好是那个新的对象,于是同步并没有起到作用。
       发现这个问题后再看自己之前的代码,在同步块里面改变对象这个操作真的是很皮呀。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页