《Java并发编程实战》读书笔记一:线程安全性

为什么需要多线程

充分利用CPU资源

  • 在多处理器/多核系统上 单线程程序只能使用一个CPU资源,而多线程程序可以同时在多个处理器上/不同核心上同时执行
  • 在某个线程被IO阻塞时 可以切换到其他线程继续执行任务,不浪费CPU执行时间

但多线程不是银弹,当CPU核数小于系统线程数时必然会发生线程的切换,线程上下文的切换也存在较大的开销,一个进程开启过多的线程导致线程频繁切换可能会得不偿失。

插曲:某些应用场景
例如,Remote Method Invocation 远程方法调用
简单对比一下RMI和RPC的区别
RPC更为灵活,使用NIO模型,是一种网络服务协议,与操作系统和实现语言无关,返回的是外部数据表示(例如json);而RMI只用于Java,使用的是BIO,返回的是对象类型或者基本数据类型。

线程安全性

共享和可变

线程安全问题是由于同一个进程内不同线程存在共享内存区域,而线程对其进行的修改操作可能会相互影响,导致了程序的执行结果与我们所预期的出现了不一致,这就是线程安全问题

  • 共享意味着变量可以被多个线程同时访问 -->加锁/同步机制打破
  • 可变意味着变量的值在其生命周期内可以变化 --> final修饰打破

当程序的内存空间同时存在共享和可变条件时,我们称程序是非线性安全的。

线程安全类

当多个线程访问某个类时,无需在外部代码中添加额外的同步机制,这个类都可以表现出正确的行为(或者说不会产生不确定的结果),那么这个类就是线程安全的。

断言:无状态的对象一定是线程安全的 √
理由:等同于打破了前面所提的可变条件,如果对象没有状态等同于对象不可变,多线程下对该对象的任意操作都不会改变该对象的状态,每个线程访问这个对象的获得的状态都是一致的,所以是线程安全的

竞态条件

竞态条件指的是 多个线程运行输出的结果取决于它们的交替执行时序 这样一个现象,也就是说竞态条件指要获得正确的结果 必须取决于线程执行的先后交替顺序

以单例模式的懒加载为例

public class Singleton{
	private static Singleton instance = null;

	private Singleton(){}

	public static Singleton getInstance(){
		if(instance == null){
			instance = new Singleton();
		}
		return instance;
	}
}

在这里存在竞态条件,假如有线程a和b同时调用getInstance(),A看到instance为空然后实例化Singleton,然而线程b调用的时机不可预测,假如b看到instance仍然为空它也会去生成额外一个Singleton,可能造成更新丢失等错误(A的Singleton被B覆盖);假如b判断instance==null时a已完成实例化,就会返回实例化好的单例。

双重检查锁的修改方式是

public class Singleton{
	private volatile static Singleton instance = null;

	private Singleton(){}

	public static Singleton getInstance(){
		if(instance == null){
			synchronized(Singleton.class){
				if(instance==null)
					instance = new Singleton();
			}
		}
		return instance;
	}
}
  • 第一次判空的作用是降低锁的粒度,只有未初始化单例的线程才会走入同步代码,当单例已初始化好以后直接返回实例
  • 第二次判空的作用避免反复实例化(类似上面的问题)
  • volatile作用禁止指令重排序,instance = new Singleton不是一个原子操作,它的步骤有1、分配内存,2、调用构造器初始化成员变量,3、将实例化的对象指向分配好的内存空间; 当线程a进入同步代码块执行时指令顺序可能会被重排为132,返回一个未初始化的Singleton

加锁机制

内置锁

Java提供了一种内置的锁机制:同步代码块(Synchronized Block),它的形式如下:

synchronized(lock){//锁的对象引用
	//由这个锁 保护的代码块
	···
}

synchronized关键字直接修饰方法的时候就说一个横跨整个方法体的同步代码块,该代码块的锁对象就是该方法调用所在的对象实例,静态的synchronized方法的锁对象以Class对象为锁;

class A{
	synchronized methodB(){}
} 
A a = new A();
a.methodB(); //这里锁的对象就是a

每个Java对象都可以作为一个实现同步的锁,这被称为内置锁或监视器锁;线程在进入同步代码块之前自动获得锁,退出时自动释放锁;并且它是一种互斥锁,最多只有一个线程能持有它。当线程a尝试获得一个线程b拥有的锁时,a必须自旋等待或阻塞,直到b释放这个锁;

重入锁

ReentrantLock可重入锁,也可以理解为可递归锁;它的意思是说,允许同一个线程多次获取同一把锁。我们知道当某个线程请求一个由其他线程持有的锁,发出请求的线程会等待或阻塞,当某个线程请求一个由它自己拥有的锁时,如果这个操作能够成功,就叫可重入锁,否则是不可重入锁;

比如一个递归函数里有加锁操作,递归过程中对于可重入锁它可以申请成功获取到锁,而不可重入锁在这种情况下则会陷入死锁。因为这个原因可重入锁也叫做可递归锁。

使用锁的时候要注意锁的粒度,特别要尽量避免IO过程中持有锁的行为。