《Java多线程编程核心技术》读书笔记

323 阅读13分钟

什么是线程

  • 线程和进程的区别?

  • 为什么使用多线程

    • 充分利用CPU的多核计算能力
    • 方便进行业务拆分,提升应用性能

    缺点:

    • 频繁切换上下文
    • 线程安全问题、死锁等

创建线程的几种方式及特点

  • 继承Thread类,覆盖run()方法
Thread th1 = new MyThread();
th1.start();
class MyThread extends Thread{
	@Override
	public void run() {
		System.out.println("thread run...");
	}
}
  • 实现Runnable接口
Thread th2 = new Thread(new Runnable() {			
	@Override
	public void run() {
		System.out.println("thread run...");				
	}
});
th2.start();
  • 实现Callable接口
Callable<String> callable = new Callable<String>() {
	@Override
	public String call() throws Exception {
		System.out.println("thread run...");
		return "ok";
	}};
FutureTask<String> futureTask = new FutureTask<String>(callable);
Thread th3 = new Thread(futureTask);
th3.start();
// 获取执行结果
String result = futureTask.get();

Thread常用方法

  • currentThread():获取当前线程
  • isAlive():线程是否处于活动状态,活动状态指线程已经启动且尚未终止
  • getId():获取线程的唯一标识
  • setPriority():设置线程的优先级,1-10
    • main线程启动A线程,则A线程优先级和main线程一样
    • B线程继承A线程,B线程优先级和A线程一样
  • setDaemon():当进程中不存在非守护线程,守护线程自动销毁
  • join():当主线程想等子线程执行完后再结束,可以使用join方法,执行th.join()即将th线程加入,主线程需等th线程执行完后再结束
  • yield():放弃当前CPU资源。线程A执行yield后,让出CPU并进入可运行状态。让出的时间片只会分配给当前线程相同优先级的线程
  • sleep():当前线程休眠
  • sleep()和wait()的区别:
    • sleep是Thread的静态方法,wait是object实例的方法
    • wait必需在同步块中调用,wait会释放锁,被notify唤醒后进入blocked阻塞状态,需要获取锁后进入可运行状态。而线程sleep结束后直接进入可运行状态。

线程状态

  • New:线程start()之前,处于新建状态
  • Runnable:Runnable包括就绪状态Ready和运行状态Running
  • Blocked:阻塞状态,等待锁
  • Waiting:无限期等待,线程调用了wait()后处于无限期等待状态,唤醒后进入Blocked状态
  • Timed_Waiting:限期等待,线程调用了sleep/wait(1000)/join(1000)处于限期等待状态,方法结束之后回到Runnable状态
  • Terminated:线程结束,已终止的线程状态

停止线程

  • interrupt:结合interrupted()使用,并将异常抛出,使线程停止事件得以传播,参考1.7.3
    • interrupted():测试当前线程是否已经中断,指执行thread.interrupted()方法的线程,会清除状态,即执行两次第一次为true第二次为false
    • isInterrupted():测试线程是否已经中断,thread.isInterrupted()指thread这个线程,不会清除状态
    • 不同状态下的线程对interrupted()的反映
      • RUNNABLE状态:如果线程在运行中,interrupt()只是会设置线程的中断标志位,没有任何其它作用。线程应该在运行过程中合适的位置检查中断标志位,如果主体代码是一个循环,可以在循环开始处进行检查,如下所示:
        public class InterruptRunnableDemo extends Thread {
           @Override
            public void run() {
                while (!Thread.currentThread().isInterrupted()) {
                    // ... 单次循环代码
                }
                System.out.println("done ");
            }
        }
        
      • WAITING/TIMED_WAITING状态:对线程对象调用interrupt()会使得该线程抛出InterruptedException,抛出异常后,中断标志位会被清空(线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态,下面代码表现为输出run end.
        public class InterruptRunnableDemo extends Thread {
        	@Override
        	public void run() {
        		System.out.println("run begin.");
        		try{
        			Thread.sleep(10000);
        		}catch(InterruptedException e){
        			 System.out.println("run 在沉睡中被终止,进入catch.");
        			 e.printStackTrace();
        		 }	
        		System.out.println("run end.");
        	}
        }
        
        • 异常如何处理:
          • 抛出去,让调用方处理。
          • 重设中断标志位为true,Thread.currentThread().interrupt()。
      • BLOCKED状态:如果线程在等待锁,对线程对象调用interrupt()只是会设置线程的中断标志位,线程依然会处于BLOCKED状态,也就是说,interrupt()并不能使一个在等待锁的线程真正”中断”。在使用synchronized关键字获取锁的过程中不响应中断请求,这是synchronized的局限性。如果这对程序是一个问题,应该使用显式锁,java中的Lock接口,它支持以响应中断的方式获取锁。对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异常。
      • NEW/TERMINATE状态:如果线程尚未启动(NEW),或者已经结束(TERMINATED),则调用interrupt()对它没有任何效果,中断标志位也不会被设置。
  • stop():已废弃,不建议使用interrupted()
    • 缺点一:强制停止线程可能使一些重要的工作没有完成,即子线程run方法代码未全部执行完
    • 缺点二:破坏原子逻辑:线程run方法代码,如a++,sleep(1000),a--;在sleep时stop,其他线程拿到锁,直接对a++之后的a进行操作,破坏了原子逻辑

Synchronized关键字

  • 修饰普通方法,锁为实例对象,等价于synchronized(this)。不同实例访问同一个加锁的方法时,不会相互影响。
  • 修饰代码块,锁可以是任意对象,synchronized(obj)
  • 修饰静态方法,锁为类对象,等价于synchronized(xxx.class)。项目里面锁住静态的logger变量,由于静态变量只有一份,所以是类锁。

Lock

  • ReentrantLock,比synchronized更加灵活
    • 掌握对象监视器condition的await(),signal(),signalAll()方法的使用
    • 公平锁和非公平锁,ReentrantLock(boolean fair)
      • 公平锁:线程获取锁的顺序按照线程加锁的顺序来分配,即先进先出的顺序。
      • 非公平锁:随机获取锁,这种方式下某些线程可能一直拿不到锁。
  • ReentrantReadWriteLock,读写锁
    • 读锁:读锁拒绝其他线程获得写锁,不拒绝其他线程获得读锁,多个上了读锁的线程可以并发读不会阻塞。
    • 写锁:写锁拒绝其他线程获取读锁和写锁。
  • Synchronized和ReentrantLock的区别
    • Synchronized是Java关键字,ReentrantLock是JDK提供的API
    • 使用上synchronized可以修饰方法和代码块,不用手动释放锁。ReentrantLock只能修饰代码块,必须手动释放锁。
    • Synchronized只提供非公平锁,ReentrantLock更加灵活,
      • 可实现公平锁:ReentrantLock(boolean fair)
      • 可实现选择性通知(锁可以绑定多个condition)
      • 等待可中断:通过lock.lockInterruptibly()

Java内存模型

  • 对于Java内存模型,《深入理解Java虚拟机》是这样描述的:

Java内存模型规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都要在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量。线程间变量值得传递均需要通过主内存来完成。

  • 数据不一致问题:一个线程在主存中修改了变量的值,而另外一个线程还继续使用它工作内存变量的拷贝,造成数据的不一致。
  • volatile关键字,怎么保证可见性的? 被volatile修饰的变量,每个写操作之后,都会加入一条store内存屏障命令,强制工作内存将此变量的最新值保存至主内存;每个读操作之前,都会加入一条load内存命令,强制工作内存从主内存中加载此变量的最新值至工作内存。

volatile关键字

  • volatile可以保证可见性和有序性,不能保证原子性。synchronized和Lock可以保证原子性、可见性以及有序性。在某些情况下volatile将提供优于锁的性能和伸缩性。
  • volatile
    • 可见性:使用volatile可以确保各个线程每次都从主存(物理内存)中读取变量,而不是从线程私有内存空间(CPU高速缓存)读取缓存值
    • 原子性:volatile不能保证变量的原子性,分析:线程A从主存读取变量x(x=1),进行x++操作(x=2),再将x回写到主存,这个过程中线程B也从主存读取变量x,读到的x=1。使用原子类AtomicInteger定义变量可以保证共享变量的原子性
    • 有序性:程序执行的顺序按照代码的先后顺序执行,指令重排序不会影响单个线程的执行,但会影响到线程并发执行的正确性,而使用volatile可以保证有序性

      指令重排序:正常情况下,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的

  • synchronized、Lock
    • 可见性:synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性。
    • 原子性:其他线程被阻塞了,只有持有锁的线程可以操作,所以可以保证原子性。
    • 有序性:synchronized和Lock保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
  • 要使 volatile变量提供理想的线程安全,在满足下面两个条件情况下,可以使用 volatile 代替 synchronized:
    • 对变量的写操作不依赖于当前值(x++这种不满足)
    • 该变量没有包含在具有其他变量的不变式中(x<y这种不满足)。
  • 使用场景:
    • 状态标志isfd.privilege.notify.consumer.AbstractEventConsumer
    • 单例模式,由于指令重排序导致双重检查锁定被破坏,使用volatile可以解决
    • 更多使用场景:blog.csdn.net/vking_wang/…
  • volatile详解:www.cnblogs.com/dolphin0520…

线程间通信

死锁

ThreadLocal

  • Thread:定义了变量threadLocals,类型为ThreadLocal.ThreadLocalMap,用于存放线程变量,其他线程无法访问
  • ThreadLocal:定义了内部类ThreadLocalMap,提供线程变量的get,set,remove方法
  • ThreadLocalMap:定义了内部类Entry
  • Entry:继承了WeakReference,类型为ThreadLocal的key为弱引用
  • 内存泄露
    • 什么情况下会发生内存泄露:当线程一直不结束(线程池),ThreadLocalMap的数据一直不会被回收,而且不手动移除key,就可能导致内存泄露。
    • JDK设计上如何预防大多数内存泄露情况:将ThreadLocal.ThreadLocalMap.Entry的key,即ThreadLocal设计为弱引用,弱引用只能存活到下一次垃圾回收,垃圾回收后key为null,value在下一次ThreadLocal调用set,get,remove时会被清除。
    • 开发人员使用时如何避免内存泄露:在使用完ThreadLocal时,及时调用它的remove方法清除数据。
    • 内存泄露分析详解:
  • 应用:
    • spring事务管理
    • 项目使用:isfd-core单点登录、upm存储待发送的mq消息,将通知数据set到ThreadLocal对象中,等事务提交后再从ThreadLocal对象getAndRemove出来,进行发通知操作。

线程池

  • 为什么要使用线程池 《Java并发编程的艺术》提到的使用线程池的好处如下:
    • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 线程池的参数

锁优化

  • 自旋锁:是乐观锁,认为共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程不值得,可以让后面请求锁的线程执行一个忙循环(自旋),不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。

  • 自适应自旋:自旋的时间不是固定的,如果是自旋等待刚刚成功获得过的锁,虚拟机就会认为这次自旋很可能再次成功,运行自旋等待持续更长的时间,比如100个循环。如果对于某个锁很少自旋成功获得,那以后要获取这个锁可能省掉自旋过程。

  • 锁消除:虚拟机检测到不可能存在共享数据竞争的所进行消除,StringBufffer.append()方法中都有一个同步块,锁就是buffer对象,这里的锁可以被安全地消除掉。

    public String contactString(String s1, String s2, String s3) {
    	StringBuffer buffer = new StringBuffer();
    	buffer.append(s1);
    	buffer.append(s2);
    	buffer.append(s3);
    	return buffer.toString();
    }
    
  • 锁粗化:一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中,虚拟机会把加锁同步的范围粗化到整个操作序列的外部。

  • 轻量锁:轻量级锁是在无竞争情况下使用CAS消除同步使用的互斥量。 要理解轻量级锁、偏向锁,需要先了解HotSpot虚拟机对象头Mark Word,其存储内容如下:

    在代码进入同步块的时候,如果同步对象没有被锁定,锁标志位状态为“01”,虚拟机将在当前线程的栈帧中建立一个锁记录(Lock Record)空间,存储锁对象目前的Mark Word的拷贝(Displaced Mark Word),如下图所示:
    虚拟机使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果更新成功,那么这个线程就拥有了该对象的锁,随着的Mark Word的锁标志位转变为“00”,表示对象处于轻量锁定的状态,这时线程堆栈和对象头状态如下图所示:
    解锁过程就是使用CAS将对象当前的Mark Word和线程栈帧中复制的Displaced Mark Word替换回来,如果替换失败,说明有其他线程尝试获取过该锁,需要在释放锁的同时唤醒被挂起的线程。

  • 偏向锁

    • 偏向锁是在无竞争的情况下把同步消除。
    • 锁对象第一次被线程获取时,虚拟机把对象头的状态标志位设为“01”,同时使用CAS操作把获取到锁的线程ID记录到对象的Mark Word中,如果操作成功,持有偏向锁的线程以后每次进入这个锁的同步代码块时,虚拟机可以不再进行任何同步操作。
    • 当另一个线程尝试获取锁时,偏向模式结束,如果当前线程仍然处于锁定状态,将会升级为轻量级锁。
  • 锁主要存在4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争的情况逐渐升级,这几个锁只有重量级锁是需要使用操作系统底层mutex互斥原语来实现,其他的锁都是使用对象头来实现的。需要注意锁可以升级,但是不可以降级。锁的膨胀过程可以通过下面这张图表示: