多线程系列提高(3)--对象的共享

来源:互联网 发布:去淘宝刻章会被钓鱼吗 编辑:程序博客网 时间:2024/05/16 19:55

一、可见性

在单线程环境中,如果向某个变量先写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值。然而,当读操作和写操作在不同的线程中执行时,就是另外一种情况。我们无法确保执行读操作的线程能够适时的看到其他线程写入的值,为了确保多个线程之间对内存心如操作的可见性,必须使用同步机制。
下面以代码为例:

package MultiThreading;import java.io.Reader;/** * Created by L_kanglin on 2017/4/13. *///在没有同步的情况下共享变量public class NoVisibility {    private static boolean ready;    private static int number;    private static class ReaderThread extends Thread{        public void run(){            while(!ready){                Thread.yield();            }            System.out.println(number);        }    }    public static void main(String[] args) throws InterruptedException {        ReaderThread thread=new ReaderThread();        thread.start();        //休眠1s        thread.sleep(1000);        number=42;        ready=true;    }}

在代码中,主线程和读线程都将访问共享变量ready和number。主线程启动读线程,然后将number设为42,并将ready设为true,读线程一直循环直到发现ready的值变为true,然后输出number的值。虽然NoVisibility看起来会输出42,单事实上可能输出0,或者根本无法终止。这是因为在diamante中没有使用足够的同步机制,因此无法保证主线程写入的ready值和number值对于读线程来说是可见的。

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其它线程来说是都是可见的。否则,如果一个线程在未持有正确锁的情况下读取某个变量,那么读到的可能是一个失效值。

二、volatile变量

volatile变量是一种稍弱的同步机制,用来确保将变量的更新操作通知到其它线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其它内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其它处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
由上面的一段话可以总结到volatile的功能主要有两个:
(1)内存的可视性机制:读取volatile类型的变量时总会返回最新写入的值。
(2)禁止指令重排序:JVM内存模型允许编译器对操作顺序进行指令重排序,有上层的Java–>c/c++–>字节码—>汇编机器语言,才操作完成。此时为了提高运行效率,可能会进行重排序,而volatile类型的变量则不会执行这个操作,因为已经告诉了编译器这个变量是共享的,不会与其它内存操作一起重排序。

因此在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量一种比synchronized关键字更轻量级的同步机制。

重点理解:
volatile变量对可见性的影响比volatile变量本身更为重要。当线程A首先写入一个volatile变量并且线程B随后读取该变量时,在写入volatile变量之前对A可见的所有变量的值,在B读取了volatile变量后,对B也是可见的。因此,从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块。

volatile的局限性
volatile的语义不足以确保递增操作(count++)的原子性(读–改–写),除非你能确保只有一个线程对变量执行写操作。
注意:加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

当且仅当满足以下所有条件时,才应该使用volatile变量:
(1)对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
(2)该变量不会与其它状态变量一起纳入不变性条件中。
(3)在访问变量时不需要加锁。

三、ThreadLocal类

线程封闭:当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭。它是实现线程安全性的最简单的方式之一。当某个对象封闭在一个线程中,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。
线程封闭在Java中主要有三种实现方式
(1)Ad-hoc 线程封闭:维护线程封闭性的职责完全由程序实现来承担。
(2)栈封闭:只能通过局部变量才能访问对象
(3)ThreadLocal类:这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get和set等访问接口或方法,这些方法为每个使用该变量的线程都保存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

/** * Created by L_kanglin on 2017/4/13. */public class connectionHolder {    private static ThreadLocal<Connection> connectionHolder=new ThreadLocal<Connection>(){        public Connection initialValue(){            return DriverManager.getConnection(DB_URL);        }    };    public static Connection getConnection(){        return connectionHolder.get();    }}

当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。例如,在Java 5.0之前,Integer.toString()方法使用ThreadLocal对象来保存一个12字节大小的缓存区,用于对结果进行格式化,而不是使用共享的静态缓存区(这需要使用锁机制)或者在每次调用时都分配一个新的缓存区。
当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。从概念上看,你可以将ThreadLocal视为包含了Map

四、不变性

满足同步需求的另一种方法是使用不可变对象,如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。线程安全性是不可变对象的固有属性之一,它们的不变性条件是由构造函数创建的,只要它们的状态不改变,那么这些不变性条件就得以维持。
注意:不可变对象一定是线程安全的。

0 0
原创粉丝点击