JAVA多线程基础 之三 线程安全的相关概念(JAVA内存模型&ThreadLocal&Atomic类&死锁&重排序)
线程安全的相关概念
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语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。