由于东西很多很乱,就按照Q&A的方式整理以下

进程与线程的区别

  • 调度方面:在传统计算机中,进程是调度的基本单位,但是在引入线程之后,线程是调度的基本单位。同一个进程下的线程切换不会引起进程切换,但是不同的就会影响
  • 拥有资源方面:进程是拥有资源的单位,线程之间共享内存空间
  • 切换方面:进程切换要保存上下文程序计数器很多东西,而线程切换只用保存一些寄存器。开销远小于进程
  • 并发性:同一个进程下的线程可以并发,不同进程下的线程也可以并发

volatile关键字

volatile是不稳定的意思,不止用于java,c++也有使用,声明了这个关键字的变量会禁用缓存,每一次都从内存找最新的值,这样就能保证可见性,但是不会保证原子性,例如下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class VolatileAtomicityDemo {
public volatile static int inc = 0;

public void increase() {
inc++;
}

public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo();
for (int i = 0; i < 5; i++) {
threadPool.execute(() -> {
for (int j = 0; j < 500; j++) {
volatileAtomicityDemo.increase();
}
});
}
// 等待1.5秒,保证上面程序执行完成
Thread.sleep(1500);
System.out.println(inc);
threadPool.shutdown();
}
}

这里得不到结果的主要原因是,inc++不是一个原子操作,其实是由三个操作并成,先读取inc,再++,最后写回,可能两个线程同时都读到inc为100,这个时候再同时更改写回内存,这样就会达不到预期,所以volatile关键之只能保证可见性,不能保证原子性

如果要保证原子性,可以使用:

  • synchronized关键字上锁increase方法
  • 既然可以用synchronized方法,那肯定也可以用ReentrantLock,关于这两者的区别后文再说
  • ReentrantLock基于CAS和AQS,那肯定用CAS的原子方法也可以解决,后文再说

乐观锁和悲观锁

  • 悲观锁:悲观的觉得临界区一定会有人来竞争,所以每次访问的时候一定要上锁,synchronized和ReentrantLock就是比较典型的悲观锁
  • 乐观锁:很投机的觉得,这段代码可能不会有别的线程访问,就算有我也不上锁,用其他的方法,例如队列,链表的方式,比较典型的就是CAS(其实我觉得MVCC多少也有点这种思想)
    最大的区别就是锁的粒度不一样,悲观锁由于每一次都要上锁,肯定效率没有乐观锁高,反而言之,如果频繁发生冲突,乐观锁的效率也是很低的。总而言之,在读多写少的场景,可以使用乐观锁,写多读少的情况下,使用悲观锁。

CAS和自旋锁

CAS

compare and swap,比较并且交换,主要思想是给出三个数据:要修改的数据的更新值,这个要修改的数据的预期值,要修改数据的实际值。什么意思呢,例如我们要把一个等于1的数据修改成6,我对于这个要修改数据的预期值就是1,当且仅当数据是1的时候,我才会把它修改成6.
这个思想怎么在并发的环境下体现呢?比如这个时候两个线程都要修改这个数据,有一个别的线程抢占先机,由于这个是由volatile修饰的,另一边立即会就看到修改成了另外一个值,不再是预期值1,这个时候cas就会失效,从而保证了并发。

为什么这样就能作为锁呢?

理由很简单,cas保证原子性,为什么他敢理直气壮的说自己是原子的。这就要涉及到一个神奇的类Unsafe,在实现java虚拟机的时候,由于jvm的隔离性,使得java很难接近底层操作系统,Unsafe这个类里面全都是native方法,也就是用的本地方法,对于cas而言,这个底层的方法不是java实现的,是由Unsafe调用dll动态库,也就是c++实现的,cas这条操作可以理解成汇编的一个原子指令,既然是一条指令,那么肯定是可以保证原子性的。

那么问题来了,为什么不直接弄一条锁的原子指令,而这么拐弯抹角的用到这个语句间接完成乐观锁,小编也不知道

自旋锁

我们有了这个CAS操作,到底怎么用到锁上面呢?我们先看一个用CAS的例子:

AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。AtomicInteger是一个操作Integer类型的原子操作类,这里的getAndAddInt可以保证线程安全,获取Object对象在内存偏移量,然后在+i

1
2
3
4
5
6
7
8
9
10
11
12
13
public final int getAndAddInt (Object var1, Long var2, int var4) {
int var5;
//自循环的思想,每一次循环都找Object的偏移量为var2的那个值,这个获得的就是预期值,
//循环条件是如果跟他一样就断开,如果不一样,不阻塞,不释放CPU,但是也达到了锁的功能
//能加就加,加不上就一只循环一直尝试
do {
var5 = this.getIntVolatile(var1, var2);
} while( !this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}


主要思想是加的上就加,加不上一直尝试,即使没有加锁,但是也达到了锁的功能。

这种思想称为自旋,我们把这种dowhile的思想运用到类上就是自旋锁,我猜取这个名字的原因是因为一直循环,很像自己一个人在转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();

public void lock() {
Thread currentThread = Thread.currentThread();
// 如果锁未被占用,则设置当前线程为锁的拥有者
while (!owner.compareAndSet(null, currentThread)) {
}
}

public void unlock() {
Thread currentThread = Thread.currentThread();
// 只有锁的拥有者才能释放锁
owner.compareAndSet(currentThread, null);
}
}

AtomicReference是一个原子类,提供的compareAndSet方法封装了CAS方法,参数中前面是预期值,后面是要修改的值,如果预期值和实际值是一样的,那么就设置为修改值。

上锁过程:owner这里空参数构造,所以我的预期值是空,如果这个时候只有一个线程,那么没有人上锁肯定也是空,所以实际值也是空,满足条件,那么就把空设置为当前线程。如果有两个线程,都同时要上锁,先进来的那个上了锁,下一个compareAndSet方法肯定是假,就会一直while,不会释放cpu

解锁过程:预期值为当前线程,这个线程刚刚上锁了,所以实际值肯定存的也是这个,最后满足条件就把锁设置为null,此时第二个线程终于不用自旋了,就会获取锁。

通过这个过程也看到了,自旋的优点和缺点都很明显,优点是如果时间短的话,可以免去线程的上下文切换时间;但是如果时间长,那就是很消耗cpu的,一直不释放,这个缺点有改进措施,可以用自适应自旋锁,也就说,不再是无限循环,可以设置一个最大循环次数,就不会导致死循环的情况。

ABA问题

CAS存在一个问题,判断实际值和预期值相同,其实不一定就能说明没改过,有可能预期值是A,实际值从A变成了B再变成了A,这个问题其实蛮严重的,如果是变量还好,如果在链表中,判断头节点没变化,不代表接下来的节点没有变化,就会导致完全不一样的结果。

如何避免?

加时间戳,上文用了AtomicReference,这回用AtomicStampedReference,这里使用了时间戳,只有当时间戳相同并且实际值等于预期值,才可以。

Synchronized关键字

在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

不过,在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多。因此, synchronized 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 synchronized 。

关于偏向锁多补充一点:由于偏向锁增加了 JVM 的复杂性,同时也并没有为所有应用都带来性能提升。因此,在 JDK15 中,偏向锁被默认关闭(仍然可以使用 -XX:+UseBiasedLocking 启用偏向锁),在 JDK18 中,偏向锁已经被彻底废弃(无法通过命令行打开)。

如何使用Synchronized关键字?

  1. 修饰方法
  2. 修饰静态方法(锁住当前类):注意修饰静态方法是没有互斥性的,因为静态方法是放在进程中静态代码块那一部分的,所有线程共享,每一个线程都可以访问
  3. 修饰代码块:
  • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。
  • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁

总结:

  • synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁;
  • synchronized 关键字加到实例方法上是给对象实例上锁;
  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。

构造方法可以被Synchronized修饰吗》

不可以,因为本身构造方法就已经是线程安全的了

Synchronized底层原理

基于jvm的monitor,在java编译的时候会给加锁代码加上monitorenter和两个monitorexit,在这中间的就是互斥的,为什么要有两个呢,线程有的时候会抛出异常或者错误,这个时候就会走第二个exit,用两个是为了保证一定能退出。

monitor也是基于c++的,分为三个部分,一个是当前获取锁的对象,还有一个是等待队列,一个是阻塞队列。

上锁成功的标志是monitor的拥有者为当前线程,当释放的时候会唤醒阻塞进程,有一点要说明的是这个过程是非公平,也就是运行不在队列里的新进程也来抢。

Synchronized锁升级原理

这一块暂时还没有细细研究。

有四种锁等级,锁的重量依次递增:

  1. 不加锁
  2. 加偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。这也是某种投机行为,乐观锁思想,在很少有进程竞争的时候比较节约资源
  3. 加轻度锁:也就是自旋锁等,每次操作都是cas,还是乐观锁思想,这个就比偏向锁稍微控制了一点
  4. 重度锁,使用monitor那种悲观锁

Synchronized发生竞争会依次升级锁,高并发下变成悲观锁

Synchronized和volatile什么区别

  1. Synchronized修饰方法,静态方法,代码块等,而volatile修饰变量
  2. volatile是线程同步的轻量实现,具有可见性但是没有原子性,Synchronized可以保证可见性和原子性
  3. volatile性能肯定比Synchronized好,但是主要解决的是变量的问题,Synchronized解决方法的同步和互斥。

ReentrantLock

可重入锁,实现了Lock接口,相比Synchronized功能更多,实现了轮询,超时,中断,公平锁和非公平锁。

公平和非公平锁主要是Sync里面实现的,而Sync继承自AQS,也就是说ReentrantLock用的就是AQS思想。

AQS

主要数据结构是CLH,最早的CLH是一个虚拟的单向链表,为什么是虚拟的呢,因为甚至连指针都没有,一个CLH节点就包含两个结构,一个是当前线程,另外一个是一个bool变量,用来表示是否获得了锁。

那是怎么运作的呢?

初始化一个尾节点,线程那部分为空,bool部分为true,也就是说可以被上锁,每一个线程都用自旋的方式尝试获得来的时候的尾节点的bool值,那么这个时候来了一个线程,用CAS尝试获取现在的尾节点的bool,为真,那就可以上锁为false,同时自己的bool也为false,表示前面的节点都不能用,所以下一个也不能。

来了一个线程2,现在的尾节点是线程1,他的bool是false,线程2会一直自旋的获取前一个节点的bool值,后面的也是,如果此时线程1好了,那么线程2监听到前面一个变成了true,那就会开始线程2,以此类推。

优点很明显:如果时间短,那么获取和释放锁的开销小。CLH 的锁状态不再是单一的原子变量,而是分散在每个节点的状态中,降低了自旋锁在竞争激烈时频繁同步的开销。还有就是公平,先进先出

这样的CLH有什么缺点呢,首先是都是自旋,如果时间短还好,长了那肯定浪费资源。功能单一,不能支持ReentrantLock的那些接口功能。

AQS对于CLH的改造

针对自旋,AQS都改造成了阻塞,还是不能用自旋。针对第二个功能单一的缺点,使用了双向链表和一个全局变量state(其实我觉得之前那种看上一个节点情况的机制还蛮好的,这不是又重新变成信号量那种机制了)

state属性表示资源的状态,为0表示可以抢了,为1表示不能,为1时候来的线程进阻塞队列

  • 公平与非公平,公平就是直接每次都唤醒队列头的,非公平就是来的也一起竞争,但是可能会导致饥饿
  • 可重入:一个线程多次进入可以state自增,如果重入两次那么state就是2,但是要保证加锁几次就解锁几次,保证最终还是0

ReentrantLock和synchronized区别

  1. 首先两者都是悲观锁,也都是可重入的,synchronized是一个关键字,基于jvm实现,ReentrantLock是一个是实现类,实现Lock接口,使用的时候需要声明。
  2. ReentrantLock功能比较多,实现了可中断(中断也就说如果得不到锁就去干别的事情了,synchronized不可中断),可超时(ReentrantLock实现了,如果超时多少就不去争抢锁了,syn实现不了),锁可以绑定多个条件(Condition类,就相当于不同的信号量,可以控制一部分Condition下的进程等待,另一部分的执行)
  3. 没有竞争的时候synchronized优化很多,锁没有升级的时候性能很好;重度的时候Lock的性能比较好,因为底层还是用了cas的一部分乐观机制,相比synchronized的monitor还是好一点