进程一般作为资源的组织单位,是计算机程序的运行实例,表示正在执行的指令,有自己独立的地址空间,包含程序内容和数据,进程间资源和状态相互隔离。
线程是程序的执行流程,CPU调度执行的基本单位,有自己的程序计数器,寄存器,堆栈,帧,共享同一进程的地址空间,内存和其他资源。
当虚拟机中运行的所有线程都是守护线程时,虚拟机终止运行。
使用共享内存的方式进行多线程通信的话,可能造成可见性的相关问题,即一个线程所做的修改对于其他的线程不可见,导致其他线程仍然使用错误的值。
造成的原因:
(1)多线程的实际执行顺序
(2)CPU采用的层次结构的多级缓存架构
在写入时数据被先写入缓存中,之后在某个特定的时间被写回主存。不同的CPU可能采用不同的写入策略,如写穿透或者写返回等。由于缓存的存在,在某些时间点上,缓存中的数据域主存中的数据可能是不一致的。
(3)CPU指令重排
描述了程序中共享变量的关系以及在主存中写入和读取这些变量值得底层细节。
由程序内部代码逻辑决定的执行顺序,该顺序具备一致性。
同步动作之间的先后顺序。成功的加锁动作必然在某个解锁动作之后。常见的同步关系(即A发生在B前)如下:
A、在一个监视器对象上的解锁动作与同对象上后续成功的加锁动作保持同步
B、对一个声明为volatile的变量的写入动作与同一变量的后续读取动作保持同步
C、启动一个线程的动作与该线程执行的第一个动作保持同步
D、向线程中共享变量写入默认值的动作与该线程执行的第一个动作保持同步
E、线程A运行时的最后一个动作与另外一个线程中任何可以检测到线程A终止的动作保持同步
F、如果线程A中断线程B,那么线程A的中断动作与任何其他线程检测到线程B处于被中断的状态的动作保持同步
如果一个动作按照happens-before顺序发生在另外一个动作之前,那么前一个动作的执行结果再多线程程序中对后一个动作肯定是可见的。
A、如果A、B两个动作在一个线程中执行,同时程序顺序中A出现在B之前,则A在B之前发生
B、一个对象的构造方法的结束在该对象的finalize方法运行之前发生
C、如果动作A和动作B保持同步,则A在B之前发生
D、如果动作A在动作B之前发生,同时动作B在动作C之前发生,则A在C之前发生。(传递性)
开发者需要做的是利用Java平台提供的支持来消除程序中的数据竞争,保证程序的正确性,不需要考虑CPU和编译器可能进行的指令重排,因为Java虚拟机和编译器会确保这些指令重排不会影响程序正确性。
可以保证有序性和可见性,不保证原子性。
有序性是:对一个声明为volatile的变量的写入动作与同一变量的后续读取动作保持同步
可见性:在写入volatile变量之后,CPU缓存中的内容会被写回主存,在读取volatile变量时,CPU缓存中的该变量副本被置为失效状态,重新从主存中读取。
主要用来确保对一个变量的修改被正确地传播到其他线程中。
public class VolatileDemo { private volatile boolean done; public void setDone(boolean done){ this.done = done; } public void work() { while (!done) { //执行任务 } } }
final在JMM中的语义是可见性。
该域的值只能被初始化一次,一旦在构造器中初始化完成并正确发布后,其他线程可见。
未正确发布的实例:
public class WrongUser { private final String name; public WrongUser(String name) { UserHolder.user = this; this.name = name; } } public class UserHolder { public static WrongUser user = null; }
如果域没有被声明为final,则构造方法完成之后,其他线程不一定看得到这个域被初始化之后的值,而有可能是默认值。对于final域,在代码执行时,可以被保持在寄存器中,不用从主存中频繁重新读取。
Java代码中的一条语句,可能实际上对应的是多条字节码指令,CPU在一条指令的执行过程中不会进行线程调度和上下文切换,但是在两条指令的执行间隙,可能发生线程切换。
如果value++由一条CPU指令完成的话,就不存在多线程访问的问题==》原子操作。
具有可见性、有序性、原子性,堪称万能。
会导致线程进入blocked状态,等待获取锁。
可见性:当锁被释放时,对共享变量的修改会从CPU缓存直接写回到主存中;当锁被获取时,CPU的缓存内存被置为失效状态,从主存中重新读取共享变量的值。
有序性:编译器在处理synchronized代码块的时候,不会把其中包含的代码移到synchronized之外,从而避免了由于代码重排而造成的问题。
原子性:一个线程运行执行synchronized代码块之前,需要先获取对应的监视器上的锁对象,执行完之后自动释放。保证同一时刻只有一个线程在操作同步块里头的变量。
线程的同步,通过synchronized来解决,而线程之间的协作,则需要等待-通知机制。
public class VolatileDemo { private volatile boolean done; public void setDone(boolean done){ this.done = done; } public void work() { while (!done) { //执行任务 } } }
上面这种让线程处于忙于等待的情况,需要占用CPU的时间,会对性能造成影响,可以使用等待-通知机制。
java中每个对象除了有与之关联的监视器对象之外,还有一个与之关联的包含线程的等待集合。成功调用wait方法的前提是当前线程获取到监视器对象的锁,如果没有锁则抛出java.lang.IllegalMonitorStateException异常,如果获得到锁之后,那么当前线程被添加到对象关联的等待集合中,同时释放持有的监视器对象上的锁,进入等待状态。
wait方法必须在synchronized中进行,限期等待在一定时间过后自动唤醒,无限期等待则需要其他线程唤醒。
notify由虚拟机实现来决定唤醒哪个线程,可能是随机的,不按进入等待集合顺序的;
notifyAll唤醒该对象等待集合中的所有线程,但会导致有些没有必要的线程被唤醒之后,马上又进入等待状态,造成一定性能影响,但是可以保证程序的正确性。
被唤醒后,需要重新竞争获取对象的锁,然后执行后续方法。
处于某个对象关联的等待集合中的线程可能被意外唤醒,这是由底层操作系统和虚拟机实现所产生的非正常行为,这种意外无法避免,需要开发人员注意处理。把wait方法置于循环之中。
synchronized(obj){ //避免意外唤醒 while(/*条件不满足*/){ obj.wait(); } }
新建尚未运行
处于可运行状态:正在运行或准备运行
等待获取锁时进入的状态
通过wait方法进入的等待
通过sleep或wait timeout方法进入的限期等待的状态
线程终止状态
线程之间的一种通信方式,一般情况下,中断一个线程会再对应的Thread类的对象上设置一个标记,该标记用来记录当前的中断状态,通过Thread类的isInterrupted方法可以查询此标记来判断是否有中断请求发生。
当线程由于调用wait,join,sleep方法进入等待状态时,如果收到中断请求,线程会进入InterruptedException异常的处理逻辑。在该异常发生时,当前线程对应的Thread类的对象的中断标记会被清空,相当于该中断请求已经被异常处理逻辑处理了。如果当前异常处理代码中不适合处理该异常又无法把它重新抛出来,则应该通过interrupt方法来重新中断该线程,这样就保存了当前线程曾经被中断过的状态信息,可以让后续代码来处理该中断请求。
interrupted方法不但可以判断当前线程是否被中断,还可以清除线程内部的中断标记。如果返回true则说明该线程曾经被中断过,在该方法调用完成之后,中断标记会被清空。
如果中断发生时,线程处于对象关联的等待集合中,则该线程会被移除集合,离开等待状态(执行异常处理逻辑)。
如果中断跟唤醒同时发生时,对于notifyAll方法至少有一个线程被唤醒,或者所有的线程被中断,唤醒请求不会丢失,不受中断影响;如果一个线程被选为唤醒对象,同时又被中断,且虚拟机选择让线程中断,则等待集合中另外一个对象必须被唤醒。
A调用B的join方法,即A等待线程B完成。
public static void main(String[] args){ Thread thread = new Thread(){ public void run(){ try{ Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }; thread.start(); try{ //主线程等待线程运行结束 thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } }
sleep方法会导致线程进入TIMED_WAITING状态,但是不会释放所持有的锁,因此不要把sleep放在synchronized方法中,否则会造成其他线程等待锁的时间太长(debug是可用)。
如果当前线程因为某些原因无法继续运行,可用使用yield方法来尝试让出所占用的CPU资源,让其他线程获取运行的机会,yield对操作系统上的调度器来说是一个信号,但调度器不一定会立即进行线程切换。调用yield方法可以使线程切换频繁一些,可以暴露出多线程相关错误。
锁机制给多线程带来的性能影响主要来自于其带来的线程blocked问题。
如果能够把读取、修改、写入三步组成一个CPU的原子操作,则CPU在执行这三步时,不会发生线程切换,也不会造成数据的不一致问题。
通过CAS(Compare-and-Swap)操作可以实现不依赖锁机制的非阻塞算法。一般将其放在一个无限循环当中调用,如果当前循环没能完成修改操作,就不断进行尝试,总会在某个时机上完成修改操作。
java中的CAS操作包含在atomic包里头:
public class AtomicIdGenerator { private final AtomicInteger counter = new AtomicInteger(0); public int getNext() { return counter.getAndIncrement(); } public static void main(String[] args) { AtomicIdGenerator generator = new AtomicIdGenerator(); System.out.println(generator.getNext()); } }
AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference
把数组声明为volatile只能保证对引用变量本身的修改是对其他线程可见的,但是不涉及数组中所包含的元素。AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray类把volatile的语义扩展到了数组的元素访问中
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater,分别可以用来对声明为volatile的int类型、long类型、对象引用类型的变量进行修改。提供了一种方式把compareAndSet方法功能扩展到任何java类中声明的volatile域上。
public class TreeNode { private volatile TreeNode parent; private static final AtomicReferenceFieldUpdater<TreeNode, TreeNode> parentUpdater = AtomicReferenceFieldUpdater.newUpdater(TreeNode.class, TreeNode.class, "parent"); public boolean compareAndSetParent(TreeNode expect, TreeNode update) { return parentUpdater.compareAndSet(this, expect, update); } }