多线程&并发(一)

发展史
  • 没有操作系统,计算机从头到尾只执行一个程序,这个程序可以访问计算机所有的资源且每次只能运行一个

    资源利用率、公平性、便利性

  • 有了操作系统,出现了进程。操作系统负责为各个单独的进程分配各种资源如内存、文件句柄、安全证书等。
    并在进程间采用套接字、信号处理器、共享内存、信号量、文件来共享&通信数据

  • 有了线程。允许进程存在多个程序控制流,线程共享进程范围的资源。线程的共享&通信需要更细粒度的机制
线程的优势

除了我们都知道的,UI响应更灵敏、异步事件的处理

可以发挥多处理器的能力,一个线程最多在一个处理器上运行,也就是如果是100个CPU,单线程程序有99%资源被浪费了

可以提高单处理器系统的吞吐率,当某线程等待I/O时,处理器空闲时另外一个线程可以继续运行

线程的风险
  • 安全性问题:永远不发生糟糕的事情

    /**
     * 多线程访问 操作一个非原子性的变量
     * 得到结果具有随机性
     *
     * @author by fengruicong on 16/8/10.
     */
    public class MultiThreadTest1 {
        private static long count = 0;
    
        public static void main(String[] args) {
            ExecutorService service = Executors.newFixedThreadPool(5);
            for (int i = 0; i < 1000; i++) {
                service.execute(() -> System.out.print(getCount() + "\n"));
            }
        }
    
        public static long getCount() {
            return count++;
        }
    }
    

    看上去count++是一个操作,但其实包含了三个:读count count+1 结果写入count
    多线程执行getCount()时会出现得到的count值是一样的

  • 活跃性问题:某件正确的事情最终会发生

    问题包括:死锁、饥饿、活锁等

  • 性能问题:某件正确的事情尽快发生

    问题包括:服务时间过长、响应不灵敏、吞吐率低、资源消耗过高、可伸缩性较低等

线程安全性
  • 原子性

    竞态条件:由于执行时序不一样出现的错误结果

    1、如count++ 看起来是一个操作,但实际是“读取-修改-写入”的操作序列,结果状态依赖之前的状态,并且之前的状态是可能错误的

    2、延迟初始化:目的是到用的时候才初始化对象

    public class LazyInit{
        private ExpensiveObject instance = null;
    
        public ExpensiveObject getInstance(){
            if(instance == null){
                instance = new ExpensiveObject();
            }
        }
    }
    

    这是常见的一种获取单例对象的写法,但是如果在多线程环境中,两个线程同时执行getInstance(),有可能出现多个结果:如两个线程各自得到一个ExpensiveObject实例 或者 两个线程得到同一个ExpensiveObject实例

    3、复合操作

    java.util.concurrent.atomic包里有些原子变量类如AtomicLong,实现了数值和对象引用上的原子状态转换,保证了代码的安全性

    
    /**
     * 多线程访问 操作一个线程安全类 
     *
     * @author by fengruicong on 16/8/10.
     */
    public class MultiThreadTest2 {
        private static AtomicLong count = new AtomicLong(0);
    
        public static void main(String[] args) {
            ExecutorService service = Executors.newFixedThreadPool(5);
            for (int i = 0; i < 1000; i++) {
                service.execute(() -> System.out.print(getCount().get() + "\n"));
            }
        }
    
        public static AtomicLong getCount() {
            return count.incrementAndGet();
        }
    }
    

    其它例子 http://my.oschina.net/u/1011494/blog/535945

  • 加锁机制

    • 内置锁

    synchronized修饰方法,即同步代码块、该同步代码块的锁就是调用方法的对象

    sychronized修饰静态方法,class是锁

    每个Java对象都可以用于实现同步锁,称为内置锁,线程在进入同步代码块时获得锁,退出同步代码块时释放锁。

    获得内置锁唯一途径就是进入锁保护的代码块

    每次只有一个线程执行内置锁代码块,所以由锁保护的代码块会以原子的方式执行

        public synchronized void synMethod(){
        }
    
        public static synchronized void synStaticMethod(){
        }
    

    参考 http://blog.csdn.net/virgoboy2004/article/details/7585182

    • 重入

    一个线程请求其他线程的锁时会阻塞,但是获得由自己持有的锁是没有问题的。

    这代表着锁的操作粒度是“线程”不是“调用”

    JVM实现重入的方法:为每一个锁关联一个计数值,当计数值为0,锁被释放。当线程请求锁时,JVM记录锁的持有者并计数值1,线程再次获取锁,计数值递增,线程退出时,计数值相应递减直至计数值为0释放锁

        public class Widget{
            public synchronized void doSth(){
            ...
            }
        }
    
        public class LoggingWidget extends Widget{
            public synchronized void doSth(){
                System.out.println(toString()+": call doSth");
                super.doSth();
            }
        }
    

    如果内置锁不可重入,上面这段代码会出现死锁

  • 锁实现保护状态

    synchronized可以实现单个操作的原子状态,多个原子操作合并为复合操作需要额外的锁条件和加锁机制

    如果用同步协调对变量的访问,那所有访问这个变量的地方都需要使用同步,如果用锁协调的话,所有位置都必须使用同一个锁

  • 活跃性和性能

    需要在各种需求间找到平衡 安全性、简单、性能

    当一个锁持有时间过长时,无论是执行什么操作,都会带来活跃性和性能的问题(耗时操作的操作不要持有锁)

线程间对象的共享
  • 可见性

    /**
     * 多线程 可见性
     * 可能多种执行结果:
     * 1、打印 42
     * 2、线程持续循环 ready对读线程一直不可见
     * 3、打印 0 编译器重排序
     * 4、
     *
     * @author by fengruicong on 16/8/11.
     */
    public class NoVisible {
        private static boolean ready;
        private static int number;
    
        private static class ReaderThread extends Thread {
            @Override
            public void run() {
                System.out.println(ready);
                while (!ready) {
                    Thread.yield();
                    System.out.println(number);
                }
            }
        }
    
        public static void main(String[] args) {
            new ReaderThread().start();
            number = 42;
            ready = true;
        }
    }
    
    • 失效数据
    • 非原子的64位操作

      最低安全性:虽然得到的值失效,但是至少那个值是某个线程设置的值,而不是随机值

      Java内存模型要求,变量的读写操作为原子操作,但是非volatile得64位变量long double,JVM会把该变量的读写分解为两个32位的操作,一旦读写操作在不同的线程,就会出现高32位和低32位不一致的问题

    • 加锁和可见性

    加锁不仅局限互斥性还有可见性。

  • volatile变量

    • volatile变量对线程共享
    • 编译器和运行时不会对它重排序
    • 它不会被缓存在寄存器或者处理器看不见的其它地方
    • 没有加锁 不会造成线程阻塞
    • 只能保证内存可见性

    读取volatile变量相当于进入同步块,写入volatile变量相当于退出同步块

        volatile boolean asleep;
        ...
    
        while(asleep){
            countSomeSheep();
        }
    

    * 注意:volatile变量无法保证原子性 如count++ *

  • 对象发布和逸出

        public static Set<Secret> knownSecrets;
    
        public void initialize(){
            knownSecrets = new HashSet<Secret>();
        }
    

    当发布knownSecrets时同样会发布Secret对象 引起对象的逸出

        class UnsafeStates{
            private String[] states = new String[]{"AK","AL"};
    
            public String[] getStates(){
                return states;
            }
        }
    

    这种方式发布states,由于所有调用者都可以修改数组内容,也就是states逸出了作用域,它本来是被定义为私有的

    this引用的逸出

        public class ThisEscape{
            public ThisEscape(EventSource source){
                source.register(
                    new EventListener(Event e){
                        doSth(e);
                });
            }
        }
    

    当在构造函数使用多线程时,this引用会被多个线程共享,对象在没有完全构造完成时,新线程就可以看见它,导致逸出

    改造:

        public class SafeListener{
            private final EventListener listener;
    
            private SafeListener(){
                listener = new EventListener(){
                    public void onEvent(Event e){
                        doSth(e);
                    }
                };
            }
    
            public static SafeListener newInstance(EventSource source){
                SafeListener safe = new SafeListener();
                source.register(safe.listener);
                return safe;
            }
        }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值