线程间通信

线程开始运行,拥有自己的栈空间,执行直到终止。

-->volatile和synchronized关键字

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中,每个线程拥有一份本地拷贝,这样做的目的是加速程序的运行)所以程序在执行的过程中,一个线程看到的变量不一定是最新的。

关键字volatile可以用来修饰字段,就是告知程序任何对该变量的访问均需要从共享内存获取(读取时将本地内存置为无效,从共享内存读取),而对它的改变必须同步刷新回共享内存。保证所有线程对变量访问的可见性。

synchronized关键字可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法和同步块中,它保证了线程对变量访问的可见性和排他性。(使用javap工具查看时发现,对同步块的实现使用了monitorenter和monitorexit指令。而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成)

执行方法的线程必须先获取到该对象的监视器才能进入同步块或同步方法,而没有获取到的将会被阻塞在同步区的入口处,进入BLOCK状态。

对象、监视器、同步队列以及执行线程之间的关系:

任意线程对Object(Object由synchronized保护)的访问,首先要获得监视器,如果获取失败则进入同步队列,线程状态变为BLOCK,当访问Object的前驱释放了锁,则释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

---等待/通知机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相关操作。那么前者是生产者,后者是消费者。JAVA语言如何实现?

简单的办法是让消费者线程不断循环检测变量是否符合预期。

这样做的话有如下几个问题:

1)难以确保及时性。在睡眠时,基本不消耗处理器资源,但是如果睡得过久,就不能及时发现条件已经变化。

2)难以降低开销。如果降低睡眠时间就会增加占用处理器的时间,增加了开销。

所以java通过内置的等待/通知机制来解决上述矛盾。

注意:

1)使用wait()/notify()和notifyall()时需要先对调用对象加锁。

2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放入该对象的等待队列

3)notiyf()或notifyall()方法调用后,等待线程依旧不会从wait()返回,需要调用此方法的线程释放锁,等待线程才有机会从wait()返回

4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyall()是将等待队列中的所有线程全部移到同步队列中去;

5)从wait()方法返回的前提是获得了调用对象的锁。

因此,生产者与消费者经典模式,对应伪代码如下:

sychronized(对象) {

         while(条件不满足) {

                   对象.wait();

          }

}

通知方伪代码如下:

synchronized(对象) {

         改变条件

         对象.notifyall()

}


-------管道输入/输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输媒介为内存。

管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter。前面两种面向字节,后面两种面向字符。

-----Thread.join()的使用

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间内没有终于,那么将会从该超时方法中返回。

当线程终止时,会调用线程自身的notifyall()方法,会通知所有等待在该线程对象上的线程

-----ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

比如在AOP(面向方面编程)中,可以在方法调用前的切入点执行begin()方法,而在方法调用后的切入点执行end()方法,这样依旧可以获得方法的执行耗时。

---ThreadLocal的实现原理和内存泄漏的问题(此部分转自博客http://blog.csdn.net/lhqj1992/article/details/52451136)

ThreadLocal类用来提供线程内部的局部变量,这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型。

总结:ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。

ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。

-->实现原理

ThreadLocal可以看做是一个容器,容器里面存放着属于当前线程的变量。ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法:

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

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

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

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

可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量,那ThreadLocal内部是如何为每一个线程维护变量副本的呢?

其实在ThreadLocal类中有一个静态内部类ThreadLocalMap(其类似于Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。

总之,为不同线程创建不同的ThreadLocalMap,用线程本身为区分点,每个线程之间其实没有任何的联系,说是说存放了变量的副本,其实可以理解为为每个线程单独new了一个对象。

-->内存泄露问题

在上面提到过,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.

所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。

你可能感兴趣的:(线程间通信)