美文网首页Java篇
线程间的共享和协作

线程间的共享和协作

作者: w达不溜w | 来源:发表于2021-04-08 21:02 被阅读0次
线程间的共享

  线程运行拥有自己的栈空间,按既定的代码一步一步地执行,直到终止。如果每个线程仅仅只是孤立的运行,价值会很少,如果多个线程能够相互配合完成工作,如数据间的共享,协同处理事情,这样会带来巨大的价值。但是多个线程"同时"操作一个公共对象就会存在线程间安全问题。

synchronized

  Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,保证了线程对变量的可见性和排他性,又称为内置锁机制。

对象锁和类锁
  对象锁用于对象实例方法,或一个对象实例上,类锁用于类的静态方法或类的class对象上。

public class SynTest {
    private static long count = 0;
    private Object lock = new Object();//作为一个锁
    private Object sLock = new Object();

    /*****对象锁*****/

    /**
     * 锁非静态变量
     * 锁住同一变量的方法共享同一把锁
     */
    public void incCount3() {
        synchronized (lock) {
            count++;
        }
    }

    /**
     * 锁this
     * this就是当前对象实例,所有使用synchronized (this)共享同一把锁
     */
    public void incCount1() {
        synchronized (this) {
            count++;
        }
    }

    /**
     * 锁非静态方法
     * 等价于锁this
     */
    public synchronized void incCount2() {
        count++;
    }

    

    /*****类锁*****/


    /**
     * 锁静态变量
     * 静态变量和类信息一样也是存在方法区并且整个JVM只有一份,所以加在静态变量上可以达到类锁的目的。
     */
    public void incCount4() {
        synchronized (sLock) {
            count++;
        }
    }

    /**
     * 锁静态方法
     * 静态方法也是存在方法区并且整个JVM只有一份,所以加在静态变量上可以达到类锁的目的。
     */
    public static synchronized void incCount5() {
            count++;
    }

    /**
     * 锁Class
     */
    public void incCount6() {
        synchronized (SynTest.class) {
            count++;
        }
    }


    //线程
    private static class Count extends Thread {

        private SynTest synTest;

        public Count(SynTest synTest) {
            this.synTest = synTest;
        }

        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                synTest.incCount1();//count = count+1000
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynTest synTest = new SynTest();
        //启动两个线程
        Count count1 = new Count(synTest);
        Count count2 = new Count(synTest);
        count1.start();
        count2.start();
        Thread.sleep(50);
        System.out.println(synTest.count);//2000
    }
}
volatile

最轻量的同步机制,volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了这个变量的值,这个新值对其他线程是立即可见的。

public class VolatileTest {
    private volatile static boolean stop = false;//任务是否停止

    private static class WorkThread extends Thread {
        @Override
        public void run() {
            System.out.println("Work start...");
            while (!stop) ;//无限循环
            System.out.println("Work end");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new WorkThread().start();
        Thread.sleep(1000);
        stop=true;
        System.out.println("Main end");
    }
}

不加volatile时,WorkThread线程无法感知主线程修改了stop的值,从而不会退出循环。而加了volatile后,WorkThread线程可以感知主线程修改了stop的值立即退出循环。

但是volatile不能保证数据在多个线程下同时写时的安全性。

public class VolatileNoSafeTest {

    private volatile long count = 0;

    public void incCount() {
        count++;
    }

    //线程
    private static class Count extends Thread {

        private VolatileNoSafeTest noSafeTest;

        public Count(VolatileNoSafeTest noSafeTest) {
            this.noSafeTest = noSafeTest;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                noSafeTest.incCount();//count = count+10000
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        VolatileNoSafeTest noSafeTest = new VolatileNoSafeTest();
        //启动两个线程
        Count count1 = new Count(noSafeTest);
        Count count2 = new Count(noSafeTest);
        count1.start();
        count2.start();
        Thread.sleep(50);
        System.out.println(noSafeTest.count);//20000
    }
}

所以volatile适用场景:一个线程写,多个线程读

ThreadLocal

  ThreadLocal和synchronized都是用于解决多线程并发访问。synchronized是利用的锁机制,使变量或者代码块在某一个时刻只能被一个线程访问(线程等待,牺牲时间解决冲突)。而ThreadLocal是为每一个线程提供了变量的副本,使得每个线程在某一时间访问的不是同一个对象,达到了隔离多个线程对数据的数据共享(牺牲空间解决冲突)。

/**
 *类说明:演示ThreadLocal的使用
 */
public class UseThreadLocal {
    //ThreadLocal变量,每个线程都有一个副本,互不干扰
    //存放Integer类型的对象,对它的读写操作都是线程安全的。
    private static ThreadLocal<Integer> threadLocal
            = new ThreadLocal<Integer>(){
        //这个initialValue()方法是一个延迟调用的方法,在线程第一次调用set或get时才初始化,并且仅执行一次,缺省则返回null。
        @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{
        private int id;
        public TestThread(int id){
            this.id = id;
        }
        public void run() {
            System.out.println(Thread.currentThread().getName()+":start");
            //获取当前线程所对应的线程局部变量
            Integer count = threadLocal.get();
            count = count+id;
            //设置当前线程的线程局部变量的值
            threadLocal.set(count);
            System.out.println(Thread.currentThread().getName()
                    +":"+ threadLocal.get());
            //将当前线程局部变量的值删除,目的是为了减少内存的占用。当线程结束后,对应线程的局部变量将自动被垃圾回收器回收,所以非必须,但是可以加快内存回收的速度。
            //threadLocal.remove();
        }
    }

    public static void main(String[] args){
        UseThreadLocal test = new UseThreadLocal();
        test.StartThreadArray();
    }
}

源码浅析

  //ThreadLocal.java
  public T get() {
      //获取当前线程
      Thread t = Thread.currentThread();
      //调用getMap获取线程对应的ThreadLocalMap
      ThreadLocalMap map = getMap(t);
      if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
              @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
          }
      }
      return setInitialValue();
  }

   public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //调用getMap获取线程对应的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //存在map就set,不存在则创建mao并set
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

   ThreadLocalMap getMap(Thread t) {
        //Thread中维护了一个ThreaLocalMap
        return t.threadLocals;
    }

   void createMap(Thread t, T firstValue) {
        //实例化一个新的ThreaLocalMap,并赋值给线程成员变量threadLocals 
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 //Thread.java
 /* ThreadLocal values pertaining to this thread. This map is maintained
  * by the ThreadLocal class. */
  ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到,每个线程持有一个ThreadLocalMap对象

static class ThreadLocalMap {

     static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            //key、value结构,key就是ThreadLocal,value是需要隔离的线程
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

      private static final int INITIAL_CAPACITY = 16;
      //用数组保存Entry,因为可能有多个变量需要线程隔离访问
      private Entry[] table;
      //...
}
ThreadLocal.png

小结:ThreadLocal如何做到线程隔离呢?每个Thread维护了一个自己独有的ThreadLocalMap,get与set都是根据同一个ThreadLocal实例去自己的ThreadLocalMap里面找,互不干扰。

ThreadLocal内存泄漏问题
ThreadLocal引用链如图所示:

threadoom.png
  ThreadLocal在外部没有强引用时,发生GC时会被回收,那么ThreadLocal中保存的key值就会变为null,也就没办法访问这些key为null的所对应的Entry中的value了,而Entry又被ThreadLocalMap对象引用,ThreadLocalMap对象又被Thread对象所引用,如果Thread一直不结束的话,就会一直存在一条强引用链:ThreadLocal Ref—>Thread—>ThreadLocalMap—>Entry—>value,而value永远不会被访问到,所以就存在了内存泄漏。

  ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期根Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

  ThreadLocal如何避免内存泄漏:在使用完ThreadLocal变量后,手动调用remove,防止ThreadLocalMap中Entry一直保持对value的强引用,导致value不能被回收。

线程间的协作

等待/通知机制
  是指一个线程A调用了对象O的wait()方法进入等待状态,另一个线程B调用了对象O的notify()或notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
  wait( )和notify( )/notifyAll( )都不属于Thread类,而是属于Object基础类,也就是每个对象都有wait( )和notify( )/notifyAll( )的功能,因为每个对象都有锁。

  • notify()
    通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该对象获得了对象的锁,没有获得锁的线程重新进入WAITING状态。
  • notifyAll()
    通知所有等待在该对象上的线程
  • wait()
    调用该方法的线程进入WAITLING状态,只有等待另外线程的通知或被中断才会返回。调用wait()方法后,会释放对象的锁。
  • wait(long)
    超时等待一段时间,这里的参数时间是毫秒,也就是等待n毫秒,如果没有就通知就超时返回。
  • wait(long,int)
    对于超时时间更细粒度的控制,可以达到纳秒。

等待和通知的标准范式
等待方遵循如下规则:

  synchronized (对象){
          while (条件不满足){
              对象.wait();
          }
          对应的处理逻辑
     }

1)获取对象的锁
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
3)条件满足则执行对应的逻辑
通知方遵循如下规则:

    synchronized (对象){
        改变条件
        对象.notifyAll();
     }

1)获取对象的锁
2)改变条件
3)通知所有等待在对象上的线程

/**
 *类说明:快递实体类
 *wait和notify/notifyAll使用示例
 */
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(){
        this.km = 101;
        notify();
    }

    /* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
    public  synchronized  void changeSite(){
        this.site = "BeiJing";
        notifyAll();
    }

    /*线程等待公里的变化*/
    public synchronized void waitKm(){
        while(this.km<100){
            try {
                wait();
                System.out.println("Check Site thread["
                                +Thread.currentThread().getId()
                        +"] is be notified");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("the Km is "+this.km);
    }

    /*线程等待目的地的变化*/
    public synchronized void waitSite(){
        while(this.site.equals(CITY)){//快递到达目的地
            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);
    }
}
/**
 *类说明:测试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();//快递公里数变化
    }
}

  在调用wait、notify/notifyAll方法之前,一定要对竞争资源进行加锁,即只能在同步方法或者同步块中调用wait、notify/notifyAll方法。
  假设有三个线程执行了obj.wait( ),那么obj.notifyAll( )则能全部唤醒tread1,thread2,thread3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,tread1,thread2,thread3只有一个有机会获得锁继续执行,例如tread1,其余的需要等待thread1释放obj锁之后才能继续执行。
  当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此,thread1,thread2,thread3虽被唤醒,但是仍无法获得obj锁。直到调用线程退出synchronized块,释放obj锁后,thread1,thread2,thread3中的一个才有机会获得锁继续执行。
  尽可能用 notifyAll(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我 们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

yield() 、sleep()、wait()、notify()/notifyAll()等方法对锁有何影响?

  • yield()、sleep()被调用后,都不会释放当前线程所持有的锁。
  • wait()被调用后,会释放当前线程持有的锁,而且当前线程被唤醒后,会重新去竞争锁,竞争到了锁之后才会执行wait方法后面的代码。
  • notify()/notifyAll()被调用后,对锁无影响,线程只有在synchronized同步代码执行完后才会释放锁,所以notify()/notifyAll()一般都是synchronized同步代码的最后一行。

相关文章

网友评论

    本文标题:线程间的共享和协作

    本文链接:https://www.haomeiwen.com/subject/lzsekltx.html