Java实现多线程操作多账户

前言

某公司一个面试题:

1.有二十个账户,每个账户初始余额10000元。

2.有十个转账线程,对二十个账户中的两个随机选取账户进行转账,转账额度100以内正整数随机数。

3.每个线程执行100次转账操作。

4.最后请打印出二十个账户的余额。

正好很久没有做这类型题了,拿来练练手,结果碰到了一些问题。

正文

方案一:

首先描述下思路,首先用一个List数组存20个账户,然后对每个账户赋初值10000,在新建10个转账线程,对List数组中20个账户进行并发操作,每个线程分别获取两个账户进行随机额度转账。那么最简单的方式就是对List数组进行加锁就可以了。代码实现如下:

public class test {
    static int num;
    public static void main(String[] args) {
        List accountList = new ArrayList();
        for (int i = 0;i < 20;i++){
            accountList.add(10000);
        }
        System.out.println(num);

        MulThreadTest mulThreadTest = new MulThreadTest(accountList);
        ThreadGroup threadGroup =new ThreadGroup("ThreadGroup1");
        for (int i = 0;i < 10;i++){
            Thread thread = new Thread(mulThreadTest);
            thread.setName("thread="+i);
            thread.start();
        }
        try{
            Thread.sleep(5000);
        }catch(Exception e){
            e.printStackTrace();
        }
        int sum = 0;
        for (int i = 0;i < 20;i++){
            sum = sum + accountList.get(i);
            System.out.println(accountList.get(i));
        }
        System.out.println(sum);
    }
}
public class MulThreadTest implements Runnable{
    private List accountList;
    private  int first;
    private  int second;
    private  int num;

    public MulThreadTest(List accountList){
        this.accountList = accountList;
    }
    @Override
    public void run() {
        synchronized (accountList){
            for(int i = 0;i < 100;i ++) {
                first = new Random().nextInt(20);
                second = new Random().nextInt(20);
                if(first == second){
                    if(second < 19){
                        second += 1;
                    }else {
                        second = 1;
                    }

                }
                num = new Random().nextInt(100);
                if(accountList.get(first) - num < 0){
                    i --;
                    continue;
                }
                System.out.println(Thread.currentThread().getName()+",操作前:"+"账户:"+first+"="+accountList.get(first)+"账户:"+second+"="+accountList.get(second));
                accountList.set(first,accountList.get(first) -  num);
                accountList.set(second,accountList.get(second) +  num);
                System.out.println(Thread.currentThread().getName()+",操作:"+"账户:"+first+"="+accountList.get(first)+"=>"+num+"=>"+"账户:"+second+"="+accountList.get(second));
            }
        }

    }
}

Java实现多线程操作多账户_第1张图片

以下是20个账户的金额以及总金额

10114
10046
10268
10676
10250
10344
9899
10120
9629
10145
9211
9761
9903
10187
9374
10508
10046
10005
9529
9985
200000

以上可以解决这个转账并发问题,但是锁的粒度还是有点大了,在一个线程操作List时其它线程是没法操作的,那么如何减小锁的粒度,实现更高的效率呢?

方案二:

在方案一的基础上,把锁的对象修改为List中的某个账户,这样当其中一个线程在操作某个账户时,其它线程就仅仅无法操作该账户,影响范围能减小很多。代码实现如下:

public class test {
    static int num;
    public static void main(String[] args) {
        List accountList = new ArrayList();
        for (int i = 0;i < 20;i++){
            accountList.add(new Account(10000));
        }
        System.out.println(num);

        MulThreadTest1 mulThreadTest = new MulThreadTest1(accountList);
        ThreadGroup threadGroup =new ThreadGroup("ThreadGroup1");
        for (int i = 0;i < 10;i++){
            Thread thread = new Thread(mulThreadTest);
            thread.setName("thread="+i);
            thread.start();
        }
        try{
            Thread.sleep(5000);
        }catch(Exception e){
            e.printStackTrace();
        }
        int sum = 0;
        for (int i = 0;i < 20;i++){
            sum = sum + accountList.get(i).getNum();
            System.out.println(accountList.get(i).getNum());
        }
        System.out.println(sum);
    }
}
public class MulThreadTest1 implements Runnable{
    private List accountList;
    Account firstInteger;
    Account secondInteger;


    public MulThreadTest1(List accountList){
        this.accountList = accountList;
    }
    @Override
    public void run() {
         int first;
         int second;
         int num;
         
            for(int i = 0;i < 100;i ++) {
                first = new Random().nextInt(20);
                second = new Random().nextInt(20);
                if(first == second){
                    if(second < 19){
                        second += 1;
                    }else {
                        second = 1;
                    }

                }
                firstInteger = accountList.get(first);

                    synchronized (firstInteger) {

                        num = new Random().nextInt(100);
                        if (firstInteger.getNum() - num < 0) {
                            i--;
                            continue;
                        }
                        System.out.println(Thread.currentThread().getName() + ",操作前:" + "账户:" + first + "=" + accountList.get(first).getNum() + "账户:" + second + "=" + accountList.get(second).getNum());

                        firstInteger.setNum(firstInteger.getNum() - num);

                        accountList.set(first, firstInteger);

                    secondInteger = accountList.get(second);

                        synchronized (secondInteger) {
                            secondInteger.setNum(secondInteger.getNum() + num);
                            accountList.set(second, secondInteger);
                            //accountList.set(first,accountList.get(first) -  num);
                            //accountList.set(second,accountList.get(second) +  num);
                            System.out.println(Thread.currentThread().getName() + ",操作:" + "账户:" + first + "=" + accountList.get(first).getNum() + "=>" + num + "=>" + "账户:" + second + "=" + accountList.get(second).getNum());
                        }
                
                }

                   

            }

    }
}

部分结果如下:

thread=4,操作前:账户:15=10037账户:16=10307
thread=4,操作:账户:15=9972=>65=>账户:16=10372
thread=4,操作前:账户:10=10012账户:16=10372
thread=4,操作:账户:10=9958=>54=>账户:16=10426
thread=4,操作前:账户:3=9854账户:19=9845
thread=6,操作前:账户:14=9842账户:19=9998
thread=0,操作前:账户:19=9845账户:14=9842
thread=5,操作前:账户:2=9821账户:17=10387
thread=5,操作:账户:2=9786=>35=>账户:17=10422
thread=5,操作前:账户:0=9574账户:14=9822
thread=8,操作前:账户:15=9972账户:16=10426
thread=8,操作:账户:15=9962=>10=>账户:16=10436
thread=7,操作前:账户:18=10367账户:19=9760
thread=8,操作前:账户:17=10422账户:10=9958
thread=8,操作:账户:17=10414=>8=>账户:10=9966
thread=8,操作前:账户:1=10552账户:10=9966
thread=8,操作:账户:1=10487=>65=>账户:10=10031
9528
10487
9786
9763
10109
9916
9162
9803
10237
10276
10031
10163
9987
9686
9822
9962
10436
10414
10359
9760
199687

使用jps和jstack即可看到线程状态

Found one Java-level deadlock:
=============================

"thread=2":
  waiting to lock Monitor@0x00000000192b2598 (Object@0x00000000d5ea31b8, a com/xxx/testdemo/multhreadtest/Account),
  which is held by "thread=7"
"thread=7":
  waiting to lock Monitor@0x00000000192b1038 (Object@0x00000000d5ea30a8, a com/xxx/testdemo/multhreadtest/Account),
  which is held by "thread=9"
"thread=9":
  waiting to lock Monitor@0x00000000192b3cf8 (Object@0x00000000d5ea31a0, a com/xxx/testdemo/multhreadtest/Account),
  which is held by "thread=2"

Found a total of 1 deadlock.

Thread 1: (state = BLOCKED)


Thread 26: (state = BLOCKED)
 - com.rain.testdemo.multhreadtest.MulThreadTest1.run() @bci=270, line=56 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)


Thread 25: (state = BLOCKED)
 - com.rain.testdemo.multhreadtest.MulThreadTest1.run() @bci=91, line=41 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)


Thread 24: (state = BLOCKED)
 - com.rain.testdemo.multhreadtest.MulThreadTest1.run() @bci=270, line=56 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)


Thread 23: (state = BLOCKED)
 - com.rain.testdemo.multhreadtest.MulThreadTest1.run() @bci=91, line=41 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)


Thread 22: (state = BLOCKED)
 - com.rain.testdemo.multhreadtest.MulThreadTest1.run() @bci=91, line=41 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)


Thread 21: (state = BLOCKED)
 - com.rain.testdemo.multhreadtest.MulThreadTest1.run() @bci=270, line=56 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)


Thread 20: (state = BLOCKED)
 - com.rain.testdemo.multhreadtest.MulThreadTest1.run() @bci=270, line=56 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)


Thread 19: (state = BLOCKED)
 - com.rain.testdemo.multhreadtest.MulThreadTest1.run() @bci=270, line=56 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)


Thread 18: (state = BLOCKED)
 - com.rain.testdemo.multhreadtest.MulThreadTest1.run() @bci=270, line=56 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)


Thread 17: (state = BLOCKED)
 - com.rain.testdemo.multhreadtest.MulThreadTest1.run() @bci=91, line=41 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=745 (Interpreted frame)


Thread 11: (state = IN_NATIVE)
 - java.net.SocketInputStream.socketRead0(java.io.FileDescriptor, byte[], int, int, int) @bci=0 (Interpreted frame)
 - java.net.SocketInputStream.socketRead(java.io.FileDescriptor, byte[], int, int, int) @bci=8, line=116 (Interpreted frame)
 - java.net.SocketInputStream.read(byte[], int, int, int) @bci=117, line=171 (Interpreted frame)
 - java.net.SocketInputStream.read(byte[], int, int) @bci=11, line=141 (Interpreted frame)
 - sun.nio.cs.StreamDecoder.readBytes() @bci=135, line=284 (Interpreted frame)
 - sun.nio.cs.StreamDecoder.implRead(char[], int, int) @bci=112, line=326 (Interpreted frame)
 - sun.nio.cs.StreamDecoder.read(char[], int, int) @bci=180, line=178 (Interpreted frame)
 - java.io.InputStreamReader.read(char[], int, int) @bci=7, line=184 (Interpreted frame)
 - java.io.BufferedReader.fill() @bci=145, line=161 (Interpreted frame)
 - java.io.BufferedReader.readLine(boolean) @bci=44, line=324 (Interpreted frame)
 - java.io.BufferedReader.readLine() @bci=2, line=389 (Interpreted frame)
 - com.intellij.rt.execution.application.AppMainV2$1.run() @bci=36, line=47 (Interpreted frame)


Thread 10: (state = BLOCKED)


Thread 9: (state = BLOCKED)


Thread 8: (state = BLOCKED)
 - java.lang.Object.wait(long) @bci=0 (Interpreted frame)
 - java.lang.ref.ReferenceQueue.remove(long) @bci=59, line=143 (Interpreted frame)
 - java.lang.ref.ReferenceQueue.remove() @bci=2, line=164 (Interpreted frame)
 - java.lang.ref.Finalizer$FinalizerThread.run() @bci=36, line=209 (Interpreted frame)


Thread 7: (state = BLOCKED)
 - java.lang.Object.wait(long) @bci=0 (Interpreted frame)
 - java.lang.Object.wait() @bci=2, line=502 (Interpreted frame)
 - java.lang.ref.Reference.tryHandlePending(boolean) @bci=54, line=191 (Interpreted frame)
 - java.lang.ref.Reference$ReferenceHandler.run() @bci=1, line=153 (Interpreted frame)

发现已经deadlock了 

修改如下:

public class MulThreadTest1 implements Runnable{
    private List accountList;
    Account firstInteger;
    Account secondInteger;
    volatile int flag1 = 0;
    volatile int flag2 = 0;


    public MulThreadTest1(List accountList){
        this.accountList = accountList;
    }
    @Override
    public void run() {
         int first;
         int second;
         int num;

            for(int i = 0;i < 100;i ++) {
                first = new Random().nextInt(20);
                second = new Random().nextInt(20);
                if(first == second){
                    if(second < 19){
                        second += 1;
                    }else {
                        second = 1;
                    }

                }
                firstInteger = accountList.get(first);
                if(flag1 == 0) {
                    flag1 = 1;
                    synchronized (firstInteger) {

                        num = new Random().nextInt(100);
                        if (firstInteger.getNum() - num < 0) {
                            i--;
                            continue;
                        }
                        System.out.println(Thread.currentThread().getName() + ",操作前:" + "账户:" + first + "=" + accountList.get(first).getNum() + "账户:" + second + "=" + accountList.get(second).getNum());

                        firstInteger.setNum(firstInteger.getNum() - num);

                        accountList.set(first, firstInteger);

                    }
                    flag1 = 0;
                    secondInteger = accountList.get(second);
                    if(flag2 == 0) {
                        flag2 = 1;
                        synchronized (secondInteger) {
                            secondInteger.setNum(secondInteger.getNum() + num);
                            accountList.set(second, secondInteger);
                            //accountList.set(first,accountList.get(first) -  num);
                            //accountList.set(second,accountList.get(second) +  num);
                            System.out.println(Thread.currentThread().getName() + ",操作:" + "账户:" + first + "=" + accountList.get(first).getNum() + "=>" + num + "=>" + "账户:" + second + "=" + accountList.get(second).getNum());
                        }
                    }
                }

                    flag2 = 0;

            }

    }
}

输出结果发现最终所有账户金额总额不是200000。

以上代码存在几个问题:

问题一:假如线程1对该对象进行操作时,从List中获取账户对象后,线程2也会从List中获取账户对象,然后当线程1抢到锁之前,线程2把线程1中的账户对象覆盖了,这样操作就有问题了。

问题二:假如线程1抢到了锁,并对账户1进行了操作,即将对账户2进行操作,此时发现flag2为1,那么此时线程1就会结束本次操作,这样线程1只对账户1进行了金额扣除,并没有回退操作,总金额就减少了。

问题三:设置两个flag值并不能防止死锁。当线程1锁了账户1,线程2锁了账户2,假设线程1和线程2同时制行了if(flag1==0),那么此时flag1并没有起到作用,之后线程1和线程2再次同时执行了if(flag2==0),线程1去对账户2操作,线程2去对账户1操作,发现都是锁着的,这就导致了死锁。

那么应该如何来防止以上死锁的情况呢?

只要打破造成死锁的条件即可,以上方案造成死锁的原因是同一个线程同时去抢占两个锁,这样很容易死锁,所以只要保证同一个线程同一个时刻只能获取一个锁这样就能避免死锁。

public class MulThreadTest3 implements Runnable{
    private List accountList;
    //Account firstInteger;
    //Account secondInteger;

    public MulThreadTest3(List accountList){
        this.accountList = accountList;
    }
    @Override
    public void run() {
         int first;
         int second;
         int num;

         for(int i = 0;i < 100;i ++) {
             first = new Random().nextInt(20);
             second = new Random().nextInt(20);
             if(first == second){
                 if(second < 19){
                     second += 1;
                 }else {
                     second = 1;
                 }
             }
             //System.out.println(Thread.currentThread().getStackTrace()+Thread.currentThread().getName());
             Account firstInteger = accountList.get(first);
             synchronized (firstInteger) {

                 num = new Random().nextInt(100);
                 if (firstInteger.getNum() - num < 0) {
                     i--;
                     continue;
                 }
                 System.out.println(Thread.currentThread().getStackTrace()[0].getMethodName()+Thread.currentThread().getName() + ",操作前:" + "账户:" + first + "=" + accountList.get(first).getNum() + "账户:" + second + "=" + accountList.get(second).getNum());

                 firstInteger.setNum(firstInteger.getNum() - num);
                 firstInteger.setThreadName(Thread.currentThread().getName());
                 accountList.set(first, firstInteger);
             }

             Account secondInteger = accountList.get(second);
             synchronized (secondInteger) {
                 secondInteger.setNum(secondInteger.getNum() + num);
                 firstInteger.setThreadName(Thread.currentThread().getName());
                 accountList.set(second, secondInteger);
                 //accountList.set(first,accountList.get(first) -  num);
                 //accountList.set(second,accountList.get(second) +  num);
                 System.out.println(Thread.currentThread().getStackTrace()[0].getMethodName()+Thread.currentThread().getName() + ",操作:" + "账户:" + first + "=" + accountList.get(first).getNum() + "=>" + num + "=>" + "账户:" + second + "=" + accountList.get(second).getNum());
             }

         }

    }
}

部分运行结果如下

getStackTracethread=0,操作前:账户:5=11323账户:19=9676
getStackTracethread=1,操作:账户:16=10083=>10=>账户:10=9543
getStackTracethread=0,操作:账户:5=11299=>24=>账户:19=9700
getStackTracethread=1,操作前:账户:11=10993账户:9=10525
getStackTracethread=0,操作前:账户:18=9344账户:1=9939
getStackTracethread=1,操作:账户:11=10983=>10=>账户:9=10535
getStackTracethread=0,操作:账户:18=9324=>20=>账户:1=9959
getStackTracethread=1,操作前:账户:10=9543账户:19=9700
getStackTracethread=0,操作前:账户:18=9324账户:10=9543
getStackTracethread=1,操作:账户:10=9522=>21=>账户:19=9721
getStackTracethread=0,操作:账户:18=9307=>17=>账户:10=9539
getStackTracethread=1,操作前:账户:19=9721账户:15=10155
getStackTracethread=1,操作:账户:19=9689=>32=>账户:15=10187
getStackTracethread=1,操作前:账户:3=10113账户:5=11299
getStackTracethread=1,操作:账户:3=10097=>16=>账户:5=11315
getStackTracethread=1,操作前:账户:13=9419账户:11=10983
getStackTracethread=1,操作:账户:13=9398=>21=>账户:11=11004
getStackTracethread=1,操作前:账户:5=11315账户:3=10097
getStackTracethread=1,操作:账户:5=11251=>64=>账户:3=10161
9280
9959
10004
10161
10641
11251
10040
8710
9800
10535
9539
11004
10635
9398
9814
10187
10083
9963
9307
9689
200000

总结

本题考察了多线程,以及多线程场景中锁的应用,如何避免死锁是关键,方案一虽然能很快解决本地,但是应该不是最理想的方案。

你可能感兴趣的:(java,开发语言)