JAVA多线程基础 之三 线程安全的相关概念(JAVA内存模型&ThreadLocal&Atomic类&死锁&重排序)

线程安全的相关概念

 

JAVA内存模型

共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

JAVA多线程基础 之三 线程安全的相关概念(JAVA内存模型&ThreadLocal&Atomic类&死锁&重排序)

 

ThreadLocal

ThreadLocal提供一个线程的局部变量,访问某个线程拥有自己局部变量。

 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal的接口方法

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

•     void set(Object value)设置当前线程的线程局部变量的值。

•     public Object get()该方法返回当前线程所对应的线程局部变量。

•     public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

•     protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

案例:

主程序:

public class ThreadLocalDemo implements Runnable {

    private Res res ;

    public ThreadLocalDemo(Res res)
    {
        this.res = res;
    }

    public static void main(String[] args){
        Res res = new Res();
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo(res);
        Thread t1 = new Thread(threadLocalDemo,"t1");
        Thread t2 = new Thread(threadLocalDemo,"t2");
        t1.start();
        t2.start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("name: " + Thread.currentThread().getName() +"  number:"+ res.getNumber());
        }
    }
}

不使用ThreadLocal,不能保证各个线程间count相对独立

//未使用ThreadLocal
class Res
{
    private Integer count = 0;

    public Integer getNumber() {
        return ++count;
    }
}

运行结果:

name: t1  number:1

name: t2  number:1

name: t2  number:3

name: t1  number:2

name: t1  number:5

name: t2  number:4

使用ThreadLocal后,各线程间count相对独立

//使用ThreadLocal 底层是一个map集合
class Res
{
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0;
        }
    };

    public Integer getNumber() {
        int count = threadLocal.get()+1;
        threadLocal.set(count);
        return count;
    }
}

运行结果:

name: t1  number:1

name: t2  number:1

name: t1  number:2

name: t2  number:2

name: t1  number:3

name: t2  number:3

 

Atomic类

单纯使用Atomic只能保证本身方法的原子性,并不能保证多次方法的原子性。

public class AtomicUse {



    private static AtomicInteger count = new AtomicInteger(0);



    /**

     * 若不加synchronized 只能保证一个方法的原子性 不一定每次结果为整十

     * @return

     */

    public synchronized int multiAdd(){

        try {

            Thread.sleep(500);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        count.addAndGet(1);

        count.addAndGet(2);

        count.addAndGet(3);

        count.addAndGet(4);//+10

        return count.get();

    }



    public static void main(String[] args){

        AtomicUse atomicUse = new AtomicUse();

        List<Thread> ts = new ArrayList<>();

        for (int i = 0; i < 100; i++) {

            ts.add(new Thread(new Runnable() {

                @Override

                public void run() {

                    System.out.println(atomicUse.multiAdd());

                }

            }));

            ts.get(i).start();

        }

    }



}

死锁

锁现象:当两个或两个以上进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们将一直阻塞下去。

产生死锁的四个必要条件:

(1) 互斥条件:一个资源每次只能被一个进程使用。

(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3) 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。

(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

例子:

/**

 * Created by zhanghaipeng on 2018/11/8.

 * 模拟死锁

 * 死锁问题定位

 * 1。使用jps或系统的ps命令、任务管理器等工具,确定pid

 * 2。调用jstack获取xianchengzhan${JAVA_HOME}\bin\jstack 53342your_pid jstack

 */

public class DeadLockSample extends Thread {

    private String first;

    private String second;

    public DeadLockSample(String name, String first, String second) {

        super(name);

        this.first = first;

        this.second = second;

    }



    public void run() {

        synchronized (first) {

            System.out.println(Thread.currentThread().getId()+ this.getName() + " obtained: " + first);

            try {

                Thread.sleep(1000L);

                synchronized (second) {

                System.out.println(Thread.currentThread().getId()+ this.getName() + " obtained: " + second);

                   }

                } catch (InterruptedException e) {

                   // Do nothing

                }

            }

    }

    public static void main(String[] args) throws InterruptedException {

        String lockA = "lockA";

        String lockB = "lockB";

        DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB);

        DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA);

        t1.start();

        t2.start();

        t1.join();

        t2.join();

    }

}

 

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.181-b13 mixed mode):

 

"Attach Listener" #13 daemon prio=9 os_prio=31 tid=0x00007fd17f81b000 nid=0xe07 waiting on condition [0x0000000000000000]

   java.lang.Thread.State: RUNNABLE

 

"Thread2" #12 prio=5 os_prio=31 tid=0x00007fd17e011000 nid=0xa803 waiting for monitor entry [0x000070000801d000]

   java.lang.Thread.State: BLOCKED (on object monitor)

    at com.test.threads.DeadLockSample.run(DeadLockSample.java:25)

    - waiting to lock <0x000000076abdb870> (a java.lang.String)

    - locked <0x000000076abdb8a8> (a java.lang.String)

 

"Thread1" #11 prio=5 os_prio=31 tid=0x00007fd17e010800 nid=0xa903 waiting for monitor entry [0x0000700007f1a000]

   java.lang.Thread.State: BLOCKED (on object monitor)

    at com.test.threads.DeadLockSample.run(DeadLockSample.java:25)

    - waiting to lock <0x000000076abdb8a8> (a java.lang.String)

    - locked <0x000000076abdb870> (a java.lang.String)

 

死锁检查工具

import java.lang.management.ManagementFactory;

import java.lang.management.ThreadInfo;

import java.lang.management.ThreadMXBean;

import java.util.concurrent.Executors;

import java.util.concurrent.ScheduledExecutorService;

import java.util.concurrent.TimeUnit;



public static void checkDeadThread(){

    ThreadMXBean mbean = ManagementFactory.getThreadMXBean();

    Runnable dlCheck = new Runnable() {

        @Override

        public void run() {

            long[] threadIds = mbean.findDeadlockedThreads();

            if (threadIds != null) {

                ThreadInfo[] threadInfos = mbean.getThreadInfo(threadIds);

                System.out.println("Detected deadlock threads:");

                for (ThreadInfo threadInfo : threadInfos) {

                    System.out.println(threadInfo.getThreadId() + " " + threadInfo.getThreadName());

                }

            }

        }

    };



    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    // 稍等 5 秒,然后每 10 秒进行一次死锁扫描

    scheduler.scheduleAtFixedRate(dlCheck, 5L, 10L, TimeUnit.SECONDS);

    // 死锁样例代码…

}

重排序

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。

as-if-serial语义

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。

程序顺序规则

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能的开发并行度。

重排序对多线程的影响

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。