一、并发编程3大特性是什么?
并发编程三大特性分别是原子性、可见性和有序性。java内存模型JMM就是围绕着原子性、
可见性、原子性来处理java线程间通信的。
二、原子性
1、什么是原子性?
原子性是指一个操作是不可分割的,不可终端的;一个线程执行时,另一个线程
不会影响到它 ,即多线程操作临界资源,预期结果要与最终结果一致。
如:赋值操作 a=1 是原子操作,但注意 i++/i--、++j/--j 不是原子操作,
以下边代码举例说明:
public class Test01 {private static int count = 0;public static void increment() {count++;try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for(int i=0;i<100;i++){increment();}});Thread t2 = new Thread(() -> {for(int i=0;i<100;i++){increment();}});t1.start();t2.start();//主线程(这里是main线程)阻塞,等待t1、t2执行完成后主线程(main线程)才继续往下执行t1.join();t2.join();//若count++是原子的,那么最后count一定输出200,否则说明count++不是原子性的System.out.println(count);}
}
当前程序多线程操作时,最终结果与预期结果不一致
2、通过javap 反编译java字节码文件来观察i++操作的执行过程
1)在当前文件下执行命令 javac java文件,将java文件编译成字节码文件,
即.class 文件,如Test01.class
2)执行 javap -v Test01.class 命令,反编译字节码文件,i++ 的执行过程如下:
可以看到i++的操作分了3个步骤,即如上图所示分别是:从主内存中取变量、
在线程私有本地内存中计算、最后把计算结果刷新到主内存
3、如何保证原子性?
3.1)通过synchronized关键字 保证原子性
可以在方法上添加关键字或在方法中使用synchronized 同步代码块来保证原子性。
synchronized 可以避免多个线程同时操作临界资源,同一时间点只有一个线程在
操作临界资源。
以使用同步代码块为例,如下所示:
public static int count = 0;public void increment(){synchronized (this){count++;}}
java文件编译后,monitorenter指令插入到代码块开始的位置,表示获取了锁;
而 monitorexit 指令插入到代码块结束或异常的位置,表示释放锁。
jvm保证每个 monitorenter指令 都有一个 monitorexit 指令 与其对应
3.2)CAS 保证原子性
(1)什么是CAS
compare and swap(简称CAS)也就是比较和交换,是一条CPU层面
上的并发源语,在cpu层面上执行。它在替换内存中某个位置的值时,首先
会查看该位置的值是否与预期的值一致,若一致则交换;这个操作是原子的
java中基于Unsafe的类提供了对CAS的操作方法,jvm会帮助我们将方法实现
编译成CAS汇编指令。
注意:CAS只是比较和交换,但从内存中取变量需要自己实现;
CAS只能保证一个变量的原子性,不能保证多行代码的原子性
(2)使用CAS保证原子性,示例代码如下:
public static AtomicInteger count = new AtomicInteger(0);public static void increment(){//自增count.incrementAndGet();try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}
3.3)使用Lock 锁来保证原子性,示例代码如下:
3.4)使用ThreadLocal保证原子性
ThreadLocal保证原子性的方式是不让多线程去操作临界资源,让每个线程操作
自己私有本地内存中的资源(即操作自己的数据,不操作共享数据)
示例代码如下:
ThreadLocal 实现原理:
1)每个Thread中都存储着一个成员变量ThreadLocalMap,用与存放与其关联
的ThreadLocal,ThreadLocal绑定的是当前线程
ThreadLocal的set/get方法如下图:
2)ThreadLocal 本身不存储数据,像是一个工具类,基于ThreadLocal去操作
ThreadLocalMap
3)ThreadLocalMap本身就是基于Entry[] 实现的,因为一个线程可以绑定多个
ThreadLocal,这样一来可能需要存储多个数据,所以采用Entry[] 形式实现
4)每个Thread 都有自己独立的 ThreadLocalMap ,再基于ThreadLocal 对象
本身作为key,对value进行存取
5)ThreadLocalMap的key是一个弱引用,若引用的特点是:对象即使存在弱
引用,在GC时,也必须被回收。这里弱引用是为了解决在ThreadLocal对
象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法
回收的问题,即弱引用是为了解决ke内存泄漏的问题。
ThreadLocal 存储结构图如下:
ThreadLocal 内存泄漏的问题:
1)什么是 ThreadLocal 内存泄漏?
如果 ThreadLocal 引用丢失,key因为弱引用会被GC回收掉,如果同时
线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同
时也无法被获取到。
2) 如何解决 ThreadLocal 内存泄漏?
只需要在使用完 ThreadLocal 对象之后,及时调用 ThreadLocal 的
remove() 方法把 ThreadLocal 的值从 Entry 删除。
ThreadLocal的 remove方法如下: