• Java 多线程---volatile, ThreadLocal


    volatile关键字

    1、可见性

    Java内存模型规定所有的变量都是存在主存当中(物理内存),每个线程都有自己的工作内存(高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

    使用volatile关键字的话,如果线程1先从主存中读取变量值,当线程2进行修改时,会导致线程1的工作内存中缓存变量的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)。由于线程1的工作内存中缓存变量的缓存行无效,所以线程1再次读取变量的值时会去主存读取。

    2、有序性。

    在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

    3、原子性。volatile不保证原子性。

    参考:http://www.cnblogs.com/dolphin0520/p/3920373.html

    ThreadLocal

    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。当线程结束后,对应该线程的局部变量将自动被垃圾回收,也可以调用ThreadLocal类中的remove方法显式删除。在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。

    ThreadLocal类的方法:

    • void set(Object value)  设置当前线程的线程局部变量的值。
    • public Object get()    该方法返回当前线程所对应的线程局部变量。
    • public void remove()    将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
    • protected Object initialValue()    返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

    实例代码

    /**
     * Created by ix on 16/9/11.
     */
    public class SequenceNumber {
        //①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
        private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
            public Integer initialValue() {
                return 0;
            }
        };
    
        //②获取下一个序列值
        public int getNextNum() {
            seqNum.set(seqNum.get() + 1);
            return seqNum.get();
        }
    
        public static void main(String[] args) {
            SequenceNumber sn = new SequenceNumber();
            //③ 3个线程共享sn,各自产生序列号
            TestClient t1 = new TestClient(sn);
            TestClient t2 = new TestClient(sn);
            TestClient t3 = new TestClient(sn);
            t1.start();
            t2.start();
            t3.start();
        }
    
        private static class TestClient extends Thread {
            private SequenceNumber sn;
    
            public TestClient(SequenceNumber sn) {
                this.sn = sn;
            }
    
            public void run() {
                for (int i = 0; i < 3; i++) {//④每个线程打出3个序列值
                    System.out.println("thread[" + Thread.currentThread().getName() +
                            "] sn[" + sn.getNextNum() + "]");
                }
            }
        }
    }

    output:

    thread[Thread-0] sn[1]
    thread[Thread-1] sn[1]
    thread[Thread-2] sn[1]
    thread[Thread-1] sn[2]
    thread[Thread-0] sn[2]
    thread[Thread-1] sn[3]
    thread[Thread-2] sn[2]
    thread[Thread-0] sn[3]
    thread[Thread-2] sn[3]

    和Thread同步机制的比较

    ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

    在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

    ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

    概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

    管道

    在Java中提供了各种各样的输入/输出流Stream,使我们能够很方便地对数据进行操作,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读数据,通过使用管道,实现不同线程间的通信,而无须借助类似临时文件之类的东西。在JDK中使用4个类来使线程间可以进行通信:PipedInputStream, PipedOutputStream, PipedReader, PipedWriter。使用代码类似inputStream.connect(outputStream)或outputStream.connect(inputStream)使两个Stream之间产生通信连接。

    附:几种进程间的通信方式 

    • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
    • 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
    • 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
    • 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
    • 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
    • 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
    • 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  • 相关阅读:
    javaSE基础知识点
    java自定义注解
    java自定义线程池
    java写投票脚本自动化初探
    java线程安全初窥探
    锁的深入理解
    java守护线程与非守护线程
    java设计模式初探
    java内存模型初窥探
    uniapp中组件之间跳转遇到的问题
  • 原文地址:https://www.cnblogs.com/hesier/p/5629702.html
Copyright © 2020-2023  润新知