多线程系列复习【三】多线程高级(Java内存模型,多线程特性,多线程控制类)

目录

1、Java内存模型

1.1、Java程序执行流程回顾

1.2、Java内存模型

1.2.1、PC程序计数器

1.2.2、Java栈JavaStack(虚拟机栈JVM Stack)

1.2.3、方法区MethodArea

1.2.4、常量池ConstantPool

1.2.5、本地方法栈Native Method Stack

2、多线程特性

2.1、原子性

2.2、可见性

2.3、有序性

3、多线程控制类

3.1、ThreadLocal

3.1.1、作用

3.1.2、示例

3.1.3、分析

3.2、原子类

3.2.1、废原子性操作问题演示

3.2.2、原子类解决非原子性操作问题

3.2.3、原子类CAS原理分析

3.2.4、CAS的ABA问题及解决

3.3、Lock类

3.3.1、Lock接口关系图

3.3.2、可重入锁

3.3.3、读写锁

3.4、Volatile关键字

3.4.1、作用

3.4.2、应用场景


1、Java内存模型

Java内存模型是每个java程序员必须掌握理解的,这是Java的核心基础,对我们编写代码特别是并发编程时有很大帮助。由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。

1.1、Java程序执行流程回顾

如图所示

首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),

然后由JVM中的类加载器加载各个类的字节码文件,

加载完毕之后,交由JVM执行引擎执行。

Java内存模型指的就是Runtime Data Area(运行时数据区),即程序执行期间用到的数据和相关信息保存区。

1.2、Java内存模型

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。结构如下图:

 

1.2.1、PC程序计数器

  1. 每个线程对应有一个程序计数器。
  2. 各线程的程序计数器是线程私有的,互不影响,是线程安全的。
  3. 程序计数器记录线程正在执行的内存地址,以便被中断线程恢复执行时再次按照中断时的指令地址继续执行

1.2.2、Java栈JavaStack(虚拟机栈JVM Stack)

  1. 每个线程会对应一个Java栈;
  2. 每个Java栈由若干栈帧组成;
  3. 每个方法对应一个栈帧;
  4. 栈帧在方法运行时,创建并入栈;方法执行完,该栈帧弹出栈帧中的元素作为该方法返回值,该栈帧被清除;
  5. 栈顶的栈帧叫活动栈,表示当前执行的方法,才可以被CPU执行;
  6. 线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  7. 栈扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

1.2.3、方法区MethodArea

  1. 方法区是Java堆的永久区(PermanetGeneration)
  2. 方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,
  3. 方法区是被Java线程共享的
  4. 方法区要使用的内存超过其允许的大小时,会抛出OutOfMemoryError: PremGen space的错误信息。

1.2.4、常量池ConstantPool

  1. 常量池是方法区的一部分。
  2. 常量池中存储两类数据:字面量和引用量。

         字面量:字符串、final变量等。

         引用量:类/接口、方法和字段的名称和描述符,

  1. 常量池在编译期间就被确定,并保存在已编译的.class文件中

1.2.5、本地方法栈Native Method Stack

  1. 本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。
  2. 本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

1.3、Java内存模型工作示意图

  1. 首先类加载器将Java代码加载到方法区
  2. 然后执行引擎从方法区找到main方法
  3. 为方法创建栈帧放入方法栈,同时创建该栈帧的程序计数器
  4. 执行引擎请求CPU执行该方法
  5. CPU将方法栈数据加载到工作内存(寄存器和高速缓存),执行该方法
  6. CPU执行完之后将执行结果从工作内存同步到主内存

线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。

当个多个线程同时读写某个内存数据时,就会产生多线程并发问题,要解决这些问题就涉及到多线程编程三个特性:原子性,有序性,可见性。

2、多线程特性

多线程编程要保证满足三个特性:原子性、可见性、有序性。

2.1、原子性

原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

2.2、可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。显然,对于单线程来说,可见性问题是不存在的。

2.3、有序性

有序性即程序执行的顺序按照代码的先后顺序执行。

3、多线程控制类

3.1、ThreadLocal

3.1.1、作用

ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本。

当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal,比如数据库连接Connection,每个请求处理线程都需要,但又不相互影响,就是用ThreadLocal实现。

3.1.2、示例

两个线程分别转账:

package com.multithread.thread;

public class Bank {
    ThreadLocal<Integer> t = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue(){
            return 0;
        }
    };
    public Integer get(){
        return t.get();
    }
    public void set(){
        t.set(t.get()+10);
    }
    
    public static void main(String[] args){
        Bank bank = new Bank();
        Transfer transfer = new Transfer(bank);
        Thread t1 = new Thread(transfer);
        Thread t2 = new Thread(transfer);
        t1.start();
        t2.start();
    }
}

class Transfer implements Runnable{
    Bank bank;
    public Transfer(Bank bank){
        this.bank = bank;
    }
    public void run() {
        for (int i=0;i<10;i++){
            bank.set();
            System.out.println(Thread.currentThread()+""+bank.get());
        }
    }
}

打印结果:

3.1.3、分析

 

  1. 在ThreadLocal类中定义了一个ThreadLocalMap,
  2. 每一个Thread都有一个ThreadLocalMap类型的变量threadLocals
  3. threadLocals内部有一个Entry,Entry的key是ThreadLocal对象实例,value就是共享变量副本
  4. ThreadLocal的get方法就是根据ThreadLocal对象实例获取共享变量副本
  5. ThreadLocal的set方法就是根据ThreadLocal对象实例保存共享变量副本

3.2、原子类

Java的java.util.concurrent.atomic包里面提供了很多可以进行原子操作的类,分为以下四类:

  1. 原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong
  2. 原子更新数组:AtomicIntegerArray、AtomicLongArray
  3. 原子更新引用:AtomicReference、AtomicStampedReference等
  4. 原子更新属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater

提供这些原子类的目的就是为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题。

3.2.1、废原子性操作问题演示

非原子性的操作会引发什么问题呢?下面以i++为例演示非原子性操作问题。

i++并不是原子操作,而是由三个操作构成:

tp1 = i;

tp2 = tp1+1;

i = tp2;

所以单线程i的值不是有问题,但多线程下就会出错,多线程示例代码如下:

package com.multithread.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicClass {
    static int n = 0;
    public static void main(String[] args) throws InterruptedException {
        int j = 0;
        while(j<100){
            n = 0;
            Thread t1 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        n++;
                    }
                }
            };
            Thread t2 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        n++;
                    }
                }
            };
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("n的最终值是:"+n);
            j++;
        }

    }
}

执行结果如下:发现n的最终值可能不是2000

3.2.2、原子类解决非原子性操作问题

以上代码修改如下:

package com.multithread.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicClass {
    static AtomicInteger n;
    public static void main(String[] args) throws InterruptedException {
        int j = 0;
        while(j<100){
            n = new AtomicInteger(0);
            Thread t1 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        n.getAndIncrement();
                    }
                }
            };
            Thread t2 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        n.getAndIncrement();
                    }
                }
            };
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("n的最终值是:"+n);
            j++;
        }

    }
}

执行结果如下:n的值永远是2000

3.2.3、原子类CAS原理分析

3.2.4、CAS的ABA问题及解决

当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。如下图:

package com.multithread.thread;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicClass {
    static AtomicStampedReference<Integer> n;
    public static void main(String[] args) throws InterruptedException {
        int j = 0;
        while(j<100){
            n = new AtomicStampedReference<Integer>(0,0);
            Thread t1 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        int stamp;
                        Integer reference;
                        do{
                            stamp = n.getStamp();
                            reference = n.getReference();
                        } while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
                    }
                }
            };
            Thread t2 = new Thread(){
                public void run(){
                    for(int i=0; i<1000; i++){
                        int stamp;
                        Integer reference;
                        do{
                            stamp = n.getStamp();
                            reference = n.getReference();

                        } while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
                    }
                }
            };
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("n的最终值是:"+n.getReference());
            j++;
        }

    }
}

执行效果如下:执行结果也是2000

注意:采用AtomicStampedReference会降低性能,慎用。

3.3、Lock类

3.3.1、Lock接口关系图

Lock和ReadWriteLock是两大锁的根接口

Lock 接口支持重入、公平等的锁规则:实现类 ReentrantLock、ReadLock和WriteLock。
ReadWriteLock 接口定义读取者共享而写入者独占的锁,实现类:ReentrantReadWriteLock

3.3.2、可重入锁

  • 不可重入锁,即线程请求它已经拥有的锁时会阻塞。
  • 可重入锁,即线程可以进入它已经拥有的锁的同步代码块。
public class ReentrantLockTest {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();

        for (int i = 1; i <= 3; i++) {
            lock.lock();
        }

        for(int i=1;i<=3;i++){
            try {

            } finally {
                lock.unlock();
            }
        }
    }
}

3.3.3、读写锁

读写锁,即可以同时读,读的时候不能写;不能同时写,写的时候不能读。

示例代码:

package com.multithread.thread;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 读写操作类
 */
public class ReadWriteLockDemo {

    private Map<String, Object> map = new HashMap<String, Object>();
    //创建一个读写锁实例
    private ReadWriteLock rw = new ReentrantReadWriteLock();
    //创建一个读锁
    private Lock r = rw.readLock();
    //创建一个写锁
    private Lock w = rw.writeLock();

    /**
     * 读操作
     *
     * @param key
     * @return
     */
    public Object get(String key) {
        r.lock();
        System.out.println(Thread.currentThread().getName() + "读操作开始执行......");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            return map.get(key);
        } finally {
            r.unlock();
            System.out.println(Thread.currentThread().getName() + "读操作执行完成......");
        }
    }

    /**
     * 写操作
     *
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
        try {
            w.lock();
            System.out.println(Thread.currentThread().getName() + "写操作开始执行......");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
        } finally {
            w.unlock();
            System.out.println(Thread.currentThread().getName() + "写操作执行完成......");
        }
    }

    public static void main(String[] args) {
        final ReadWriteLockDemo d = new ReadWriteLockDemo();
        d.put("key1", "value1");
        new Thread(new Runnable() {
            public void run() {
                d.get("key1");
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                d.get("key1");
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                d.get("key1");
            }
        }).start();
    }

}

执行效果如下:写操作为独占锁,执行期间不能读;读操作可

3.4、Volatile关键字

3.4.1、作用

一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(注意:不保证原子性
  2. 禁止进行指令重排序。(保证变量所在行的有序性

当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

3.4.2、应用场景

基于volatile的作用,使用volatile必须满足以下两个条件:

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中

常见应用场景如下:

状态量标记:

volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            
 
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

双重校验:

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

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值