java多线程基础

等待池

假设一个线程a 调用了某个对象的wait方法,线程a就会释放该锁,进入到该对象的等待池中。等待池中的线程不会参与锁的竞争。

锁池

假设线程a已经拥有了某个对象的锁,而其他线程想要执行这个对象的某个同步代码块(syschronized)的代码,由于这些线程在进入对象的同步代码块(syschronized)之前必须先获得这个对象的锁,但是该对象的锁目前正被线程a拥有,所以这些线程就进入了该对象的锁池中。

wait

  1. wait 方法只能在同步代码块(syschronized)中调用,调用后就会释放该锁,进入到等待池中。(暂时失去锁机制 wait(long timeOut)到超时时间后还需要返还对象锁);

  2. 进入wait 状态的线程能够被notify 和notiffyAll唤醒,然后从等待池进入锁池中参与锁的竞争

  3. wait通常有条件的执行,直到某个条件为真(while)

sleep

sleep 使当前线程进入停滞状态(阻塞当前线程),让出cpu的使用,目的使不让线程独自霸占该进程的所获的所有cpu资源,留给其他线程执行的机会。

sleep 使Thread的静态方法,他不能改变对象的锁,所以当在一个同步代码块中调用sleep方法,虽然线程休眠了,但是对象的锁并没有被释放,其他线程不能访问这个对象的锁;

在sleep的休眠期满了之后,该线程不一点会立即执行,这是因为其他线程可能正在运行而且没有被调度为放弃执行,除非此线程有更高的优先级。

yield

yield 和sleep大部分一样, yield和sleep的主要区别是,yield会临时暂停当前正在执行的线程,来让有同样优先级的线程有机会执行,或则等待线程的优先级都比较低,那么该线程会继续执行。执行了yeild方法的线程,什么时候会继续运行由调度器来决定。

notify

notify 调用后 只会将等待池中的一个随机移到锁池,所以这个函数使用不当会造成死锁,当这个被移到锁池的线程获得锁之后没有唤醒其他线程(调用notigy,notifyAll)就会可能造成死锁。因为在等待池中的线程不会参与锁的竞争。

notifyAll

notifyAll 会将等待池中的全部线程移到锁池。

原子性

Java的原子性就和数据库事务的原子性差不多,一个操作中要么全部执行成功或者失败。

JMM只是保证了基本的原子性,但类似于i++之类的操作,看似是原子操作,其实里面涉及到:

  • 获取 i 的值。
  • 自增。
  • 再赋值给 i。

这三步操作,所以想要实现i++这样的原子操作就需要用到synchronized或者是lock进行加锁处理。

可见性

由于CPU直接从主内存中读取数据的效率不高,所以都会对应的CPU高速缓存,先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。

volatile关键字就是用于保证内存可见性,当线程A更新了 volatile 修饰的变量时,它会立即刷新到主线程,并且将其余缓存中该变量的值清空,导致其余线程只能去主内存读取最新值。

使用volatile关键词修饰的变量每次读取都会得到最新的数据,不管哪个线程对这个变量的修改都会立即刷新到主内存。

synchronized和加锁也能能保证可见性,实现原理就是在释放锁之前其余线程是访问不到这个共享变量的。但是和volatile相比开销较大。

顺序性

正常情况下的执行顺序应该是1>>2>>3。但是有时JVM为了提高整体的效率会进行指令重排导致执行的顺序可能是2>>1>>3。但是JVM也不能是什么都进行重排,是在保证最终结果和代码顺序执行结果一致的情况下才可能进行重排。

重排在单线程中不会出现问题,但在多线程中会出现数据不一致的问题。

Java 中可以使用volatile来保证顺序性,synchronized 和 lock也可以来保证有序性,和保证原子性的方式一样,通过同一段时间只能一个线程访问来实现的。

除了通过volatile关键字显式的保证顺序之外,JVM还通过happen-before原则来隐式的保证顺序性。

其中有一条就是适用于volatile关键字的,针对于volatile关键字的写操作肯定是在读操作之前,也就是说读取的值肯定是最新的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private static volatile Singleton singleton;

private Singleton() {
}

public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}

}

这里的volatile关键字主要是为了防止指令重排。 如果不用volatilesingleton = new Singleton();,这段代码其实是分为三步:

  • 分配内存空间。(1)
  • 初始化对象。(2)
  • singleton对象指向分配的内存地址。(3)

加上volatile是为了让以上的三步操作顺序执行,反之有可能第三步在第二步之前被执行就有可能导致某个线程拿到的单例对象还没有初始化,以致于使用报错。

volatile

volatile关键字只能保证可见性,顺序性,不能保证原子性