当我们访问某些方法会加上关键synchronized ,以HashTable为例的put方法为例,我们知道hashtable是线程安全的,为什么是线程安全的?他的put方法源码如下
public synchronized V put(K var1, V var2) {
if (var2 == null) {
throw new NullPointerException();
} else {
Hashtable.Entry[] var3 = this.table;
int var4 = var1.hashCode();
int var5 = (var4 & 2147483647) % var3.length;
for(Hashtable.Entry var6 = var3[var5]; var6 != null; var6 = var6.next) {
if (var6.hash == var4 && var6.key.equals(var1)) {
Object var7 = var6.value;
var6.value = var2;
return var7;
}
}
this.addEntry(var4, var1, var2, var5);
return null;
}
}
其中他调用了addEntry方法,
private void addEntry(int var1, K var2, V var3, int var4) {
++this.modCount;
Hashtable.Entry[] var5 = this.table;
if (this.count >= this.threshold) {
this.rehash();
var5 = this.table;
var1 = var2.hashCode();
var4 = (var1 & 2147483647) % var5.length;
}
Hashtable.Entry var6 = var5[var4];
var5[var4] = new Hashtable.Entry(var1, var2, var3, var6);
++this.count;
}
在他的底部有一个++this.count
我们都知道++i不是原子操作,线程在工作的时候,每个线程都有自己的独立工作空间,当他们对自己本地缓存数据做修改的时候另外的线程并不知道,所以就会造成了这种数据不一致原子性问题问题,synchronized保证了线程的原子性和内存可见。
synchronized特性
1.简单加锁的方式
private int count=10;
public void test(){
synchronize(this){
count--;
}
}
2.
private int count=10;
public synchronized void test2(){
count--;
}
上面这两种锁的方式是其实是等价的。都是锁定当前对象。
静态方法的锁
静态方法是没有this的对象的,当静态方法增加synchronized的代表synchronized(T.class)。
这个锁就是锁的Class对象。
T.class是单例的吗?
一般情况下,在同一个类加载器空间他一定是。不是同一个类加载器就不是,不同的类加载器之间也不能访问。所以能访问那他一定是单例的。
脏读
假设有这样的业务场景,假设张三有100块钱,刚开始起个线程初始化100块钱但是在初始化的时候sleep了两秒,也就是他的set方法sleep了两秒,注意set方法加锁了,初始化完之后后面直接掉get获取的 方法,代码如下:
package com.tom.syn;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class Account {
private String name;
private double balance;
public synchronized void set(String name,double balance){
this.name=name;
try {
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
this.balance=balance;
}
public static void main(String[] args) {
Account account=new Account();
new Thread(()->account.set("zhangsan",100.0)).start();;
try {
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(account.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(account.getBalance("zhangsan"));
}
public double getBalance(String name) {
return this.balance;
}
}
直接说结果:
0.0
100.0
由于getBalance没有加锁,所以他不需要等待由于初始化的时候sleep了两秒所以在打印第一行的时候打印出来的结果是0.00
第二次打印因为已经初始化过了所以打印的是100.0
这就是一种脏读现象。
如果我们对读操作也加锁,getBalance方法增加synchronized,因为都是this锁所以他们是同一把锁,所以读操作会等写操作完成了之后才会进行下一步操作,保证了数据的准确性。
可重入
synchronized可重入是为了解决自己死锁的问题提出来的。
synchronized(this){
i++;
synchronized(this){
j++;
}
}
当一个同步方法调用另一个同步方法时,外层的方法已经拿到锁,当他进入内部时发现也需要锁,然后发现锁是它自己的锁,我需要,你也需要,如果不解决这种情况就出现了死锁的情况。可重入的概念就是这样提出来的,当他发现是同一个线程申请这个锁的时候,允许,这就叫做可重入做。
锁升级
早期的jdk,这个synchronized的底层实现是重量级的重量级到这个synchronized要去操作系统去申请锁的地步,这就造成了synchronized的效率非常低。后来就有了锁升级的概念。
HotSpot的实现:如果只有第一个线程访问的时候实际上是没有给这个资源加锁的,在内部实现的时候,只是记录了这个线程的id。这时是偏向锁
偏向锁如果有线程竞争的话就会升级会自旋锁
自旋锁转圈10次之后就会升级会重量级锁,重量级锁就是去操作系统那里申请资源,这就是锁升级的一个全过程。
所以并不是cas的效率就一定比系统锁的效率要高,这个要区分实际情况。