likes
comments
collection
share

线程基本使用入门

作者站长头像
站长
· 阅读数 42

线程基本使用入门

  • 错误的加锁与原因分析:synchronized锁的是对象,一定要保证锁的这个对象不能发生变化

    • 重要函数:identityHashCode

      • 作用:屏蔽对hashCode()的重写

        • hashCode是Object中的成员方法,任何java类均可对其覆盖,
        • 调用identityashCode后,即使对hashCode()进行了重写,返回的仍然是为未重写的hashCode
        • hashCode可认为是对象的唯一标识符,是拿到对象在内存的位置后计算出来的(存在哈希冲突的问题)
      • 源码:

         public static native int identityHashCode(Object x);
        
    • 为什么在执行i++操作后,成员变量i(加了锁的)的hashCode发生了改变

      • 代码展示:

         package cn.enjoyedu.ch1.syn;
         ​
         /**
          * 类说明:错误的加锁和原因分析
          */
         public class TestIntegerSyn {
         ​
             public static void main(String[] args) throws InterruptedException {
                 Worker worker=new Worker(1);
                 //Thread.sleep(50);
                 for(int i=0;i<5;i++) {
                     new Thread(worker).start();
                 }
             }
         ​
             private static class Worker implements Runnable{
         ​
                 private Integer i;
         ​
                 public Worker(Integer i) {
                     this.i=i;
                 }
         ​
                 @Override
                 public void run() {
                     synchronized (i) {
                         Thread thread=Thread.currentThread();
                         System.out.println(thread.getName()+"--@"+
                                 System.identityHashCode(i));//屏蔽hashCode覆盖(这个是对象在内存中的地址,排除哈希冲突)
                         i++;
                         System.out.println(thread.getName()+"-------"+i+"-@"+
                                 System.identityHashCode(i));
                         try {
                             Thread.sleep(3000);
                         } catch (InterruptedException e) {
                             e.printStackTrace();
                         }
                         System.out.println(thread.getName()+"-------"+i+"--@"+
                                 System.identityHashCode(i));
                     }
         ​
                 }
         ​
             }
         ​
         }
        
      • 运行结果:

        线程基本使用入门

      • 寻找原因:

        1. 拿到程序的 .class文件,调用反编译工具进行查看

          i++ 变成了:Integer.valueOf(this.i.intValue() + 1);

        2. 查看Integer.valueOf:在i++的时候会创建新的对象,对象都变了,当然锁不住

           public static Integer valueOf(int i) {
               if (i >= IntegerCache.low && i <= IntegerCache.high)
                   return IntegerCache.cache[i + (-IntegerCache.low)];
               return new Integer(i);
           }
          
        3. 怎么解决synchronized(i)的问题?

          1. 锁this

          2. 实例化Object

             Object o = new Object();
             ……
                 synchronized(o){
                     //同步代码块的内容
                 }
            
  • volatile关键字

    • 概述:最轻量的同步机制,保证可见性

      • 可见性:当线程修改volatile变量的值,保证这个新值被其他的线程看到

        • 不加同步机制,还不一定能看到
    • 可见性验证:

      • 代码:不加volatile关键字

         package cn.enjoyedu.ch1.vola;
         ​
         import cn.enjoyedu.tools.SleepTools;
         ​
         /**
          * 类说明:演示Volatile的提供的可见性
          */
         public class VolatileCase {
             private static boolean ready;
             private static int number;
         ​
             private static class PrintThread extends Thread{
                 @Override
                 public void run() {
                     System.out.println("PrintThread is running.......");
                     while(!ready);
                     System.out.println("number = "+number);
                 }
             }
         ​
             public static void main(String[] args) {
                 new PrintThread().start();
                 SleepTools.second(1);
                 number = 51;
                 ready = true;
                 SleepTools.second(5);
                 System.out.println("main is ended!");
             }
         }
        
      • 运行截图:主线程都结束了,子线程还在死循环,证明了主线程修改的变量在子线程中可见

        线程基本使用入门

      • 代码:加上volatile关键字

         package cn.enjoyedu.ch1.vola;
         ​
         import cn.enjoyedu.tools.SleepTools;
         ​
         /**
          * 类说明:演示Volatile的提供的可见性
          */
         public class VolatileCase {
             private volatile static boolean ready;
             private static int number;
         ​
             private static class PrintThread extends Thread{
                 @Override
                 public void run() {
                     System.out.println("PrintThread is running.......");
                     while(!ready);
                     System.out.println("number = "+number);
                 }
             }
         ​
             public static void main(String[] args) {
                 new PrintThread().start();
                 SleepTools.second(1);
                 number = 51;
                 ready = true;
                 SleepTools.second(5);
                 System.out.println("main is ended!");
             }
         }
        
      • 运行截图:子线程死循环退出了,证明了主线程修改的变量在子线程中可见

        线程基本使用入门

    • volatile能取代synchronized关键字吗?

      • 不能,volatile只能保证可见性,并不能保证操作原子性(一个数据在多线程环境下的并发安全性)

      • 应用场景:一写多读

        • 只有一个线程写变量,多个线程读取这个变量

ThreadLocal:可以保证多线程的安全性

  • ThreadLocal与synchronized的区别:

    • synchronized:使用锁机制,保证在多线程环境下,同一时刻只能有一个线程访问某段代码/变量

    • ThreadLocal:为每一个线程提供变量的副本(只管理自己的)

      • 实现了线程的隔离:每个线程只访问自己的数据,保证安全性

      • 在Spring中的事务中使用了这个ThreadLocal

        • 每个线程保存自己的链接,将链接绑在线程上作为参数
  • 使用细节

    1. 可以对ThreadLocal初始化赋值
    2. 可以使用,get/set/remove进行操作
  • 验证ThreadLocal为每一个线程提供变量的副本

    • 代码:

       package cn.enjoyedu.ch1.threadlocal;
       ​
       /**
        *类说明:演示ThreadLocal的使用
        */
       public class UseThreadLocal {
          
          //TODO
           private static ThreadLocal<Integer> threadLocal
                   = new ThreadLocal<Integer>(){
               //在new的时候可以进行初始化
               @Override
               protected Integer initialValue() {
                   return 1;
               }
           };
       ​
           /**
            * 运行3个线程
            */
           public void StartThreadArray(){
               Thread[] runs = new Thread[3];
               for(int i=0;i<runs.length;i++){
                   runs[i]=new Thread(new TestThread(i));
               }
               for(int i=0;i<runs.length;i++){
                   runs[i].start();
               }
           }
           
           /**
            *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
            */
           public static class TestThread implements Runnable{
               int id;
               public TestThread(int id){
                   this.id = id;
               }
               public void run() {
                   System.out.println(Thread.currentThread().getName()+":start");
                   //TODO
                    Integer s = threadLocal.get();//取的时候可以用get
                    s = s+id;
                    threadLocal.set(s);//在放入的时候可以set,还可以调用remove()将其清除掉
                    System.out.println(Thread.currentThread().getName()+":"+
                           +threadLocal.get());
       ​
               }
           }
       ​
           public static void main(String[] args){
              UseThreadLocal test = new UseThreadLocal();
               test.StartThreadArray();
           }
       }
      
    • 运行截图:

      线程基本使用入门

  • ThreadLocal的实现:Map(当前线程,Object)

    • 工作原理:为每个变量保存副本

      • 当来了一个新的线程,那么就为其创建一个独有(这个ThreadLocalMap是静态的)的ThreadLocalMap(ThreadLocal,object)//object就是要保存的东西

      • 示意图:

        线程基本使用入门

    • get方法:

      • 执行流程:

        1. 拿到当前线程
        2. 将当前线程传给了getMap
      • 源代码:

         public T get() {
             Thread t = Thread.currentThread();
             ThreadLocalMap map = getMap(t);//拿到当前线程独有的ThreadLocalMap
                if (map != null) {//如果map不为空,去拿里面的entry
                     ThreadLocalMap.Entry e = map.getEntry(this);
                     if (e != null) {
                         @SuppressWarnings("unchecked")
                         T result = (T)e.value;
                         return result;
                     }
                 }
        
      • getMap:返回了一个当前线程的threadLocals

         ThreadLocalMap getMap(Thread t) {
             return t.threadLocals;
         }
        
      • threadLocals:为ThreadLocal的成员变量ThreadLocalMap

         ThreadLocal.ThreadLocalMap threadLocals = null;
        
      • ThreadLocalMap(每个ThreadLocal对应唯一的ThreadLocalMap):内置了Entry<ThreadLocal <?> k,Object v),并且Entry是一个初始化大小为16的数组(因为有可能定义多个ThreadLocal< ? >)

        线程基本使用入门

      • 当map不为空,去拿到map里面的 Entry数组(通过位运算)

         private Entry getEntry(ThreadLocal<?> key) {
             int i = key.threadLocalHashCode & (table.length - 1);
             Entry e = table[i];
             if (e != null && e.get() == key)
                 return e;
             else
                 return getEntryAfterMiss(key, i, e);
         }
        
    • set:内部会解决hash冲突

  • ThreadLocal内存泄漏分析:

    • 使用JDK自带工具查看java进程内存变化:

      • 代码:

         package cn.enjoyedu.ch1.threadlocal;
         ​
         import java.util.concurrent.LinkedBlockingQueue;
         import java.util.concurrent.ThreadPoolExecutor;
         import java.util.concurrent.TimeUnit;
         ​
         /**
          * 类说明:ThreadLocal造成的内存泄漏演示
          */
         public class ThreadLocalOOM {
             private static final int TASK_LOOP_SIZE = 500;
         ​
             //定义了大小为5的线程池
             final static ThreadPoolExecutor poolExecutor
                     = new ThreadPoolExecutor(5, 5, 1,
                     TimeUnit.MINUTES,
                     new LinkedBlockingQueue<>());
         ​
             static class LocalVariable {
                 private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
             }
         ​
             final static ThreadLocal<LocalVariable> localVariable
                     = new ThreadLocal<>();
         ​
             //在每一个Runnable任务中只是简单new了LocalVariable,同时申请了5MB的内存
             public static void main(String[] args) throws InterruptedException {
                 for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
                     poolExecutor.execute(new Runnable() {
                         public void run() {
                             new LocalVariable();
                             System.out.println("use local varaible");
                         }
                     });
         ​
                     Thread.sleep(100);
                 }
                 System.out.println("pool execute over");
             }
         ​
         }
        
      • 内存分析:观察到就堆内存持续在25MB进行变化(五个线程同时运行,每个线程分配5MB)

        线程基本使用入门

      • 修改代码:引入ThreadLocal,为每个线程保存一个变量的副本

          public static void main(String[] args) throws InterruptedException {
                 for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
                     poolExecutor.execute(new Runnable() {
                         public void run() {
                             localVariable.set(new LocalVariable());
         ​
                             System.out.println("use local varaible");
                         }
                     });
         ​
                     Thread.sleep(100);
                 }
                 System.out.println("pool execute over");
             }
         ​
        
      • 内存监视:发现内存一度达到150MB+

        线程基本使用入门

      • 再次修改代码:在set之后紧跟remove

          public void run() {
                             localVariable.set(new LocalVariable());
         ​
                             System.out.println("use local varaible");
                             localVariable.remove();
                         }
        
      • 内存监视:发现这个跟未引入ThreadLocal效果一致

        线程基本使用入门

        意思就是,引入了ThreadLocal造成了差不多70MB+的内存泄漏问题

    • 为什么会发生内存泄漏?

      • 查看ThreadLocalMap源码:发现使用了弱引用

             static class ThreadLocalMap {
                 static class Entry extends WeakReference<ThreadLocal<?>> {
                     /** The value associated with this ThreadLocal. */
                     Object value;
         ​
                     Entry(ThreadLocal<?> k, Object v) {
                         super(k);
                         value = v;
                     }
                 }
                 private static final int INITIAL_CAPACITY = 16;
         ​
                 private Entry[] table;
             }
        
      • 当发生GC,ThreadLocal一定会被回收

      • 为什么发生内存泄漏:数据不可达但是它真实存在

        • 示意图:

          线程基本使用入门

        • 流程分析:

          上面那条线

          1. 一个ThreadLocal实例,对应了一个线程独有的(这个static的)ThreadLocalMap,KEY为ThreadLocal实例,value就是我们set进去的东西;堆中的ThreadLocal实例在栈上是有一个引用的(强)

          2. 一旦发生GC,这个ThreadLocal就会被回收,因为ThreadLocal被虚引用包裹,此时将栈上的ThreadLocal实例的引用置空(null),此时并不会回收Entry里面的value(之间是一条虚引用),但是map里面的key是ThreadLocal,这个都置空了,value就访问不到了

            上面那条线就断了

          下面那条线还存在

          1. 问题是在每一个线程中就有一个ThreadLocalMap,map里面又有一个Entry,这个Entry又指向了里面的value

          2. 但是KEY已经不见,相当于value存在但是无法引用,所以造成了内存泄漏

            此时需要将当前线程一起回收

        • 使用ThreadLocal的细节:set之后跟上remove

    • 为什么,看起来分配了2500MB(一个线程此次分配5MB,共执行500次,并且共有5个线程),但是在内存检测过程中,对内内存稳定在25MB?get/set中还是会清除,但是不及时

      • 回到get源码:

         public T get() {
             Thread t = Thread.currentThread();
             ThreadLocalMap map = getMap(t);
             if (map != null) {
                 //这个里面有个getEntry(this)
                 ThreadLocalMap.Entry e = map.getEntry(this);
                 if (e != null) {
                     @SuppressWarnings("unchecked")
                     T result = (T)e.value;
                     return result;
                 }
             }
        
      • getEntry(this):

         private Entry getEntry(ThreadLocal<?> key) {
             int i = key.threadLocalHashCode & (table.length - 1);
             Entry e = table[i];
             if (e != null && e.get() == key)
                 return e;
             else
                 //这个里面有一个getEntryAfterMiss
                 return getEntryAfterMiss(key, i, e);
         }
        
      • getEntryAfterMiss:

         private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
             Entry[] tab = table;
             int len = tab.length;
         ​
             while (e != null) {
                 ThreadLocal<?> k = e.get();
                 if (k == key)
                     return e;
                 if (k == null)
                     //这个里面还有个expungeStaleEntry方法,这个方法就是专门用来清除key为null的,ThreadLocalMap的;
                     expungeStaleEntry(i);
                 else
                     i = nextIndex(i, len);
                 e = tab[i];
             }
             return null;
         }
        
      • 在set方法中也有相关的东西

        线程基本使用入门

      • 在remove里面也有相关的东西:

        线程基本使用入门

    • 为什么JDK要定义成弱引用

      • 如果使用强引用,那么一定会发生内存泄漏

        • 定义成弱引用,还有机会可以回收,就是处理ThreadLocalMap中的key为null
        • 定义成强引用的话,连这个检查的机会都没有
    • 四大引用

      • 强引用:正常new出的东西

        • 执行方法的时候,方法打包成栈帧,对象实例在堆上分配,引用在栈上面,引用指向了堆中的对象实例
        • 只要强引用存在,堆中的对象实例就不会被回收
      • 软引用(SoftRefence):虽然指向了,但是虚拟机要发生OOM了,就回收掉,不管栈上有无引用指向堆中实例,发生GC后,内存够,就不收这个

      • 弱引用(WeakReference):只要发生GC就一定会回收

      • 虚引用:随时都会被回收

  • ThreadLocal的线程不安全性:处理静态变量的时候

    • 多个线程调度静态变量,变量放在ThreadLocal里面去,此时发生线程不安全

      • 代码:

         package cn.enjoyedu.ch1.threadlocal;
         ​
         import cn.enjoyedu.tools.SleepTools;
         ​
         /**
          * 类说明:ThreadLocal的线程不安全演示
          */
         public class ThreadLocalUnsafe implements Runnable {
         ​
             public static Number number = new Number(0);
         ​
             public void run() {
                 //每个线程计数加一
                 number.setNum(number.getNum()+1);
               //将其存储到ThreadLocal中
                 value.set(number);
                 SleepTools.ms(2);
                 //输出num值
                 System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
             }
         ​
             public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
             };
         ​
             public static void main(String[] args) {
                 for (int i = 0; i < 5; i++) {
                     new Thread(new ThreadLocalUnsafe()).start();
                 }
             }
         ​
             private static class Number {
                 public Number(int num) {
                     this.num = num;
                 }
         ​
                 private int num;
         ​
                 public int getNum() {
                     return num;
                 }
         ​
                 public void setNum(int num) {
                     this.num = num;
                 }
         ​
                 @Override
                 public String toString() {
                     return "Number [num=" + num + "]";
                 }
             }
         ​
         }
        
      • 运行截图:

        线程基本使用入门

      • 原因:

        虽然说每个线程都保存了变量的一个副本,在各自的线程中独立操作;但是在此处究其根本,保存的是对象引用,这些引用都指向了堆中的同一个实例(因为这个对象被静态修饰了)

      • 解决办法一:去掉对象的static修饰

        • 每个线程保存的就是各自的对象的引用,不会引起安全性问题
      • 解决办法二:

        • 在初始化的时候为ThreadLocal里面的泛型指定初始值

          这个是没有解决的

  • 线程相互协作:通知模式

    • 场景:

      • 线程A中对类属性做修改,线程B持续监测类属性,满足条件后触发
    • 实现手段:

      1. 轮询:如果说线程B中有sleep函数,则会带来无端的资源浪费

      2. 使用wait()+notify():

        • 线程B调用wait函数,当满足条件后,notify()线程B--->触发线程B

        • wait和notify与notifyAll不是Thread的方法是Object的方法(针对某个对象)

        • 使用:一定要有synchronized包裹(不包裹,运行的时候就会抛异常)

           //等待范式
           sync(对象){
               while(条件不满足){
                   对象.wait()//调用wait()--->线程释放所持有的对象锁;当一个wait的线程被唤醒后,会去竞争锁,一旦拿到锁,接着就检查条件,条件满足,就进行业务逻辑
                  业务逻辑
                       
               }
               //条件满足
               具体的业务逻辑代码
           }
           //通知范式
           sync(对象){
               //业务逻辑,改变条件
               对象.notify/notifyAll();//调用后不会释放持有的对象锁
               //业务逻辑2,等到这个同步代码块执行完后,才释放对象锁
               这个notify/notifyAll会放在同步代码块的最后一行,因为它不释放对象锁
           }
          
    • wait与notify()实战

      • 定义了两个类:Express,TestWN

        • Express:这里面就有两个线程(等待公里数变化,等待地点变化),两种唤醒
        • TestWN:
      • 实现细节:

        • 关于notify与notifyAll的使用:一般情况下都是使用notifyAll

          • notify:只会唤醒,这个持有这把锁的任意一个线程(不能确定,唤醒信号不能向下传递)
          • notifyAll:唤醒所有持有这把锁的对象
        • 关于notify与notifyAll,wait

          • 是在对象上面等待和唤醒,需要注意如果等待的条件是类的属性,这个时候就会要注意细节

            • 一个对象一个条件就可以用notify

            • 一个对象多个条件用notifyAll

              • 或者直接去锁那个条件,但是这个是有点问题的,需要确保对象不改变,那种i++就锁不到
          • 必须在同步代码块中
          • wait函数后需要对异常进行处理:JDK对于内在阻塞方法都是具有中断机制的
      • 代码示意:

        Express:

         package cn.enjoyedu.ch1.wn;
         ​
         /**
          *类说明:快递实体类
          */
         public class Express {
             public final static String CITY = "ShangHai";
             private int km;/*快递运输里程数*/
             private String site;/*快递到达地点*/
         ​
             public Express() {
             }
         ​
             public Express(int km, String site) {
                 this.km = km;
                 this.site = site;
             }
         ​
             /* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
             public synchronized void changeKm(){
                 //TODO
                 this.km = 101;
                 notifyAll();//使用notify是不行的;唤醒了等待地点的线程,等待公里数没有被触发,因为
             }
         ​
             /* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
             public  synchronized  void changeSite(){
                 this.site = "BeiJing";
                 notifyAll();
             }
         //线程等待公里数的变化,当公里数小于100就不管
             public synchronized void waitKm(){
                 //TODO
                 while(this.km<100){
                     try {
                         wait();
                         System.out.println("Check Site thread["
                                 +Thread.currentThread().getId()
                                 +"] is be notified");
                     } catch (InterruptedException e) {//所有JDK实现的阻塞方法,都会有中断处理机制
                         e.printStackTrace();
                     }
                 }
                 System.out.println("the Km is "+this.km+",I will change db");
             }
         //等待地方的变化,当这个地点发生变化后进行处理
             public synchronized void waitSite(){
                 while(this.site.equals(CITY)){//快递到达目的地,这个地方使用while(当条件不满足时,这个线程继续休眠一直等到条件满足后,触发通知)
                     //用了if,它就只搞了一次了
                     try {
                         wait();
                         System.out.println("Check Site thread["+Thread.currentThread().getId()
                                 +"] is be notified");
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
                 //这个呼叫用户是不进行打印的
                 System.out.println("the site is "+this.site+",I will call user");
             }
         }
        

        TestWN

         package cn.enjoyedu.ch1.wn;
         ​
         /**
          *类说明:测试wait/notify/notifyAll
          */
         public class TestWN {
             private static Express express = new Express(0,Express.CITY);
         ​
             /*检查里程数变化的线程,不满足条件,线程一直等待*/
             private static class CheckKm extends Thread{
                 @Override
                 public void run() {
                    express.waitKm();
                 }
             }
         ​
             /*检查地点变化的线程,不满足条件,线程一直等待*/
             private static class CheckSite extends Thread{
                 @Override
                 public void run() {
                    express.waitSite();
                 }
             }
         ​
             public static void main(String[] args) throws InterruptedException {
                 for(int i=0;i<3;i++){
                     new CheckSite().start();
                 }
                 for(int i=0;i<3;i++){
                     new CheckKm().start();
                 }
         ​
                 Thread.sleep(1000);
                 express.changeKm();//快递地点变化,这个时候使用notify,
                 // 会唤醒express对象实例加锁的任意一个线程(连条件都不能确定,并且唤醒后,唤醒信号就丢失了),notifyAll是全部唤醒
             }
         }
        
转载自:https://juejin.cn/post/7067235972215209992
评论
请登录