java多线程那点事

屌丝程序员们对自己的技术能力总是毫不掩饰的高调,更有甚者每当完成一个简单的功能或算法实现,恨不得从工位上跳起来,生怕谁不知道一样,心情能理解,但个人完全鄙视这种行为。说到底,大家日常的coding,大多在单线程下执行,代码书写的顺序即执行的顺序,很多时候也是我们解决问题的逻辑顺序。有很多代码,如果考虑多线程,从并发的角度去实现,伪“大牛”们可能就要原形毕露了,很多同学更是束手无策。那么,多线程真的那么可怕么?接下来本人 把自己的一些理解分享出来,如有不当,欢迎指正。

java多线程问题简单一点说就是同一时间内有多个java线程对同一份内存数据进行存取。如果业务逻辑上对该份数据的值变化过程有要求,那么多线程的执行顺序不确定性将打乱该要求从而引起问题.因此,搞明白以下三个事情,也就能有效的理解并规避上述问题。

1.线程如何对内存中的变量进行存取?

2.线程究竟以何种顺序执行?

3.多线程如何控制同步?

大家知道,由于计算机的存储设备与处理器的运算速度之间有着数量级之间的差距,因此java采用高速cache作为中间桥梁,在缓存一致性协议下与主内存同步,如下图(java内存模型):

java内存模型规定所有的变量都存储在共享内存中,每条线程都有属于自己的工作内存,该工作内存中保存了该线程使用到的变量的共享内存副本拷贝,线程在工作期间对变量的所有存取操作都只是针对此副本拷贝,而不是真正共享内存中的变量,不同的线程之间无法直接访问对方工作内存中的变量,线程执行完操作后,同步协议将副本拷贝与共享内存对应变量进行同步,由于虚拟机对线程的抢占式调度,当多个线程同时同步一份数据的时候,必然出现并发问题。因此,解决并发首先要了解此同步协议,简单点说,该同步协议主要有以下八种操作:

(1) lock:将共享内存中的变量锁定,为一个线程所独占

(2) unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量

(3) read:将共享内存中的变量值读到工作内存当中

(4) load:将read读取的值保存到工作内存中的变量副本中。

(5) use:将值传递给线程的代码执行引擎

(6) assign:将执行引擎处理返回的值重新赋值给变量副本

(7) store:将变量副本的值存储到共享内存中。

(8) write:将store存储的值写入到共享内存的共享变量当中。

我们可以看到,要保证数据的同步,lock和unlock定义了一个线程访问一次共享内存的界限,有lock操作也必须有unlock操作,另外一些操作也必须要成对出现才可以,像是read和load、store和write需要成对出现,如果单一指令出现,那么就会造成数据不一致的问题。Java内存模型也针对这些操作指定了必须满足的规则:

(1) read和load、store和write必须要成对出现,不允许单一的操作,否则会造成从主内存读取的值,工作内存不接受或者工作内存发起的写入操作而主内存无法接受的现象。

(2) 在线程中使用了assign操作改变了变量副本,那么就必须把这个副本通过store-write同步回主内存中。如果线程中没有发生assign操作,那么也不允许使用store-write同步到主内存。

(3) 在对一个变量实行use和store操作之前,必须实行过load和assign操作。

(4) 变量在同一时刻只允许一个线程对其进行lock,有多少次lock操作,就必须有多少次unlock操作。在lock操作之后会清空此变量在工作内存中原先的副本,需要再次从主内存read-load新的值。在执行unlock操作前,需要把改变的副本同步回主存。

明白了java线程对变量的存取后,接下来需要知道的是线程究竟是以何种顺序执行,可以简单总结为一句话:”如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的“。即在线程内,指令的顺序表现为串行,在线程之间,指令表现为重排序以及工作内存与主内存的同步延迟。线程的执行有以下一些天然的顺序规则,粗略的说,如果两个操作之间的顺序关系不在以下规则里,并且也无法从以下规则中推导出,那么就可以断定它们是没有顺序性保障的,其行为不是线程安全的。

1.程序次序规则:在一个线程内,按照程序代码控制流(非书写顺序流,因为有分支,循环等),书写在前面的操作先行发生于后面的操作。这条规则保证了我们在日常的单线程环境下,能用业务逻辑的顺序去处理代码编写的顺序。当然,很多伪大牛们也在这条规则下coding的风生水起。不过此处仍有一个注意点,那就是JVM的优化有时候会导致指令重排,即后面的代码有可能先被处理器执行,比如 int a = 0; int b =1; 有可能执行器先给b赋值,然后给a赋值。这并不违反此规则,因为重排从线程内根本无法感知,无伤大雅,只是JVM做的一个优化。

2.管程锁定规则:对同一个锁,unlock操作先行发生于后面的lock操作。

3.volatile变量规则:对volatile修饰的变量,写操作先行发生于读操作。这也是大多数情况下我们使用volatile保证多线程操作变量的可见性的原因。

4.线程启动规则:Thread对象的start方法先行发生于此线程的每一个工作。这是必须
的。有点类似先取媳妇再生娃,未婚先育是不允许的。

5.线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。

6.线程的中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。有点类似先有鸡,然后由鸡粪。

7.对象终结规则:一个对象的初始化完成先行发生于它的finalize方法的开始。有点类似先有男人,然后有人妖。

8.先行传递规则:如果操作A先行发生于B,操作B先行发生于C,那么操作A先行发生于C。

上面的规则是无须使用任何同步手段保障就会发生的,在单线程下,重点参考规则1,一般来说无须考虑过多的顺序问题,但在多线程下,从上面的规则可以看到,无论是对变量的存取还是对象方法的执行,都很难套用上述规则,因此大部分都是线程不安全的。

既然多线程下不安全,那如何控制同步以促使其安全呢,这也是我们需要回答的第三个问题。一般我们需要做好以下几点:

1.尽量将对象放在单线程里进行存取,其他线程不参与修改,即所谓的线程封闭。如局部变量,final修饰的不可变对象,ThreadLocal类封装等。

2.使用volatile修饰变量使其多线程可见,使用synchronized同步方法或代码块

3.使用线程安全的容器替代非线程安全的容器,如Hashtable,synchronizedMap,ConcurrentMap,CopyOnWriteArrayList,ConcurrentLinkedQueue等。

4.使用java.util.concurrent.atomic下多种类小工具包,使读取-更新-保存过程原子化。

5.使用同步工具类进行互斥同步,主要包括以下几种类:

a:闭锁CountDownLatch,类似游戏关卡,只用把预先设定的所有关卡都countDown()后,await()才会解除阻塞,继续执行。

b:FutureTask.get,任何完成后此方法才返回结果,未完成前处于阻塞状态。

c:信号量Semaphore,类似公共厕所的蹲位,调用acquire()占用一个蹲位后开始LS,完事后调用release()释放蹲位提裤子走人,当蹲位满时,调用acquire()想用蹲位的人必须          憋着等待有人释放,如果一直人没有释放,就一直憋着或超时拉在裤子上。多用于池化实现,如连接池。

d:栅栏Barrier,类似旅游团包车,人数一定,每个上车的人调用await等待其他的人上车,当所有旅客都上车后,最后一个上车的司机开动车子,驶向预定的目标。

当然多线程还需要考虑很多的事情,以上阐述也也只是很小很浅显的一部分,水平有限,再咧咧恐贻笑大方了。简单理解多线程同步,就是利用同步手段使多个线程按一定顺序存取变量,无非也就涉及到如何操作存取,如何执行存取,如何调度存取,因此明白了本文最开始提出的三个问题,相信大家在今后的多线程并发中不会感到茫然,起码头脑中会有基本的思路,也可以帮伪大牛们真正走向成为大牛的道路,当然多线程远不止这些,真正成为大牛还得不断的摸索实践,也欢迎大家多分享。

来自为知笔记(Wiz)

时间: 2024-12-15 12:53:27

java多线程那点事的相关文章

关于JAVA多线程的那些事__初心者

前言 其实事情的经过也许会复杂了点,这事还得从两个月前开始说.那天,我果断不干IT支援.那天,我立志要做一个真正的程序猿.那天,我26岁11个月.那天,我开始看Android.那天,我一边叨念着有朋自远方来,一边投身了JAVA的怀抱.那天,一切将会改变. 好吧,反正总的来说就是时隔4年半,我又开始搞JAVA了.Eclipse还是Eclipse:NetBeans还是NetBeans:Java被收之后已经来到了7,现在是8:在入手了几本JAVA的书籍后发现<JAVA编程思想>还是这么伟大:开始了新

Java多线程导致的的一个事物性问题

业务场景 我们现在有一个类似于文件上传的功能,各个子站点接受业务,业务上传文件,各个子站点的文件需要提交到总站点保存,文件是按批次提交到总站点的,也就是说,一个批次下面约有几百个文件. 考虑到白天提交这么多文件会影响到子站点其他系统带宽,我们将分站点的文件提交到总站点这个操作过程独立出来,放到晚上来做,具体时间是晚上7:00到早上7:00. 这个操作过程我们暂且称作"排程". 排程在运行之后,先获取所有需要上传到总站点的批次信息,拿到批次信息之后,将这个批次表的状态置为正在同步数据,这

Rhythmk 一步一步学 JAVA (21) JAVA 多线程

1.JAVA多线程简单示例 1.1 .Thread  集成接口 Runnable 1.2 .线程状态,可以通过  Thread.getState()获取线程状态: New (新创建) Runnable (可以运行) Blocked  (被阻塞) Waiting  (等待) Timed waiting (计时等待) Terminated  (被终止) ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

【转】 Java 多线程之一

转自   Java 多线程 并发编程 一.多线程 1.操作系统有两个容易混淆的概念,进程和线程. 进程:一个计算机程序的运行实例,包含了需要执行的指令:有自己的独立地址空间,包含程序内容和数据:不同进程的地址空间是互相隔离的:进程拥有各种资源和状态信息,包括打开的文件.子进程和信号处理. 线程:表示程序的执行流程,是CPU调度执行的基本单位:线程有自己的程序计数器.寄存器.堆栈和帧.同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源. 2.Java标准库提供了进程和线程相关

java多线程(二)——用到的设计模式

接上篇:java多线程(一)http://www.cnblogs.com/ChaosJu/p/4528895.html java实现多线程的方式二,实现Runable接口用到设计模式——静态代理模式 一.代理模式 代理模式的定义 代理模式(Proxy Pattern)是对象的结构型模式,代理模式给某一个对象提供了一个代理对象,并由代理对象控制对原对象的引用. 代理模式不会改变原来的接口和行为,只是转由代理干某件事,代理可以控制原来的目标,例如:代理商,代理商只会卖东西,但并不会改变行为,不会制造

java从基础知识(十)java多线程(下)

首先介绍可见性.原子性.有序性.重排序这几个概念 原子性:即一个操作或多个操作要么全部执行并且执行的过程不会被任何因素打断,要么都不执行. 可见性:一个线程对共享变量值的修改,能够及时地被其它线程看到 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量 每个线程都有自己的工作内存,存有主内存中共享变量的副本,当工作内存中的共享变量改变,会主动刷新到主内存中,其它工作内存要使用共享变量时先从主内存中刷新共享变量到工作内存,这样就保证了共享变量的可见性. 可

java多线程并发编程与CPU时钟分配小议

我们先来研究下JAVA的多线程的并发编程和CPU时钟振荡的关系吧 老规矩,先科普 我们的操作系统在DOS以前都是单任务的 什么是单任务呢?就是一次只能做一件事 你复制文件的时候,就不能重命名了 那么现在的操作系统,我一边在这边写BLOG,一边听歌,一边开着QQ,一边…………………… 显然,现在的操作系统都是多任务的操作系统 操作系统对多任务的支持是怎么样的呢? 每打开一个程序,就启动一个进程,为其分配相应空间(主要是运行程序的内存空间) 这其实就支持并发运行了 CPU有个时钟频率,表示每秒能执行

java 多线程详解

线程的同步 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问. 由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块. 1. synchronized 方法:通过在方法声明中加入 synch

java多线程使用学习笔记

初学Java多线程,后续继续改进 一,Callable Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其他线程执行的任务 Callable和Runnable的区别如下: 1.Callable定义的方法是call,而Runnable定义的方法是run. 2.Callable的call方法可以有返回值,而Runnable的run方法不能有返回值. 3.Callable的call方法可抛出异常,而Runnable的run方法不能抛出异常.