并发和并行
并发是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并行是说在单位时间内多个任务同时在执行。
Java内存模型
Java内存模型(如下图)规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作内存,线程读写变量时操作的是自己工作内存中的变量。
在线程执行的时候,首先会从主内存中read变量值,再load到工作内存中的副本中,然后再传给处理器执行,执行完毕后再给工作内存的副本赋值,随后工作内存再把值传回给主内存,主内存中的值才更新。
内存模型的三大特性
-
原子性(Atomicity) 对于读取和赋值操作都是原子性操作,要做一定做完,要么就没有执行。比如i = 2;就是简单的读取操作,必定是原子操作, j = i不是原子操作,分两步,先读取i的值,然后再将值赋给j,JMM只是实现了基本的原子性,如果是i++这样的操作,需要借助Synchronized和Lock保证整块代码的原子性。线程在释放锁之前,必定会把i的值刷回到主存的。
-
可见性(Visibility)
Java就是利用volatile来提供可见性的。当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其他线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。其实通过synchronized和Lock也能保证可见性,线程释放锁之前,会报共享变量值都刷回到主存,但是synchronized和Lock的开销都更大。 -
有序性(Ordering)
JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial含义,即不管怎么重排序,程序的执行结果不能改变。JMM保证了重排序不会影响单线程的执行,但是在多线程中却容易出现问题。
内存的不可见性
如果多个线程同时执行的时候,就会存在内存的不可见性问题。例:
public class ThreadVolatileDemo extends Thread{
public boolean flag = true;
@Override
public void run() {
System.out.println("开始执行子线程...");
while (flag) {
}
System.out.println("线程停止");
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public static void main(String[] args) throws InterruptedException {
ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
threadVolatileDemo.start();
Thread.sleep(3000);
threadVolatileDemo.setFlag(false);
System.out.println("flag 已经设置成false");
Thread.sleep(1000);
System.out.println(threadVolatileDemo.flag);
}
}
结果:线程会一直循环执行
由于线程之间的不可见性,main线程设置flag的值并没有成功
解决办法:public boolean flag = true; 加上volatile关键字
volatile关键字
volatile修饰的共享变量,有两大特性:
- 保证了不同线程对该变量操作的内存可见性
- 禁止指令重排序
volatile保证了JVM每次使用这个变量的时候,都会直接从主内存中取,不通过工作内存取数据。保证了内存的可见性。
volatile虽然可以保证内存的可见性,但是不能保证操作的原子性。但volatile是非原子性
volatile使用时机
- 写入变量值不依赖变量的当前值。因为如果依赖当前值,将是获取一计算一写入三步操作,这三步操作不是原子性,volatile不保证原子性
- 读写变量值没有加锁。加锁已经可以保证内存的可见性了
Atomic原子类
由于volatile修饰变量是非原子性,会引起并发不一致问题。Java提供了Atomic原子类去修饰变量,即保证了原子性又保证了可见性。Atomic原子类采用的CAS操作,不会引起阻塞,比加锁的性能好很多。
synchronized关键字
synchronized块是一个总原子性内置锁,是排它锁。
synchronized解决共享变量的内存可见性。synchronized块内使用到的变量从工作内存中清除,这样synchronized块使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存获取。退出synchronized块就是对共享变量的修改刷新到主内存。
synchronized关键字和volatile关键字的区别
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
- 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞。
- volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
ThreadLocal
ThreadLocal保证每个线程都有自己的局部变量,为每个线程提供独立的变量副本,每个线程都可以独立改变自己的变量副本,不会影响到其他线程的副本。
ThreadLocal类有4个方法:
- void set(Object value) 设置当前线程局部变量的值
- public Object get() 获取当前线程局部变量的值
- public void remove() 删除当前线程局部变量的值,可以加快内存回收的速度
- protected Object initalValue() 返回线程局部变量的初始值
Demo: 每个线程都从1开始获取的自己的序列号
public class Res {
public static Integer count = 0;
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public Integer getNum() {
int count = threadLocal.get() + 1;
threadLocal.set(count);
return count;
}
}
public class ThreadLocaDemo extends Thread{
private Res res;
public ThreadLocaDemo(Res res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "----" + res.getNum());
}
}
public static void main(String[] args) {
Res res = new Res();
ThreadLocaDemo threadLocaDemo1 = new ThreadLocaDemo(res);
ThreadLocaDemo threadLocaDemo2 = new ThreadLocaDemo(res);
ThreadLocaDemo threadLocaDemo3 = new ThreadLocaDemo(res);
threadLocaDemo1.start();
threadLocaDemo2.start();
threadLocaDemo3.start();
}
}
原理
ThreadLocal 内部维护的是一个类似 Map 的ThreadLocalMap 数据结构,key 为当前对象的 Thread 对象,值为 Object 对象