1.类的线程安全定义
如果多线程下使用这个类,不管多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的。
类的线程安全表现为:
- 操作的原子性
- 内存的可见性
不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。
2.怎么才能做到类的线程安全?
2.1 栈封闭
所有的变量都是在方法内部声明和使用,这些变量都处于栈封闭状态。
2.2 无状态
没有任何成员变量的类,就叫无状态的类
public class StatelessClass {
public int service(int a,int b) {
return a*b;
}
//...public void t(){}
}
2.3 让类不可变
Java中有:
- String
- 包装类
- LoacalDateTime
让状态不可变,两种方式:
- 1)加final关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
public class ImmutableFinal {
private final int a;
private final int b;
public ImmutableFinal(int a, int b) {
super();
this.a = a;
this.b = b;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
}
如下就不能保证安全了!
public class ImmutableFinalRef {
private final int a;
private final int b;
private final User user;//这里,就不能保证线程安全啦
public ImmutableFinalRef(int a, int b) {
super();
this.a = a;
this.b = b;
this.user = new User(2);
}
public int getA() {
return a;
}
public int getB() {
return b;
}
public User getUser() {
return user;
}
public static class User{
private int age;
public User(int age) {
super();
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public static void main(String[] args) {
ImmutableFinalRef ref = new ImmutableFinalRef(12,23);
User u = ref.getUser();
//u.setAge(35);
}
}
- 2)根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值
public class ImmutetableToo {
private List<Integer> list = new ArrayList<>(3);
public ImmutetableToo() {
list.add(1);
list.add(2);
list.add(3);
}
public boolean isContains(int i) {
return list.contains(i);
}
}
另外参考:AKKA
2.4 Volatile
保证类的可见性,最适合一个线程写,多个线程读的情景。
如果有多个线程写,可以使用锁(如synchronized)等效于一个线程写,ConcurrentHashMap就是这么做的。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
2.5 加锁和CAS
锁等问题请参考《现代操作系统》——第2章 2.3 进程间通信
2.6 安全的发布
类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。
方法:
- 用线程安全的容器替换
- 返回副本,深拷贝
- 要么加锁
2.7 TheadLocal
2.8 关于Servlet(生命周期;是否安全)
不是线程安全的类。
为什么我们平时没感觉到:
- 1)在需求上(http请求),很少有共享的需求
- 2)接收到了请求,返回应答的时候,都是由一个线程来负责的
3.线程不安全引发的问题
死锁等问题请参考《现代操作系统》——第6章 死锁
3.1 死锁
资源一定是多于1个,同时小于等于竞争的线程数,资源只有一个,只会产生激烈的竞争。
死锁的根本成因:获取锁的顺序不一致导致。
3.1.1 普通的死锁
private static void fisrtToSecond() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueFirst) {
System.out.println(threadName+" get first");
SleepTools.ms(100);
synchronized (valueSecond) {
System.out.println(threadName+" get second");
}
}
}
//先拿第二个锁,再拿第一个锁
private static void SecondToFisrt() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueSecond) {
System.out.println(threadName+" get first");
SleepTools.ms(100);
synchronized (valueFirst) {
System.out.println(threadName+" get second");
}
}
}
检查死锁方法:
- 通过jps 查询进程 id
- 再通过jstack id 查看应用的锁的持有情况
解决办法:保证加锁的顺序性
3.1.2 动态顺序死锁
动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的。
活锁产生TransferAccount.java
public static void main(String[] args) {
PayCompany payCompany = new PayCompany();
UserAccount zhangsan = new UserAccount("zhangsan",20000);
UserAccount lisi = new UserAccount("lisi",20000);
// ITransfer transfer = new TransferAccount();
// ITransfer transfer = new SafeOperate();
ITransfer transfer = new SafeOperateToo();
TransferThread zhangsanToLisi = new TransferThread("zhangsanToLisi"
,zhangsan,lisi,2000,transfer);
TransferThread lisiToZhangsan = new TransferThread("lisiToZhangsan"
,lisi,zhangsan,4000,transfer);
zhangsanToLisi.start();
lisiToZhangsan.start();
}
public class TransferAccount implements ITransfer {
@Override
public void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
synchronized (from){//先锁转出
System.out.println(Thread.currentThread().getName()
+" get"+from.getName());
Thread.sleep(100);
synchronized (to){//再锁转入
System.out.println(Thread.currentThread().getName()
+" get"+to.getName());
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
}
解决方法:
- 1)通过内在排序,保证加锁的顺序性(identityHashCode原生的hashhode,按照这个的顺序制定加锁顺序;如果哈希值相等,使用tieLock);
可以使用任何唯一化的ID。
统一加锁顺序SafeOperate.java
public class SafeOperate implements ITransfer {
private static Object tieLock = new Object();//加时赛锁
@Override
public void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
//先锁hash小的那个
if(fromHash<toHash) {
synchronized (from){
System.out.println(Thread.currentThread().getName()
+" get"+from.getName());
Thread.sleep(100);
synchronized (to){
System.out.println(Thread.currentThread().getName()
+" get"+to.getName());
from.flyMoney(amount);
to.addMoney(amount);
}
}
}else if(toHash<fromHash) {
synchronized (to){
System.out.println(Thread.currentThread().getName()
+" get"+to.getName());
Thread.sleep(100);
synchronized (from){
System.out.println(Thread.currentThread().getName()
+" get"+from.getName());
from.flyMoney(amount);
to.addMoney(amount);
}
}
}else {//解决hash冲突的方法
synchronized (tieLock) {
synchronized (from) {
synchronized (to) {
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
}
}
}
- 2)通过尝试拿锁,也可以。拿锁失败以后,休眠随机数,以避免活锁。
尝试拿锁随机休眠SafeOperateToo.java
public class SafeOperateToo implements ITransfer {
@Override
public void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
Random r = new Random();
while(true) {
if(from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName()
+" get "+from.getName());
if(to.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName()
+" get "+to.getName());
//两把锁都拿到了
from.flyMoney(amount);
to.addMoney(amount);
break;
}finally {
to.getLock().unlock();
}
}
}finally {
from.getLock().unlock();
}
}
SleepTools.ms(r.nextInt(10));
}
}
}
3.2 活锁
尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。
没有那个随机休眠,就会产生不同程度的活锁!
public class SafeOperateToo implements ITransfer {
@Override
public void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
Random r = new Random();
while(true) {
if(from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName()
+" get "+from.getName());
if(to.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName()
+" get "+to.getName());
//两把锁都拿到了
from.flyMoney(amount);
to.addMoney(amount);
break;
}finally {
to.getLock().unlock();
}
}
}finally {
from.getLock().unlock();
}
}
//SleepTools.ms(r.nextInt(10));
}
}
}
解决办法:每个线程休眠随机数,错开拿锁的时间。
3.3 线程饥饿
低优先级的线程,总是拿不到执行时间
4.性能和思考
4.1 衡量指标
线程调度请参考《现代操作系统》——第2章 2.4节 调度
使用并发的目标是为了提高性能,引入多线程后,其实会引入额外的开销,如线程之间的协调、增加的上下文切换,线程的创建和销毁,线程的调度等等。过度的使用和不恰当的使用,会导致多线程程序甚至比单线程还要低。
衡量应用的程序的性能:服务时间,延迟时间,吞吐量,可伸缩性等等,其中服务时间,延迟时间(多快),吞吐量(处理能力的指标,完成工作的多少)。多快和多少,相对独立,甚至是相互矛盾的。
对服务器应用来说:多少(可伸缩性,吞吐量)这个方面比多快更受重视。
我们做应用的时候:
1、 先保证程序正确,确实达不到要求的时候,再提高速度。(黄金原则)
2、 一定要以测试为基准。
一个应用程序里,串行的部分是永远都有的。
Amdahl定律 : 1/(F+(1-N)/N) F:必须被串行部分,程序最好的结果, 1/F。
多线程算法请参考《算法导论》——第27章 多线程算法
4.2 影响性能的因素
4.2.1 上下文切换(合理分配线程数)
是指CPU 从一个进程或线程切换到另一个进程或线程。一次上下文切换花费5000~10000个时钟周期,几微秒。在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
4.2.2 内存同步(减少锁的竞争)
一般指加锁,对加锁来说,需要增加额外的指令,这些指令都需要刷新缓存等等操作。
4.2.3 阻塞(减少锁的竞争)
会导致线程挂起【挂起:挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作】。很明显这个操作包括两次额外的上下文切换。
4.3 提高性能的方法
4.3.1 减少锁的竞争
4.3.2 减少锁的粒度
使用锁的时候,锁所保护的对象是多个,当这些多个对象其实是独立变化的时候,不如用多个锁来一一保护这些对象。但是如果有同时要持有多个锁的业务方法,要注意避免发生死锁
public class FinenessLock {
public final Set<String> users = new HashSet<String>();
public final Set<String> queries = new HashSet<String>();
public void addUser(String u) {
synchronized (users) {//只对使用的对象加锁
users.add(u);
}
}
public void addQuery(String q) {
synchronized (users) {//只对使用的对象加锁
queries.add(q);
}
}
}
4.3.3 缩小锁的范围
对锁的持有实现快进快出,尽量缩短持由锁的的时间。将一些与锁无关的代码移出锁的范围,特别是一些耗时,可能阻塞的操作
private Map<String,String> matchMap = new HashMap<>();
public synchronized boolean isMatch(String name,String regexp) {
String key = "user."+name;
String job = matchMap.get(key);
if(job == null) {
return false;
}else {
return Pattern.matches(regexp, job);//很耗费时间
}
}
变为:
public boolean isMatchReduce(String name,String regexp) {
String key = "user."+name;
String job ;
synchronized(this) {
job = matchMap.get(key);
}
if(job == null) {
return false;
}else {
return Pattern.matches(regexp, job);
}
}
4.3.4 避免多余的缩减锁的范围
两次加锁之间的语句非常简单,导致加锁的时间比执行这些语句还长,这个时候应该进行锁粗化—扩大锁的范围。
synchronized(this) {
job = matchMap.get(key);
}
job = job + "sc"; // 时间太短,而加锁是会产生额外的指令的
synchronized(this) {
job = matchMap.get(key);
}
合并后变为:
synchronized(this) {
job = matchMap.get(key);
job = job + "sc";
job = matchMap.get(key);
}
4.3.5 锁分段
ConcurrrentHashMap就是典型的锁分段。
缺点:访问整个容器比较麻烦。
4.3.6 替换独占锁
在业务允许的情况下:
- 1) 使用读写锁
- 2)用自旋CAS
- 3) 使用系统的并发容器
5.线程安全的单例模式
5.1 双重检查锁定的问题
正确代码请参考: 双重检查SingletonDoubleCheck.java
双重检查锁定在ConcurrentHashMap中也有用到!
解决办法:加volatile关键字
5.2 懒汉式
懒汉式SingletonLazy.java
延迟初始化的其他应用
类初始化模式,也叫延迟占位模式。在单例类的内部由一个私有静态内部类来持有这个单例类的实例。
延迟占位模式还可以用在多线程下实例域的延迟赋值。
有个问题:static 类和 static成员变量在最初应该就初始化好了,那为什么叫做懒汉式,是不是因为new这个操作延迟?
Java中静态内部类可以访问其外部类的成员属性和方法,同时,静态内部类只有当被调用的时候才开始首次被加载,利用此特性,可以实现懒汉式,在静态内部类中静态初始化外部类的单一实例即可。
public class SingletonLazy {
private SingletonLazy(){}
//定义一个私有类,来持有当前类的实例
private static class InstanceHolder{
public static SingletonLazy instance = new SingletonLazy();
}
public static SingletonLazy getInstance(){
return InstanceHolder.instance;
}
}
延迟初始化有很多应用:
public class InstanceLazy {
private Integer value;
private Integer val ;//可能很大,如巨型数组1000000;
public InstanceLazy(Integer value) {
super();
this.value = value;
}
public Integer getValue() {
return value;
}
private static class ValHolder {
public static Integer vHolder = new Integer(1000000);
}
public Integer getVal() {
return ValHolder.vHolder;
}
}
5.3 饿汉式
- 在声明的时候就new这个类的实例,因为在JVM中,对类的加载和类初始化,由虚拟机保证线程安全。
public class SingletonHungry {
public static SingletonHungry singletonHungry = new SingletonHungry();
private SingletonHungry(){}
}
- 或者使用枚举。
参考
- 1)享学课堂Mark老师笔记
- 2)锁以及调度 请参考《现代操作系统》第2章、第6章
- 3)多线程算法 请参考《算法导论 第3版》 第27章