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

    xiaoxiao2022-06-24  181

    线程安全的相关概念

     

    JAVA内存模型

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

     

    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语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

     


    最新回复(0)