【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)原子性(多线程在不加锁情况下修改基础类型数据和非基础数据类型的区别(6)synchronized括号后面加的东西是什么?应该怎么加?什么时候释放锁(7)互斥锁(8)悲观锁和乐观锁(9)CountDownLatch的使用
- 【Java】多线程系列(一)之共享数据修改
- java多线程系列----------- 共享受限资源(一)
- java基础多线程之共享数据
- java多线程通信之共享数据
- JAVA多线程(四)多线程数据共享
- JAVA多线程共享数据
- Java多线程共享数据
- [java多线程]多线程数据共享
- Java基础:多线程之线程范围内的数据共享ThreadLocal
- java基础巩固笔记(5)-多线程之共享数据
- java多线程实现数据共享
- java多线程系列----------- 共享受限资源(二)
- java多线程系列----------- 终结任务(一)
- java多线程系列(一)基础概念
- java学习系列2(多线程一)
- Java多线程干货系列—(一)Java多线程基础
- java多线程共享数据和数据并发
- Java多线程系列--“JUC锁”03之 公平锁(一)
- eclipse打开properties文件
- JAVA MAIL 发送邮件(一)
- struts2简介
- java8 LinkedList源码阅读【2】- 总结
- 沟通的法宝!"三季人法则"
- 【Java】多线程系列(一)之共享数据修改
- 2.基本语法
- 书籍管理系统
- Day2:实战作业
- 2017-4-17
- 3.数据类型
- 127.App显示的ListView的item点击事件,弹出泡泡窗
- 活动安排问题
- php中使用mysqli_fetch_array跟mysqli_fetch_row的区别