线程安全性和对象的共享

参考书籍:《Java并发编程实战》

前言:理论指导实践

  • 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。共享意味着变量可以由多个线程同时访问,而可变则意味着变量的值在其生命周期内可以发生变化。只要有数据在多个线程之间共享,就要用正确的同步。

  • 我们更侧重于如何防止在数据上发生不受控的并发访问。

  • 一个对象是否需要线程安全的,取决于他是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。要使对象是线程安全的,需要采用同步机制来协同对对象可变状态的访问。如果无法实现协同,那么可导致数据破坏以及其他不该出现的结果。

  • 当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。

  • Java的主要同步机制是关键字synchronized,它提供了一种独占的加锁机制。但同步这个术语还包括volatile类型的变量,显示锁(Explicit Lock)以及原子变量。

如果多个线程访问同一个可变的状态变量时没有合适的同步,那么程序就会出现错误,有三种方式可以修复这个问题:

  1. 不在线程之间共享该状态变量 

  2. 将状态变量修改为不可变的变量 

  3. 在访问状态变量时使用同步 

 

线程安全性

先来看一个示例代码:

package thread;

class UnsafeThread {

    private int value;

    public int getNext() {
        return value++;
    }
}


public class TestUnsafeThread {

    public static void main(String args[]) {

        final UnsafeThread unsafeThread = new UnsafeThread();

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread() {
                public void run() {
                    System.out.println(unsafeThread.getNext());
                }
            };
            t.setName("thread-" + i);
            t.start();
        }
    }
}

结果:

1203456789

从结果看,不符合线程安全性的定义,所以线程不是安全的,如何定义一个线程安全的类,这个类的代码应该是这样的。

package thread;

class UnsafeThread {

    private int value;

    public int getNext() {
        synchronized (this) {
            return value++;
        }
    }
}


public class TestUnsafeThread {

    public static void main(String args[]) {

        final UnsafeThread unsafeThread = new UnsafeThread();

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread() {
                public void run() {
                    System.out.print(unsafeThread.getNext());
                }
            };
            t.setName("thread-" + i);
            t.start();
        }
    }
}

要对value共享变量的访问加锁,进行同步访问。

---什么是线程安全性

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

---原子性

an atomic action is one that effectively happens all at once. an atomic action cannot stop in the middle.『原子性』很明确地说明: 原子操作一旦开始执行,必将在线程切换前完成。

----竞态条件Race Condition

在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,正式的名字:

竞态条件(Race Condition)

常见的竞态条件类型是先检查后执行(Check-Then-Act)、“读取-修改-写入”等

在java.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。

延迟初始化中的竞态条件

在LazyInitRace中包含了一个竞态条件,他可能会破坏这个类的正确性。假定线程A和线程B同时执行getInstance方法。A看到instance为空,因而创建一个新的实例。B同样需要判断instance是否为空。此时的instance是否为空取决于不可预测的时序,包括线程的调度方式,以及A需要花多长时间初始化实例并设置instance。如果当B检查时,instance为空,那么两次调用getInstance时可能会得到不同的结果,即使getInstance通常会被认为返回相同的实例。

package com.lyx;
public class LazyInitRace {
    private Integer instance = null;
    /** 
     * 读取-修改-写入---复合操作 
     * 如果两个线程同时执行getInstance, 
     * 此时的instance是否为空取决于不可预测的时序 
     * 
     * 要避免该竞态条件,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用 
     * 这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在 
     * 修改状态的过程中。 
     * @return 
     */ 
    public Integer getInstance(){ 
        if (instance==null){ 
            instance = new Integer(0); 
        } 
        return instance; 
    }
}

 

加锁机制

---内置锁

Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)

同步代码块包括两部分:一个作为锁的对象的引用,一个作为由这个锁保护的代码块

synchronized(lock){
    //访问或修改由锁保护的共享状态
}

每个Java对象都可以用做一个实现同步的锁,这些锁称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)

线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出,获得内置锁的唯一途径是进入由这个锁保护的同步代码快或方法。

‍‍Java的内置锁相当于一种互斥锁,这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A将永远地等下来。‍‍

 

---锁的重入Reentrancy

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞,然而,由于内置锁是可以重入的,如果某个线程试图获取一个已经由他自己持有的锁,那么这个请求就会成功。如下示例代码,

public class Widget {
    public synchronized void doSomething() {
        ...
    }
}
 
public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();//若内置锁是不可重入的,则发生死锁
    }
}

 

---用锁来保护状态

如果在符合操作的执行过程中持有一个锁,那么会使复合操作成为原子操作。然而,仅仅将复合操作封装到一个同步代码块中是不够的。

如果用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步。而且,当使用锁来协调对某个变量的访问时,在访问变量的所有位置都要使用同一个锁。

锁能使其保护的代码块路径以串行形式来访问。对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有一个同一个锁,在这种情况下,我们称状态变量是这个锁保护的。

一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。

 

---活跃性和性能

无论是执行计算密集的操作,还是在执行某个可能阻塞的操作,如果持有锁的时间过长,那么都将会带来活跃性或性能问题

同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的错误是,认为关键字synchronized只能用于实现原子性或者确定”临界区“。同步还有另一个重要的方面内存可见性(Memory Visibility)。不仅希望防止某个线程正在使用对象状态而两一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。

 

对象的共享

内存可见性(memory visibility):我们不仅希望防止某个线程正在使用对象状态二另一个线程同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。如果没有同步,这种情况就无法实现。你可以通过显示的同步或者类库内置的同步来保证对象被安全的发布。

---可见性(Memory Visibility)

不仅要防止某个线程正在使用对象状态而另一个线程在同时修改状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。

为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

package com.lyx;
public class NoVisibility {
    private static boolean ready = false; 
    private static int number;
    private static class ReaderThread extends Thread{ 
         public void run(){ 
             while (!ready){ 
                 System.out.println("hello"); 
                 Thread.yield(); 
             } 
             System.out.println(number); 
         } 
    }
    public static void main(String args[]){ 
        for (int i=0;i<6;i++){ 
            new ReaderThread().start(); 
        }
        /** 
         * 无法保证主线程写入的ready值和number值对读线程来说是可见的 
         */ 
        number = 32; 
        ready = true; 
    } 
}
运行结果:
hello 
32 
hello 
hello 
32 
32 
32 
32 
32

每次运行结果不一样(运行时序不同)

 

线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术称为线程封闭。Swing中的可视化组件和数据模型对象都不是线程安全的,Swing通过将他们封闭到Swing中的事件分发线程中来实现线程安全性。线程封闭技术的常见应用是JDBC中的Connection对象。

---ThreadLocal

ThreadLocal对象通常用于防止对可变的单实例变量或全局变量进行共享。例如在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时传递一个Connect对象。由于JDBC的连接对象不一定是线程安全的,因此当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。

当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都需要分配该临时对象,就可以使用这个技术。

ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这样处理后,能够优雅的解决一些实际问题,比如Hibernate中的OpenSessionInView,就是使用ThreadLocal保存Session对象,还有我们经常用ThreadLocal存放Connection。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

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

那么 ThreadLocal 是如何实现为每个线程保存独立的变量的副本的呢?通过查看它的源代码,我们会发现,是通过把当前“线程对象”当作键,变量作为值存储在一个 Map 中。 

总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点: 

  • 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象 

  • 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦

ThreadLocal详见:http://my.oschina.net/xinxingegeya/blog/297192

==============END==============

转载于:https://my.oschina.net/xinxingegeya/blog/297017

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值