学习笔记之《深入理解Java虚拟机》---- 第五部分 高效并发

第12章 Java内存模型与线程

  • 在处理器中,为了使得处理器内部的运算单元尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。
  • 与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序优化
  • Java内存模型规定了所有的变量都存储在主内存中每条线程都有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量
  • 不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
  • Java内存模型中定义的8种原子操作:
  1. lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态
  2. unlock(解锁):作用与主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read(读取):作用域主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本
  5. use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  7. store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的放入主内存的变量中
  • 如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作;如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。Java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行
  • Java内存模型还规定了在执行上述8种原子操作时必须满足如下规则:
  1. 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现
  2. 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后把该变化同步回主内存
  3. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中
  4. 一个新的变量只能在主内存中“诞生”,不允许在工作内存中之间使用一个未被初始化(load和assign)的变量。即对一个变量进行use、store操作之前,必须先执行了assign和load操作
  5. 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁
  6. 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load和assign操作初始化变量的值(确保使用的是最新的)
  7. 如果一个变量事先没有被lock操作锁定,那就不允许对它进行unlock操作,也不允许去unlock一个被其他线程锁定住的变量
  8. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)
  • volatile 关键字作用:
  1. 保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量(未用volatile修饰的变量)的值在线程间传递均需通过主内存来完成
  2. 禁止指令重排序优化。
  • volatile 并不能保证线程安全:
package learningNotes;

import java.util.*;

public class Main {
	public static volatile int race = 0;
	public static void increase() {
		race++;
	}
	private static final int THREADS_COUNT = 20;
	public static void main(String[] args) {
		Thread[] threads = new Thread[THREADS_COUNT];
		for(int i = 0;i < THREADS_COUNT;i++) {
			threads[i] = new Thread(new Runnable() {
				@Override
				public void run() {
					for(int i = 0;i < 10000;i++) {
						increase();
					}
				}
			});
			threads[i].start();
		}
		while (Thread.activeCount() > 1) {
			Thread.yield();
		}
		System.out.println(race);
	}
}
  • 由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性:
  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  2. 变量不需要与其他的状态变量共同参与不变约束
  •  long和double的非原子性协定:允许虚拟机可以将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这四个操作的原子性
  • 如果有多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个既非原值,也不是其他线程修改的值的代表了“半个变量的数值”
  • 三个可以实现可见性的关键字:volatile、synchronized和final
  • 先行发生原则:先行发生是Java内存模型中定义的两项操作的偏序关系。如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等
  • Java内存模型的一些“天然的”先行发生关系:
  1. 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作(不应该指明是对同一个变量的操作吗???)。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构
  2. 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调是同一个锁,而“后面”是指时间上的先后顺序
  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序
  4. 线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作
  5. 线程终止规则:线程中所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行
  6. 线程中断规则:对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生
  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize方法的开始
  8. 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论
  • 一个操作“时间上的先发生”不代表这个操作会是先发生,一个操作“先行发生”也不代表这个操作是“时间上的先发生”
  • 实现线程的主要三种方式:
  1. 使用内核线程实现
  2. 使用用户线程实现
  3. 使用用户线程加轻量级进程混合实现,将内核线程和用户线程一起使用
  • 线程调度方式主要有两种:协同式线程调度和抢占式线程调度
  • Java中的5种线程状态:
  1. 新建(New)
  2. (可)运行(Runable):Runable包含了操作系统线程状态中的Running和Ready
  3. 等待(Waiting):等待分为有限期等待和无限期等待
  4. 阻塞(Blocked):阻塞与等待的区别是:阻塞状态在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而等待状态则是在等待一段时间或者唤醒动作的发生。当程序等待进入同步区域的时候,线程将进入阻塞状态
  5. 结束(Terminated)

第13章 线程安全与锁优化

  • 按照线程安全的“安全程度”由强至弱来排序,Java 语言中各种操作共享的数据分为以下5类:
  1. 不可变。由final修饰
  2. 绝对线程安全。即不管运行时环境如何,调用者都不需要任何额外的同步措施
  3. 相对线程安全。即通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施。在Java语言中,大部分的线程安全类都属于这种类型
  4. 线程兼容。线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。Java API中大部分的类都是属于线程兼容的
  5. 线程对立。线程对立是指无论调用端是否采取了同步措施,都无法再多线程环境中并发使用的代码。由于Java语言天生就具备多线程特性,线程对立这种排斥多线程的代码时很少出现的,而且通常都是有害的,应当尽量避免
  • 在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。如以下Vector类,即使它所有的方法都被修饰成同步,也不意味着调用它的时候永远都不再需要同步手段了
package learningNotes;

import java.util.*;

public class Main {
	private static Vector vector = new Vector<>();
	public static void main(String[] args) {
		while (true) {
			for(int i = 0;i<10;i++) {
				vector.add(i);
			}
			Thread removeThread = new Thread(new Runnable() {
				public void run() {
					for(int i = 0;i < vector.size();i++) {
						vector.remove(i);
					}
				}
			});
			Thread printThread = new Thread(new Runnable() {
				public void run() {
					for(int i = 0;i < vector.size();i++) {
						//System.out.println(vector.get(i));
						if(vector.get(i) > 9) System.out.println(vector.get(i)); 
					}
				}
			});
			removeThread.start();
			printThread.start();
			while (Thread.activeCount() > 20) ;
		}
	}
}
  • 针对上例,解决代码如下:
package learningNotes;

import java.util.*;

public class Main {
	private static Vector vector = new Vector<>();
	public static void main(String[] args) {
		while (true) {
			for(int i = 0;i<10;i++) {
				vector.add(i);
			}
			Thread removeThread = new Thread(new Runnable() {
				public void run() {
					synchronized (vector) {
						for(int i = 0;i < vector.size();i++) {
							vector.remove(i);
						}
					}
				}
			});
			Thread printThread = new Thread(new Runnable() {
				public void run() {
					synchronized (vector) {
						for(int i = 0;i < vector.size();i++) {
							//System.out.println(vector.get(i));
							if(vector.get(i) > 9) System.out.println(vector.get(i)); 
						}
					}
				}
			});
			removeThread.start();
			printThread.start();
			while (Thread.activeCount() > 20) ;
		}
	}
}
  • 互斥同步:
  1. 临界区互斥量信号量都是主要的互斥实现方式。最基本的互斥手段就是synchronized关键字。如果Java程序中的synchronized明确指定了对象参数,那么锁定的就是这个对象的引用;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象
  2. 除了synchronized之外,还可以使用java.util.concurrent包中的重入锁来实现同步。相比synchronized,ReentrantLock具备以下高级功能:等待可中断可实现公平锁以及锁可以绑定多个条件
  3.  等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
  4. 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但ReentrantLock可以通过带布尔值的构造函数要求使用公平锁
  5. 锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象
  • 非阻塞同步:
  1. 互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步
  2. 从处理问题的方式上说,互斥同步属于一种悲观的并发策略,即总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。而一种基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止)
  3. 这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步
  • 非阻塞同步之Atomic的原子自增运算
package learningNotes;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

import javax.sound.midi.Instrument;

public class Main {
	public static AtomicInteger race = new AtomicInteger(0);
	private static final int THREADS_COUNT = 20;
	public static void increase() {
		race.incrementAndGet();
	}
	public static void main(String[] args) {
		Thread[] threads = new Thread[THREADS_COUNT];
		for(int i = 0;i < THREADS_COUNT;i++) {
			threads[i] = new Thread(new Runnable() {
				public void run() {
					for(int i = 0;i < 10000;i++) {
						increase();
					}
				}
			});
			threads[i].start();
		}
		while (Thread.activeCount() > 1) {
			Thread.yield();
		}
		System.out.println(race);
	}
}
  • 无同步方案:
  1. 要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性
  2. 可重入代码:可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的
  3. 线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就尝试把共享数据的可见范围限制在同一个线程之内。
  • 锁优化:
  1. 自旋锁与自适应自旋:让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,只需让线程执行一个忙循环(自旋);自旋次数的默认值为10 。。而自适应自旋锁意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
  2. 锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸技术的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然无须进行
  3. 锁粗化:原则上,在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。如果虚拟机探测到一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
  4. 轻量级锁:本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁
  5. 偏向锁:它的目的是消除数据在无竞争情况下的同步原语。它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步

 

你可能感兴趣的:(Java虚拟机,学习笔记)