读《java并发编程》零星笔记

java并发编程
书中代码

线程安全

“当多个线程访问某个类时,不管运行时环境采用何种调度方式,或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的”。

  • 对象的状态是指存储在状态变量(实例或者静态域)中的数据。

  • “共享”意味着变量可以有多个线程同时访问,而“可变”意味着变量的值在其生命周期内可以变化。

  • 编写线程安全的代码的核心在于:要对状态访问操作进行管理,特别时对共享的(shared)和可变(Mutable)的状态的访问。

  • 一个对象是否需要是线程安全的,取决于它是否需要被多个线程访问,不需要被多线程访问,则不谈它的安全性。要使对象是线程安全的,需要采用同步机制来协同对象可变状态的访问,如果无法实现协同,则可能导致数据被破坏以及错误结果。

  • 无状态对象一定时线程安全的,如Servlet,就没有定义任何的属性。所以多个线程调用它的方法总能得到正确的结果。但是自定义的Servlet子类中如果定义了一些状态,那么共享对象需要同步机制来保证对象可变状态的访问。

内存可见性

  • 做到内存可见性的原则:对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行stroe、write操作)
    必看-内存可见性视频
  • 多个线程对共享变量进行读写操作时,如果线程A修改了共享变量,其它线程应该能够知道这个修改。实现方法有:synchronized、final、Volatile变量。
  • 枷锁来实现内存可见,内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。

深入理解Java虚拟机笔记---原子性、可见性、有序性

重排序

  • JVM为了能够充分利用多核处理器的强大性能,在缺乏同步的情况下,Java内存模型允许编译器对操作顺序进行重排序,并将数值寄存在寄存器中。此外,他还允许CPU对操作顺序进行重排序,并将计算值缓存在处理器特定的缓存中。(如果没有重排序存在,在编写并发代码时可以省去一些事,但是这是存在的所以需要做一些事情来防止对关键代码的重排序
  • 以下代码中主线程中的代码可能存在重排序,即在缺少同步情况下,JVM允许编译器和CPU对操作顺序进行重排序,那么最后 number = 42; ready = true;语句的执行顺序就会颠倒变为 ready = true;number = 42; 。这对ReaderThread线程来说是个悲剧,因为他可能先读到ready=true,在执行还没等主线程为number设置42,就执行了输出number操作,结果就为0。那么这种结果不是我期望的42结果。这就是由于多个线程之间对内存写入操作的不可见导致的结果。实现内存可见性如上面所示。
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) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

深入理解Java内存模型(二)——重排序

  • java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性

正确性

某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态s,以及定义各种后验条件(Postcondition)来描述对象操作的结果。

  • 补充:不变性条件可能涉及对象的多个状态,比如,对象的a状态变化时b也要变化,如果这个不变性在多线程中会被破坏了则该类不是线程安全的,因此当不变性台条件涉及多个变量时,当更新某一个变量时,需要在同一个原子操作中对其它变量同时进行更新。可用锁实现。

非原子性的64位操作

内存可见性和原子性:Synchronized和Volatile的比较

Java内存模型要求,变量的读取操作和写入操作必须时原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作和写操作分解为两个32位的操作,当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位,因此在多线程中使用共享且可变(某个线程会对该变量执行写操作)的long和double等类型的bain了也是不安全的,除非使用volatile来声明他们或者使用锁保护起来。(也许以后的处理器就都可以提供64位数值的原子操作)

volatile变量

  • java语言提供一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其它线程。当把变量声明位volatile类型后,编译器运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排。voldatilte变量不会被缓存到寄存器或者对其他处理器不可见的地方,因此在读取volatile类型变量时总会返回最新写入的值。

  • 什么叫将变量的更新操作通知到其它线程?首先该变量是一个共享变量,可以被多个线程访问,就是上面讲的内存可见性

  • 不要过度使用volatile变量,仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用他们,如果在验证正确性时(某个类的行为与其规范完全一致)需要对可见性进行复杂的判断,那么就不要使用volatile变量。

  • volatile变量的一种典型用法:检查某个状态标记以判断是否退出循环。下面示例中,线程通过数绵羊的方法进入休眠。为了使这个示例能正确执行,asleep必须为volatile变量。否则当asleep被另一个线程修改时,执行判断的线程却发现不了。为什么发现不了?答:JVM在server模式中(另一个模式client做了相对较少的优化)对代码进行了更多的优化,其中就包括将循环中未被修改的变量提升到循环外部,对于该代码中,asleep在while中没有被修改,如果asleep不是volatile类型,那么JVM就会将asleep的判断条件提升到循环体外部,这将导致一个无线循环。

public class CountingSheep {
    volatile boolean asleep;
    void tryToSleep() {
        while (!asleep)
            countSomeSheep();
    }
    void countSomeSheep() {
        // One, two, three...
    }
}
  • volatile变量只能确保可见性,不能确保原子性,而加锁机制两种都可以。因为volatile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。在访问volatile变量时不会执行加锁操作,因为也就不会执行线程阻塞。

  • 当且仅当满足以下条件时才使用volatile变量:

    • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
    • 该变量不会与其它状态变量一起纳入不变性条件中。(在良好的规范中通常会定义各种不变性条件Invariant来约束对象的状态s,以及定义各种后验条件Postcondition来描述对象操作的结果,)
    • 在访问变量时不需要枷锁。
  • 补充不变性条件:不变性条件可能涉及对象的多个状态,比如,对象的a状态变化时b也要变化,如果这个不变性在多线程中会被破坏了则该类不是线程安全的,因此当不变性台条件涉及多个变量时,当更新某一个变量时,需要在同一个原子操作中对其它变量同时进行更新。可用锁实现。

    • 在LinkedList集合中存在多个不变性条件,其中一条如下:链表的第一个节点指针first和最后一个节点指针last的不变性关系。
/**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

细说Java多线程之内存可见性-视频

发布与逸出

  • 发布一个对象:是对象能能在当前作用域之外的代码中使用。当发布一个对象可能会间接地发布其他对象。当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。
  • 逸出:当某个不应该被发布的对象被发布时,这种情况称为逸出。
  • 发布对象方法:
  • 将对象的引用保持到一个公有静态变量中。
  • 指向该对象的应用保持到其它代码可以访问的地方
  • 在一个非私有方法中返回对象的引用。
  • 发布一个内部的类实例。如下代码:ThisEscape 发布EventListener时,也隐含的发布了ThisEscape实例本身,因为这个内部类实例中保护了对EventListener实例的隐含引用。这种非期望的发布就造成了ThisEscape对象的逸出。
public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }
    void doSomething(Event e) {
    }
    interface EventSource {
        void registerListener(EventListener e);
    }
    interface EventListener {
        void onEvent(Event e);
    }
    interface Event {
    }
}
  • 防止逸出:
    不要在构造过程中使用this引用。
    如果想着构造函数中注册一个事件监听器或者启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法。如下:
public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        };
    }
    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }
    void doSomething(Event e) {
    }
    interface EventSource {
        void registerListener(EventListener e);
    }
    interface EventListener {
        void onEvent(Event e);
    }
    interface Event {
    }
}

竞态条件与复合操作

  • 当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。出现竞态条件,就可能会造成线程不安全。
    下面代码就显示了延迟初始化中的竞态条件,它破坏了这个类的正确性。
public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }
}

以下代码存在竞态条件,count++包含了”读取-修改-写入”三个操作。

public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++count;
        encodeIntoResponse(resp, factors);
    }
....
}
  • 常见竞态条件:先检查后执行、读取-修改-写入。
  • 避免竞态条件产生的线程不安全问题,这些操作应该是原子性的。即为了确保线程安全性,把”先检查后执行“、”读取-修改-写入“等操作统称为复合操作:包含了一组必须以原子方式执行的操作。
  • 实现复合操作的原子性:枷锁机制(可实现多个状态的原子操作)、原子变量类(针对只有一个状态)
  • 为了实现这种复合操作的原子性可以使用加锁机制。对于一些只包含一个状态的复合操作可以使用java.until.concurrent.atomoc包中包含的一些原子变量类来解决。
  • java.until.concurrent.atomoc包中包含的一些原子变量类,用于实现在数值和对象引用上的原子状态转换。

如下代码:使用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问都是原子的,

public class CountingFactorizer extends GenericServlet implements Servlet {
    private final AtomicLong count = new AtomicLong(0);

    public long getCount() { return count.get(); }
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        count.incrementAndGet();
        encodeIntoResponse(resp, factors);
    }
}

线程封闭

  • 定义:当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭技术(Thread Confinement),它是实现现在安全性的最简单方式之一。

  • 案例:JDBC的Connection对象就使用了线程封闭技术,JDBC规范并不要求Connection对象必须是线程安全的。在典型的服务器应用程序中,线程从连接池中获得一个Connection对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。由于大多数请求(如Servlet)都是有单个线程采用同步技术的方式来处理,并且再Connection对象返回之前,连接池不会再将他分配给其它线程,因此这中连接管理模式再处理请求时隐含的将Connection对象封装再线程中。
    Servlet的多线程和线程安全

  • 注:应用程序服务器提供的连接池是线程安全的连接池通常会由多个线程访问,因此非线程安全的连接池是毫无意义的。

  • 实现线程封闭性的技术:Java没有强制规定某个变量必须有锁来保护,同样也无法强制将对象封装再某个线程中。线程封闭是再程序设计中考虑的一个因素。但是Java语言及其核心类库提供了一些机制来帮助维持线程封闭性,例如栈封闭和ThreadLocal类,还有一种是Ad-hoc线程封闭,即便如此程序员也需要负责确保封闭性在线程中的对象不会从线程中逸出。

Ad-hoc线程封闭(脆弱,不推荐)

维护线程封闭性的职责完全有程序实现来承担,很脆弱不建议使用。
如下代码通过Map实现线程封闭性:
其中static类型的data是线程间共享的,但是为了实现数据和线程绑定,所以通过map来存放不同线程操作的数据data,data的获取和线程也是绑定的,这样就实现了data数据在单线程中访问了,不会与其它线程共享,注map是和其它线程共享的, 这样每个线程中操作的A、B类都是共享和本线程绑定的那个data,从而不会冲突和出错。

Ad-hoc线程封闭

栈封闭性

  • 栈封闭性是线程封闭性的一种特例,再栈封闭中,只能通过局部变量才能访问对象。
  • 局部变量固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这各栈。栈封闭(也被称为线程内部使用或者线程局部局部使用,不要与核心类库中的ThreadLocal混淆)比Ad-hoc线程封闭更易维护。
  • 如下:对于loadTheArk方法的局部变量numPairs,无论如何也不会破环栈的封闭性。因为任何方法都无法获得对基本类型的引用,因此java语言的这种语义就确保了基本类型的局部变量始终封闭在线程内。
  • 在维护对象引用的栈封闭性时,程序员需要确保被引用的对象不会逸出。loadTheArk方法中animals引用指向了一个SortedSet对象,此时只有一个引用指向了集合animals,这个引用被封闭在了局部变量中,因此也被封闭在执行线程中。但是,如果发布了对集合animals的引用,那么封闭性也被破坏,并导致对象animals逸出。
public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;

        // animals confined to method, don't let them escape!
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a : animals) {
            if (candidate == null || !candidate.isPotentialMate(a))
                candidate = a;
            else {
                ark.load(new AnimalPair(candidate, a));
                ++numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }

ThreadLocal类(重点)

  • 这个类能使线程中的某个值与保存该值的对象关联起来。ThreadLocal提供类get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
  • ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。如:单线程应用中可能会位置一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都需要传递一个Connection对象(实现线程内数据共享)。
  • 如下代码,通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。
public class ConnectionDispenser {
    static String DB_URL = "jdbc:mysql://localhost/mydatabase";

    private ThreadLocal<Connection> connectionHolder
            = new ThreadLocal<Connection>() {
                public Connection initialValue() {
                    try {
                        return DriverManager.getConnection(DB_URL);
                    } catch (SQLException e) {
                        throw new RuntimeException("Unable to acquire Connection, e");
                    }
                };
            };

    public Connection getConnection() {
        return connectionHolder.get();
    }
}
  • 更多案例:1.如果需要将一个单线程应用移植到多线程环境中,通过将共享的全局变量转换为ThreadLocal(如果全局变量的语义允许),可以维持线程安全性。2.在EJB调用期间,J2EE容器需要将一个事务上下文(Transaction Context)与某个执行中的线程关联起来。通过将Transaction Context保存在静态的ThreadLocal对象中,可以很容易实现这个功能。

  • 实现机制:可以将ThreadLocal<T>视为包含了Map<Thread,T>对象,其中保存了特定于该线程的值,但ThreadLocal的实现并非如此,这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾回收。

  • 注意:不要滥用ThreadLocal,例如将所有全局变量都作为ThreadLocal变量,或者作为“隐藏”方法参数的手段(设为全局就不需要通过参数传递过来)。ThreadLocal变量类似全局变量,它降低了代码的可重用性,并在类之间引入隐含的耦合性(一个线程中或涉及操作多个类,这些类中有的方法就有可能依赖ThreadLocal变量),因此要格外小心。

线程封闭性、线程内数据共享

  • 一个线程T1内操作多个对象A、B时,A、B中操作的数据都属于该线程范围内的。

  • 比如:javaWeb中存钱操作,会操作数据库。

  • 张三开启T1线程获取连接connection,然后T1内操作取钱类A取钱,操作记录类B记录日志,然后进行conn提交。

  • 李四开启T2线程获取连接connection,然后T1内操作取钱类A取钱,操作记录类B记录日志,然后进行conn提交。

  • 线程间独立:以上两个线程获取的connection应该是独立的,只属于该线程,如果T1和T2共享一个connection,那么如果张三转入钱后还没来的急转出,就被李四提前转出了,那么就会出错。 (即实现线程封闭性)

  • 线程内共享:每个线程中的connection对象是对该线程中所有被操作对象都是共享的。

Paste_Image.png

不变性

  • 满足同步需求的另一种方法是使用不可变对象(Immutable Object).

  • 当满足以下条件,对象才是不可变的:

  • 对象创建以后其状态就不能修改。(比如:可通过关键字-》简单类型状态、程序控制实现-》引用类型状态)

  • 对象的所有域都是final类型(有例外)。

  • 对象是正确常见的(在创建对象期间,this引用没有逸出)

  • 不可变性并不等于将对象中所有的域都声明为final类型,即使都为final类型,这个对象也仍然是可变的,因为final域可以保存可变对象的引用。

  • 如下代码:在不可变对象基础上构建不可变类,尽管Set对象是可变的,但从ThreeStooges设计中可以看到,在Set对象构造完成后,无法对其进行修改。(程序控制)

public final class ThreeStooges {
    private final Set<String> stooges = new HashSet<String>();

    public ThreeStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }
    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
    public String getStoogeNames() {
        List<String> stooges = new Vector<String>();
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
        return stooges.toString();
    }
}
  • 这时候你可能会郁闷,没有不将域声明为final也可以啊,为什么要设为final。答:1.(自己理解)final域能确保初始化过程的安全性。2.其次通过将域声明为final类型,也相当于告诉维护人员这些域是不会变化的。3.良好的编程习惯。

Final域

  • fianl类型的域是不能修改的,但是如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的。
  • 在JMM中,final域能确保初始化过程的安全性

安全发布

不正确的发布:正确的对象被破坏

final域的内存语义

不可变对象与初始化安全性

安全发布的常用模式

详解Java中的clone方法 -- 原型模式
string 在clone()中的特殊性 (转载)

--------待更新

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容