Java并发编程笔记-第2章 线程安全性

public class UnsafeCountingFactorizer implements Servlet{
    private long count = 0;
    public long getCount(){ return count;}
    public void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigIntefer[] factors = factor(i);
        ++count;
        encodeIntoResponse(resp, factors);
    }
}

不幸的是,UnsafeCountingFactorizer并非线程安全的,在下面所示的不安全代码中可能存在下面的情况:

   每个线程读到的值都为9, 接着执行递增操作,并且都将计数器的初始值设为10,发生了递增操作丢失的情况

Note: 同一个线程类 UnsafeCountingFactorizer, 同一个该类的对象引用, 接着使用该对象引用创建了多个线程Instance

不安全代码:

UnsafeCountingFactorizer factorizer = new UnsafeCountingFactorizer();
// 下面的两个线程启动后,可能会导致问题
new Thread(factorizer).start();
new Thread(factorizer).start();

概括:

==“先检查后执行”型竞态条件本质==——基于一种可能失效的观察结构来做出判断或执行某个计算,在现这种竞态条件下,观察完结构后,观察结果可能变得无效(另一个线程修改了),所以会出现各种问题(未预期的异常、数据被覆盖、文件被破坏等)

  1. 内置锁即互斥同步方法

Java 提供了一种内置的锁机制,通过互斥实现同步,synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。
如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象

根据虚拟机规范的要求,在执行monitorenter指令时候,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时候,锁就被释放。 如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止

在虚拟机规范对monitorenter和monitorexit的行为描述中,有两点是需要特别注意:

  1. synchronized同步块对同一线程来说是可重入的,不会出现自己把自己锁死的问题
  2. 同步块在进入的线程执行完前,会阻塞后面其他线程的进入。在第12章讲过,Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态切换到核心态中,因此状态转换需要耗费很多的处理器事件,对于代码简单的同步块(如被synchronized修饰的getter()或setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说,synchronized是Java语言中一个重量级的操作,有经验的程序员都会在确实必要的情况下才使用这种操作,而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁切入到核心态中。

除了synchronized外,我们还可以是i用java.until.concurrent(下文称J.U.C)包中的重入锁来实现同步,在基本用法上,ReentrantLock与synchronized很相似,都具备一样的线程重入特性,只是代码写法上有点区别,一个表现为API层面的互斥锁,另外一个表现为原生语法层面的互斥锁。但ReentrantLock增加了一些高级功能,主要有以下3项:

1. 等待可中断-指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,该特性对处理时间非常长的同步块很有帮助

2. 公平锁-多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,**ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求来使用公平锁**

3. 锁绑定多个条件-一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外增加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可

Note: 互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,====共享数据的锁定状态持续很短的一段时间====,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁,为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁,自旋锁在jdk1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX+UseSpinning参数来开启,在jdk1.6中就已经改为默认开启了,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而带来性能浪费。因此自旋超过了限定的次数仍然没有成功获得,就应该使用传统的方式去挂起线程,自旋次数的默认值是10次,用户可用使用参数-XX:PreBlockSpin来更改,在jdk1.6中引入了自适应的自旋锁,自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来确定

  1. 非阻塞同步
    CAS:依赖于处理器指令Compare-and-Swap,简称CAS
    java.util.concurrent包中的类Atomic中的方法使用了Unsafe类的CAS操作,下面代码实现了原子自增操作:
public class AtomicTest{
    public static AtomicInteger race = new AtomicInteger(0);
    public static void increase(){
        race.incrementAndGet()//该方法使用了Unsafe类的CAS操作
    }
    private static final int THREADS_COUNT =20;
    public static void main(String[] args) throws Exception{
        Thread[] threads = new Thread[THREADS_COUNT];
        for(int i = 0; i<THREADS_COUNT; i++){
            threads[i] = new Thread(new Runnable(){
                @Override
                public void run(){
                    for(int i =0; i<10000;i++){
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        while(Thread.activeCount()>1)
            Thread.yield();
        System.out.println(race);
    }
}

运行结果: 200000

而另外一种不能够

public class VolatileTest{
    public static volatile int race = 0;
    public static void increase(){
        race++;
    }
    private static final int THREADS_COUNT =20;
    public static void main(String[] args) throws Exception{
        Thread[] threads = new Thread[THREADS_COUNT];
        for(int i = 0; i<THREADS_COUNT; i++){
            threads[i] = new Thread(new Runnable(){
                @Override
                public void run(){
                    for(int i =0; i<10000;i++){
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        //等待所有累加线程都结束
        while(Thread.activeCount()>1)
            Thread.yield();
        System.out.println(race);

    }
}

运行结果: 每次运行程序,输出的结果都不一样,都是一个小于200000的数字

  1. 无同步方案
    线程本地存储(Thread Local Storage)
    通过java.lang.ThreadLocal类来实现线程本地存储的功能 每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal

Note: 同步是指:多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(或者是一些,使用信号量的时候)

猜你喜欢

转载自blog.csdn.net/jjf09/article/details/80210551
今日推荐