【Java】多线程系列(一)之共享数据修改

来源:互联网 发布:火星哥在美国地位 知乎 编辑:程序博客网 时间:2024/06/05 14:33

博文延续前几篇以代码讲解知识点的风格。。。

前言

我们知道,多线程中共享一个数据,并对其进行修改,这种场景下很多情况下都会出现,例如,卖火车票。火车票总量是一个共享数据,而每个售票窗口就相当于一个线程,多个售票窗口同时进行售票。票的总数就是一个共享数据,但是每一次操作都是对总票数的操作。

下面本文以该例子来实例讲解多线程环境下的共享数据修改的问题。

测试代码:

package thread;public class threadStopTest {    public static void main(String[] args) {        ThreadTest runn = new threadStopTest().new ThreadTest();        Thread th1 = new Thread(runn, "Thread1");        Thread th2 = new Thread(runn, "Thread2");        Thread th3 = new Thread(runn, "Thread3");        Thread th4 = new Thread(runn, "Thread4");        long start=System.currentTimeMillis();        System.out.println(System.currentTimeMillis());        th1.start();        th2.start();        th3.start();        th4.start();        try {            th1.join();            th2.join();            th3.join();            th4.join();        } catch (Exception e) {            e.printStackTrace();        }        long end=System.currentTimeMillis();        System.out.println("时间差:"+(end-start));        System.out.println("完毕");    }    // 为什么这个就不需要加锁呢?什么时候需要加锁?    class ThreadTest implements Runnable {        private int tickets = 1000;        /*         * method1:这种方式消耗时间和单线程没有区别,因为代码块被锁住,某一刻只能被某个线程占用         *      (1)在没有加Thread.sleep(10);这句代码之前,会出现每一个线程出现分片现象(每个线程连续执行多次,之后再切到另一个线程),但是每个线程执行的次数可能很不均衡,甚至有的线程没有机会执行。         *          执行时间和单线程没有太大区别,甚至时间消耗更大(因为有锁,会占用一定时间)         *      (2)加了这句代码之后,每个线程执行的次数比较均匀,线程执行时成片交替进行         *///      @Override        public void run() {            while(true){                synchronized (this) {                    if(tickets<=0){                        break;                    }                    System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--);//                  try {//                      Thread.sleep(10);//                  } catch (InterruptedException e) {//                      e.printStackTrace();//                  }                }            }        }        //method2:这种方式消耗时间和单线程没有区别,而且运行方式和单线程没有区别,整个执行流程是第一个执行的线程掌握整个资源//      public void run() {//          synchronized (this) {//              while (tickets > 0) {//                  System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--);//                  try {//                      Thread.sleep(1);//                  } catch (InterruptedException e) {//                      e.printStackTrace();//                  }//              }//          }//      }        //method3:下面这个时间根据线程个数决定,线程越多,时间根据线程数量线性缩短(在睡眠1ms的时候会出现0)//      public void run() {//          while (tickets > 0) {//              System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--);////                try {////                    Thread.sleep(1);////                } catch (InterruptedException e) {////                    e.printStackTrace();////                }//          }//      }    }}

上面的代码贴出了三种不同的执行方式,但是都是完成同一个目标(即对多个窗口卖票,直至售完为止)

Method1

方法1中,利用synchronized关键字对对象(this,这里指的是runn)加锁。每个时刻只能有一个线程持有这个对象,因此他的执行时间其实就是一个单线程执行的时间,可能改更长(因为有锁)。

运行结果图:

这里写图片描述

结论:

这种方式消耗时间和单线程没有区别,因为代码块被锁住,某一刻只能被某个线程占用

(1)在没有加Thread.sleep(10);这句代码之前,会出现每一个线程出现分片现象(每个线程连续执行多次,之后再切到另一个线程),但是每个线程执行的次数可能很不均衡,甚至有的线程没有机会执行。执行时间和单线程没有太大区别,甚至时间消耗更大(因为有锁,会占用一定时间)
(2)加了这句代码之后,每个线程执行的次数比较均匀,线程执行时成片交替进行

Method2

方法2中,也是利用synchronized关键字进行加锁,但是这个加锁是对整个run()内部的代码块加锁,因此,一旦线程拥有了这个锁,就会一直占用下去。

所以运行结果如下:

这里写图片描述

这里,结果截取了最后一部分,没有全部放上来,但是其实所有的结果都是Thread1在执行。这是因为Thread1一直都占用这这个锁。

结论:

这种方式消耗的时间和单线程没有区别,所以定义多个线程纯属浪费资源。

Method3

方法3中,并没有加任何锁,因此在处理数据的时候,可能会出现一些数据不准确的问题。

例如,对于方法3中的判断条件,如果多个线程同时进入了这个条件,比如A线程进入的时候票数还有1张,然后他进行了减1的操作,但是这个时候还没来得及赋值(注意:自减不是原子操作,它包括两个步骤,一个是减1,然后赋值),这个时候B线程判断发现,票数还是1,也满足while条件,因此也会执行里面的代码,这个时候,就会出现一种现象,最终结果打印的时候,会出现0的情况。

下面,截取运行时,可能会出现的一种错误,进行结果图展示。
这里写图片描述

结论:

这种方法不加锁,运行时间根据线程个数决定,线程越多,运行时间越短。时间根据线程数量线性缩短(打印结果可能会出现XXXX is saling ticket 0,然而这其实是一种错误结果,因为我们卖票的时候不可能票都完了,结果还能卖票)

值得讨论的地方

所以,根据上面的结论来看,如果既想保证数据的准确性,又想保证计算速度。对于这种共享数据修改(多线程情形),上面的方法并不可取,要么加锁导致性能降低,要么不加锁导致数据不准确。

那么问题来了。。。。
也就是,
如果既想保证数据准确性,又想保证性能,怎么实现?

这里留一个问题,也当作是对自己的提问,之后学习需要进一步深入。

后续测试:

针对上面的方法3:

由于没有加锁,但是会导致数据的不准确性问题,但并不会报错。但是如果换成是其他数据或者对象,很有可能会出现异常信息。例如,我们如果在多线程环境下,对同一HashMap对象进行操作的话,有可能会出现异常信息。
下面,继续做了一个实验来验证我的想法。

package thread;import java.util.HashMap;import java.util.Random;public class theadModifyHashMap {    public static void main(String[] args) {        ThreadTest runn = new theadModifyHashMap().new ThreadTest();        Thread th1 = new Thread(runn, "Thread1");        Thread th2 = new Thread(runn, "Thread2");        Thread th3 = new Thread(runn, "Thread3");        Thread th4 = new Thread(runn, "Thread4");        long start=System.currentTimeMillis();        System.out.println(System.currentTimeMillis());        th1.start();        th2.start();        th3.start();        th4.start();        try {            th1.join();            th2.join();            th3.join();            th4.join();        } catch (Exception e) {            e.printStackTrace();        }        long end=System.currentTimeMillis();        System.out.println("时间差:"+(end-start));        System.out.println("完毕");    }    class ThreadTest implements Runnable {        private HashMap<String, String> map=new HashMap<String, String>();        private int count=0;        //method1:不加锁(会出现ConcurrentModifyException异常,偶尔出现,不会每次都出现)//      public void run() {//          while (map.size()<10) {//              System.out.println(count);//              System.out.println(map);//              map.put(String.valueOf(new Random().nextInt(10)), String.valueOf(new Random().nextInt(10)));//              System.out.println(Thread.currentThread().getName());//              try {//                  Thread.sleep(1);//              } catch (InterruptedException e) {//                  // TODO Auto-generated catch block//                  e.printStackTrace();//              }//              count++;//          }//      }        //加锁,不会出现异常        public void run() {            while (true) {                synchronized (map) {                    if(map.size()==10){                        break;                    }                    System.out.println(count);                    System.out.println(Thread.currentThread().getName());                    map.put(String.valueOf(new Random().nextInt(10)), String.valueOf(new Random().nextInt(10)));                    System.out.println(map);                    try {                        Thread.sleep(1);                    } catch (InterruptedException e) {                        // TODO Auto-generated catch block                        e.printStackTrace();                    }                    count++;                }            }        }    }}

对于方法1,可能会出现如下的并发修改的异常信息(并不是一定出现)。

这里写图片描述

但是,如果要解决这种并发问题的话,可以考虑加锁(即方法2),或者使用并发容ConcurrentHashMap。

这里可以参看之前的一篇博文

【Java】并发容器ConcurrentHashMap和CopyOnWriteArrayList(一)

之后,会针对多线程这一部分知识,做一个系列学习的笔记

目前先列一些需要学习的东西,后续学习中继续补充。。。

1)Callable和Runnable接口的区别(2)ScheduledThreadPoolExector和Timer的区别(3)对代码块加锁和数据加锁的区别(4)加锁和volatile变量的区别(5)原子性(多线程在不加锁情况下修改基础类型数据和非基础数据类型的区别(6synchronized括号后面加的东西是什么?应该怎么加?什么时候释放锁(7)互斥锁(8)悲观锁和乐观锁(9)CountDownLatch的使用
1 0