Java并发编程实战(一)线程安全

第一章 基础知识(线程安全)

线程安全的核心问题:处理对象状态的问题。如果要处理的对象是无状态的(不变性),或者可以避免多个线程共享的(线程封闭),那么我们可以放心,这个对象可能是线程安全的。当无法避免,必须要共享这个对象状态给多线程访问时,这时候才用到线程同步的一系列技术(锁)。

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


处理多线程的大方向:如果能避免多线程问题就避免多线程问题,实在避免不了了再处理。


处理多线程问题可以考虑的几个点:

1、能否做成无状态的不变对象,无状态是线程安全的。(不变性)

2、能否线程封闭(线程封闭)

3、采用何种同步技术(锁)


多线程中需要了解的三个特性:

原子性:sychronized

可见性:sychronized、volatile、final

有序性:sychronized、volatile


内置锁:sychronized(原子性,可见性,有序性)

因为是阻塞的,虽然简单,性能却非常低。

重入:某个线程试图获得一个已经由他自己持有的锁,那么这个请求就会成功。重入避免了死锁情况的发生。

发布与逸出:

发布:对象能够在当前作用域之外的代码中使用。(发布内部对象可能会破坏封装性,并使得程序难以维持不变性条件)

逸出:当某个不应该发布的对象被发布时,称为逸出。

发布对象的方法:

1、将对象的引用保存到一个公有的静态变量中(其他对象可以间接发布)

public class Test {  
    public static List<People> list;  
    public Test(){  
        list = new ArrayList<People>();  
    }  
}

2、非私有方法中返回一个引用,发布返回的对象。

public class Test {  
    private String[] strs = {"AB","BA"};  
  
    public String[] getStrs() {  
        return strs;  
    }  
}

3、将对象引用传递给外部方法

当把一个对象传递给外部方法时,就相当于发布了这个对象 

public class Test {  
    public void get(Object obj){ //obj对象逸出  
        ...  
    }  
} 


this引用逸出:

1、内部类中this引用逸出

public class Outer {      
    private String str="Outer's string";  
      
    public class Inner{  
        public void write(){  
            System.out.println(Outer.this.str);  
        }  
    }  
      
    public static void main(String[] args) {  
        Outer out = new Outer();  
        Outer.Inner in = out.new Inner();  
        in.write();  
    }  
} 
2、构造函数中this引用逸出

public class ThisEscape {  
    private Thread t;  
    public ThisEscape(){  
        System.out.println(this);  
        t = new Thread(){  
            public void run(){  
                System.out.println(ThisEscape.this);  
            }  
        };  
        t.start();  
        // do something  
    }  
      
    public static void main(String[] args){  
        ThisEscape a = new ThisEscape();  
    }  
}
this引用被线程t共享,故线程t的发布将导致ThisEscape对象的发布,由于ThisEscape对象被发布时还未构造完成,这将导致ThisEscape对象逸出(在构造函数中创建线程是可以的,但是不要在构造函数执行完之前启动线程)
在构造函数中调用一个可改写的实例方法时,也会导致同样的问题

解决方式(私有构造函数+公有工厂方法):

public class ThisEscape {  
    private final Thread t;  
      
    private ThisEscape(){  
        t = new Thread(){  
            public void run(){  
                // do something  
            }  
        };  
        // do something  
    }  
      
    public static ThisEscape getInstance(){  
        ThisEscape thisEscape = new ThisEscape();  
        thisEscape.t.start();  
        return thisEscape;  
    }  
}

(1)无状态(不可变)

不变性:某个对象在被创建之后其状态就不能被修改,这个对象就是不可变对象。

即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。

final修饰的变量,不可修改。

final修饰的方法,不可重写。

final修饰的类,不可继承。


(2)线程封闭

1、程序控制线程封闭

2、栈封闭

使用局部变量。局部变量是保存在线程栈中的,对该线程可见,是线程安全的

3、ThreadLocal类

ThreadLocal机制本质上是程序控制线程封闭,只不过是Java本身帮忙处理了。来看Java的Thread类和ThreadLocal类

1. Thread线程类维护了一个ThreadLocalMap的实例变量
2. ThreadLocalMap就是一个Map结构
3. ThreadLocal的set方法取到当前线程,拿到当前线程的threadLocalMap对象,然后把ThreadLocal对象作为key,把要放入的值作为value,放到Map
4. ThreadLocal的get方法取到当前线程,拿到当前线程的threadLocalMap对象,然后把ThreadLocal对象作为key,拿到对应的value.

public class Thread implements Runnable {
     ThreadLocal.ThreadLocalMap threadLocals = null;
}

public class ThreadLocal<T> {
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }


    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }


    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
}


ThreadLocal的设计很简单,就是给线程对象设置了一个内部的Map,可以放置一些数据。JVM从底层保证了Thread对象之间不会看到对方的数据。
使用ThreadLocal前提是给每个ThreadLocal保存一个单独的对象,这个对象不能是在多个ThreadLocal共享的,否则这个对象也是线程不安全的。
Structs2就用了ThreadLocal来保存每个请求的数据,用了线程封闭的思想。但是ThreadLocal的缺点也显而易见,必须保存多个副本,采用空间换取效率。

(3)采用内置锁来保护对象状态

锁的对象是实例或者Class

锁的同步性和互斥性会带来性能问题,可以采用合理优化缩小同步代码块的范围来稍微提高性能。

当执行时间较长的计算或者可能无法快速完成的操作时(eg网络I/O或者控制台I/O),一定不要持有锁。



对象的组合

解决问题:

不希望每次线程访问都去分析是否线程安全,通过将一些现有的线程安全组件组合起来形成线程安全的程序。

1、设计线程安全的类三个基本要素

找出构成对象状态的所有变量

找出约束状态变量的不变性条件

建立对象状态的并发访问管理策略


2、实例封闭来确保线程安全(同步锁)

Java监视器模式:把对象所有的可变状态封装起来,并由对象自己的内置锁来保护。

3、线程安全性委托

我们经常会使用ConcurrentHashMap这样的并发对象,它们是java提供的并发变成对象,它们在内部实现了同步机制,所以它们是线程安全的,我们可以把不安全对象Map的访问完全交给它来委托管理,这样通过final类型的处理,我们只需要通过Collections的浅拷贝实现Map的线程安全。

4、在现有的线程安全类中添加功能

客户端加锁:需要明确是哪个对象需要加锁(使用哪个对象的客户端代码)

组合:



小结:

多线程并发编程核心是处理对象状态。这些对象有一个特点是线程不安全的:共享和可变

有三种办法:

1、避免可变,将对象设置为无状态的,不可变的。

2、避免共享,可以用线程封闭方法,ThreadLocal,给不同的线程分配不同的副本。用空间换取时间。ThreadLocal接口有4种方法:set(Object value),get(),remove(),initialValue()。

3、实在没有办法避免多线程并发的问题,则需要考虑哪些变量声明为volitaile,哪些变量需要用锁保护,哪些锁保护哪些变量,哪些操作必须是原子操作。是否可以采用线程安全对象组合的方式构建线程安全,组合是否需要考虑客户端加锁的办法。


在确保线程安全时需要注意的三大特性:原子性,可见性,有序性

在对象操作的时候注意发布与逸出问题。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值