<实战java高并发>自记笔记

1.java的jmm关键技术:多线程的原子性、可见性、有序性。
原子性:是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
对于32位系统的long型(64位)数据就不是原子性的。
可见性:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。
在一个线程中观察另外一个线程的变量,它们的值是否观测到、何时能观测到是没有保证的。因为有指令重排。
有序性:在程序并发时,程序的执行顺序可能会出现乱序。有效性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。对于一个线程来说,它看到的指令执行顺序一定是一致的(否则的话我们的应用根本无法正常工作)
指令重排是因为性能考虑。尽量少的中断流水线。

Java Memory Model (JAVA 内存模型)描述线程之间如何通过内存(memory)来进行交互。 具体说来, JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。

2.
Thread线程的start()方法会创建一个线程并让这个线程执行run()方法。

3.happens-before关系
什么是happens-before关系? 这个关系其实就是一个保证而已,那么保证什么呢?它保证一条语句对内存的写操作对另一条语句是可见的。换句话说,如果写操作A和读操作B存在happens-before这种关系,那么写操作在结束以后都操作才能开始。
下面是Java内存模型中的八条可保证happen—before的规则,它们无需任何同步器协助就已经存在,可以在编码中直接使用。
1、程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
2、管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
4、线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
5、线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
6、线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
7、对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
8、传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
这里的八个规则除了第三个以外都容易理解。所以专门讲一下volatile变量规则。
1、 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
2、 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令。
通俗得讲,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存。这样任何时刻,不同的线程总是能够看到该变量的最新值。
如果你不能够理解,我们可以采取一种极端的思维方式:如果有两个线程的话,对于一个普通变量,在java内存模型中它是有三个拷贝的,一个在主内存,另外两个在线程的工作内存里。如果刷新不及时,那么就可能导致两个工作内存中的变量值不一致。但是对于volatile变量,你完全可以假定其只有一份而且唯一一份拷贝在主内存中发生,所以当两个线程想对volatile变量进行更改或者读取的时候,总是得等其中一个线程完成以后才行。

Java语言中有一个“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。

JMM关于synchronized的两条规定:

  1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
  2. 线程加锁时,讲清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
    这样,线程解锁前对共享变量的修改在下次加锁时对其他线程可见。

  3. Thread的stop方法会立即释放该线程所持有的锁,而这些锁恰恰是用来维持对象一致性的。如果此时写线程写入数据正写到一半,并强行终止,那么对象就会被破坏,同时由于所已经被释放,另外一个等待该所的线程就会读到这个不一致的对象。
    太过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。单线程不会出现该问题。

5.Java中Wait、Sleep和Yield方法的区别
sleep()和yield()方法是定义在Thread类中,而wait()方法是定义在Object类中的, 这也是面试中常问的一个问题。
wait()和sleep()的关键的区别在于,wait()是用于线程间通信的,而sleep()是用于短时间暂停当前线程。更加明显的一个区别在于,当一个线程调用wait()方法的时候,会释放它锁持有的对象的管程和锁,但是调用sleep()方法的时候,不会释放他所持有的管程。
下面列出Java中wait和sleep方法的区别:
wait只能在同步(synchronize)环境中被调用,而sleep不需要。详见Why to wait and notify needs to call from synchronized method
进入wait状态的线程能够被notify和notifyAll线程唤醒,但是进入sleeping状态的线程不能被notify方法唤醒。
wait通常有条件地执行,线程会一直处于wait状态,直到某个条件变为真。但是sleep仅仅让你的线程进入睡眠状态。方法在进入wait状态的时候会释放对象的锁,但是sleep方法不会。
wait方法是针对一个被同步代码块加锁的对象,而sleep是针对一个线程。更详细的讲解可以参考《Java核心技术卷1》,里面介绍了如何使用wait和notify方法。
Thread.sleep()方法是一个静态方法,作用在当前线程上.wait方法是一个实例方法,并且只能在其他线程调用本实例的notify()方法时被唤醒。
使用sleep方法时,被暂停的线程在被唤醒之后会立即进入就绪态(Runnable state),但是使用wait方法的时候,被暂停的线程会首先获得锁(译者注:阻塞态),然后再进入就绪态。所以,如果需要暂定你的线程一段特定的时间就使用sleep()方法,如果你想要实现线程间通信就使用wait()方法。

yield()方法上来,与wait()和sleep()方法有一些区别,它仅仅释放线程所占有的CPU资源,从而让其他线程有机会运行,但是并不能保证某个特定的线程能够获得CPU资源。谁能获得CPU完全取决于调度器,在有些情况下调用yield方法的线程甚至会再次得到CPU资源。所以,依赖于yield方法是不可靠的,它只能尽力而为。

yield和sleep的区别
yield和sleep的主要是,yield方法会临时暂停当前正在执行的线程,来让有同样优先级的正在等待的线程有机会执行。如果没有正在等待的线程,或者所有正在等待的线程的优先级都比较低,那么该线程会继续运行。执行了yield方法的线程什么时候会继续运行由线程调度器来决定,不同的厂商可能有不同的行为。yield方法不保证当前的线程会暂停或者停止,但是可以保证当前线程在调用yield方法时会放弃CPU。

Java中sleep方法的几个注意点:
Thread.sleep()方法用来暂停线程的执行,将CPU放给线程调度器。
Thread.sleep()方法是一个静态方法,它暂停的是当前执行的线程。
Java有两种sleep方法,一个只有一个毫秒参数,另一个有毫秒和纳秒两个参数。
与wait方法不同,sleep方法不会释放锁
如果其他的线程中断了一个休眠的线程,sleep方法会抛出Interrupted Exception。
休眠的线程在唤醒之后不保证能获取到CPU,它会先进入就绪态,与其他线程竞争CPU。
有一个易错的地方,当调用t.sleep()的时候,会暂停线程t。这是不对的,因为Thread.sleep是一个静态方法,它会使当前线程而不是线程t进入休眠状态。
这就是java中的sleep方法。我们已经看到了java中sleep、wait以及yield方法的区别。
总之,记住sleep和yield作用于当前线程。

6.Thread.join详解
为什么要用join()方法
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了
join方法的作用
JDK中对join方法解释为:“等待该线程终止”,换句话说就是:”当前线程等待子线程的终止“。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了当前线程才能执行。
主要作用是同步,它可以使得线程之间的并行执行变为串行执行。在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。
join()方法中如果传入参数,则表示这样的意思:如果A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。
join方法的原理就是调用相应线程的wait方法进行等待操作的,例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,当B线程执行完(或者到达等待时间),B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。

6.线程中断
严格地讲,线程中断不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出。至于目标线程接到通过后何时退出,完全由目标线程自行决定。如果立即退出,又会出现stop问题。
interrupt 中断线程。目标线程中断,设置中断标识位。
isInterrupted 是否被中断。目标对象检查中断标识位。
static interrupted 判断是否被中断,并清除当前线程中断状态标识。
Thread.sleep()方法由于中断而抛出异常,此时,它会清楚中断标识。如果不加处理,那么在下一次循环开始时,就无法捕获到这个中断。所以在异常处理中,需要再次设置中断标识。

7.等待wait和通知notify
这两个方法都是在object上的,所以任何对象都可以调用这两个方法。
当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。比如线程A中,调用了obj.wait()方法,那么线程A就会停止继续执行,而转为等待状态。等待何时结束?线程A会一直等到其他线程调用了obj.notify方法为止。这时,obj对象就成为了多个线程之前的有效通信手段。
wait和notify的工作原理:如果一个线程调用了obj.wait,那么它就会进入obj对象的等待队列。这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当obj.notify被调用时,它就会等待队列中随机选择一个,将其唤醒。这个选择是不公平的,不是先等待的就先被唤醒,而是随机的选择。notifyall方法是唤醒所有等待的线程。
wait和notify必须在synchronzied中,必须先获取到目标对象的一个监视器。

8.挂起suspend和继续执行resume
这两个方法都是不推荐的,都是废弃方法。
suspend不推荐原因:该方法在导致线程暂停的同时,并不会释放任何锁资源。此刻其他任何线程想要访问被它暂用的锁时,都会被牵连,导致无法正常继续运行。直到对应的线程上进行了resume操作,被挂起的线程才能够继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是如果resume操作意外地在suspend前就执行了,那么被挂起的线程可能很难有机会继续被执行。更严重的是,它所占用的锁都不会释放,可能导致整个系统无法使用,而且对于被挂起的线程,从它的状态上看,还是runnable,影响对状态的判断。
suspend方法导致线程进入类似死锁的状态。

9.JIT编译器,英文写作Just-In-Time Compiler,中文意思是即时编译器

猜你喜欢

转载自blog.51cto.com/10884109/2315760