实战Java高并发程序设计-读书笔记

时间:2022-03-25 23:51:29

实战Java高并发程序设计-读书笔记

第一章

死锁、饥饿、活锁的概念。

并发级别:阻塞、饥饿、无障碍、无锁、无等待。

无障碍:是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。但是一旦检测到冲突,就应该进行回滚。

无锁:无锁的并行都是无障碍的,在无锁的情况下,所有的线程都能尝试对临界区进行访问,但是不同的是,无锁的迸发保证必然有一个线程能够在有限步内完成操作离开临界区。

while(!atomicVar.compareAndSet(localVar,localVar+1)){
    localVar = atomicVar.get();
}

无等待:要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。一种典型的无等待结构就是RCU(Read-Copy-Update)。它的基本思想是,对数据的读可以不加控制。因此,所有的读线程都是无等待的。但是在写数据的时候,先取得原始数据的副本,接着值修改副本数据,修改完成后,在合适的时机回写数据。

Amdah1定律

加速比定义: 加速比=优化前系统耗时/优化后系统耗时
实战Java高并发程序设计-读书笔记

Java的内存模型(JMM)

JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。

  • 原子性

原子性是指一个操作是不可中断的。即使在多个线程一起执行的时候。一个操作一旦开始,就不会被其他线程干扰。
比如对于32位系统来说,对long型数据的读写不是原子性的。对int型数据读写是原子性的。

  • 可见性

    可见性是指当一个线程修改了某一个共享变量的值,其他线程能否立即知道这个修改。可见性问题是一个综合性问题。除了缓存优化或者硬件优化(有些内存读写可能会立即出发,而会先进入一个硬件队列等待)会导致可见性问题外,指令重排以及编辑器的优化,都有可能导致一个线程的修改避讳立即被其他线程察觉。
    实战Java高并发程序设计-读书笔记

  • 有序性
    在并发时,程序的执行可能会出现乱序。给人直观的感觉是:写在前面的代码,会在后面执行。原因是程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。
    实战Java高并发程序设计-读书笔记

假设线程A首先执行write()方法,接着线程B执行reader()方法,如果发生指令重排,那么线程B在代码第10行时,不一定看到a已经被赋值为1了。
指令重排是有一个基本的前提的,就是保证串行语义的一致性。指令重排不会使串行的语义逻辑发生问题。

那些指令不能重排:Happen-Before规则

虽然Java虚拟机和执行系统会对指令进行一定的重排,但是指令重排是有原则的,并非所有的指令都可以随便改变位置,一下是一些基本原则。

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
  • 锁规则:解锁(unlock)必然发生于随后的加锁(lock)前
  • 传递性:A先与B,B先于C,那么A必然先于C
  • 线程的start()方法先于它的每个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数执行、结束先于finalize()方法

Java并行程序基础

继承Thread,继承Runable,继Callable,FutureTask等方式

线程状态图

实战Java高并发程序设计-读书笔记

2.2.1新建线程

2.2.2终止线程

线程的stop方法是被废弃而不推荐使用的,因为stop()方法太过暴力,强行把执行到一半的线程终止,可能会引发一些数据不一致的问题。

2.2.3线程中断

public void Thread.interrupt()  //中断线程
public boolean Thread.isInterrupted()  //判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态

实战Java高并发程序设计-读书笔记
实战Java高并发程序设计-读书笔记

2.2.4等待(wait)和通知(notify)

实战Java高并发程序设计-读书笔记


package com.alibaba.dubbo.consumer;

/** * {@code notify()} {@code wait()} 方法测试 * @author voidme */
public class NotifyTest {
    public final  static Object object=new Object();
    public  static class T1 extends Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println(System.currentTimeMillis()+":T1 start");
                System.out.println(System.currentTimeMillis()+": T1 wait for object");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis()+": T1 end");
            }
        }
    }

    public static class T2 extends Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println(System.currentTimeMillis()+":T2 start notify object");
                object.notify();
                System.out.println(System.currentTimeMillis()+":T2 end");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread t1=new T1();
        Thread t2=new T2();
        t2.start();
        t1.start();
    }
}

转自知乎用户 世界这么大,多走走挺好~的回答:
锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

相关衔接 锁池 等待池

然后再来说notify和notifyAll的区别

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争

优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。

有了这些理论基础,后面的notify可能会导致死锁,而notifyAll则不会的例子也就好解释了

2.2.5挂起(suspend)和继续执行(resume)线程

不推荐使用suspend方法的原因,是因为suspend()在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它暂用的锁时,都会被牵连,导致无法正常继续运行。直到对应的线程上进行了resume()操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是,如果resume()操作意外的在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上来看,居然还是Runnable,这也会严重影响我们队当前系统状态的判断。

2.2.6等待线程结束(join)和谦让(yied)

join的本质是让调用线程wait()在当前线程对象实例上。

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
while(isAlive()){
    wait(0);
}

它让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会退出前调用notifyAll()通知所有等待线程继续执行。因此,不要在Thread对象实例上使用类似wait()或者notify()等方法。

public static native void yied();

这是一个静态方法,一旦执行,它会使当前线程让出CPU。让出CPU并不表示当前线程不执行了。当前线程还是会进行CPU资源的争夺。但是能否再次被分配到,就不一定了。如果一个线程不是那么重要,或者优先级非常低,而且有害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield(),给与其他重要线程更多的工作机会。

2.3volatile与Java内存模型(JMM)

volate能保证写入操作的原子性。如

public volatile static long t=0;

但是volatile并不能替代锁,它无法保证一些复合操作的原子性。比如i++.

此外,volatile也能保证数据的可见性和有序性.

2.4 分门别类的管理:线程组

实战Java高并发程序设计-读书笔记
activeCount()可以获得活动线程总数,但是线程是动态的,这个值是一个估计值。list()方法可以打印这个线程组中所有的线程信息。

关于ThreadGroup使用注意两点:
1. 不要使用stop(),他会停止线程组中的所有线程。
2. 在创建线程组合线程的时候,给他们去一个有意义的名字。比如HttpHander、FTPService。

驻守后台:守护线程(Daemon)

守护线程是一种特殊的线程,就和它的名字一样,是系统的守护者,在后台完成一些系统的服务。