美文网首页
Java多线程编程核心技术2——同步

Java多线程编程核心技术2——同步

作者: 有奶喝先森 | 来源:发表于2017-02-17 11:04 被阅读0次

    一. 对象及变量的并发访问

    非线程安全会发生在多个线程并发访问同一个对象的实例变量时,会产生脏读,读取到的数据是被更改过的。而线程安全各个线程获得的实例变量的值都是经过同步处理的,不会出现脏读。


    1.线程是否安全呢?

    (1) 如果是方法内部的私有变量,不存在非线程安全问题。

    (2) 多线程访问的是同一个对象的实例变量时,有可能出现线程不安全问题。

    (3) 多个线程访问的是同步方法的话,一定是线程安全的。

    (4) 多个线程对应的是多个对象时,出现的结果就会是异步的,但是线程安全。


    2. synchronized关键字

    (1) synchronized获得的是对象锁,而不是把synchronized下面的方法或者代码块当做锁。

    (2) synchronized声明的方法一定是排队执行的(同步的),只有共享资源的读写访问才需要同步化。

    (3) 线程A和线程B访问同一个object对象的两个同步的方法,线程A先获取object对象的Lock锁,B线程可以以异步的方式调用object对象中的非同步方法,但是想访问该对象的同步方法的话,必须得等待,不管想访问的是不是和线程A同一个同步方法。

    (4) synchronized有锁重入的功能,即自己可以再次获取自己的内部锁,可重入锁也支持父子类继承关系中,即子类可以通过可重入锁访问父类的方法。若不可重入的话,就会造成死锁。

    (5) 当一个线程执行的代码出现异常时,其持有的锁会自动释放(即该线程结束执行)。

    (6) 同步不具有继承性。

    (7) 锁定的对象改变,比如String,可能导致同步锁无效(因为锁变了)。但是只要对象不变,对象的属性被改变,锁还是同一个。


    3.synchronized同步语句块

    synchronized同步方法是对当前对象加锁,同步代码块则是对某一个对象加锁。synchronized同步代码块运行效率应该大于同步方法。

    synchronized(this):也是锁定当前对象的。

    synchronized(非this对象):使用同步代码块来锁定非this对象,则synchronized(非this对象)与同步方法是异步的,不与其他锁this同步方法争抢this锁,可以大大提高效率。synchronized同步代码块都不采用String作为锁对象,易造成死锁。


    4.synchronized关键字加到static静态方法上是给Class类加上锁(Class锁可以对类得所有对象实例起作用),而加到非static静态方法上是给对象上锁。

    synchronized关键字加到static静态方法上是给Class类加上锁 = synchronized(xxx.Class){}


    5.多线程的死锁

    因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续执行。

    比如线程A持有了锁1在等待锁2,线程A持有了锁2在等待锁1--》导致死锁。

    解决方案:不使用嵌套的synchronized代码结构。


    6.内置类与静态内置类(补充介绍)

    非静态内置类:指定对象.new 内置类();

    静态内置类:可直接new 内置类();


    7.volatile关键字:使变量在多个线程中可见

    作用:强制从公共堆栈中获取变量的值,而不是从线程私有数据栈中获取。

    ※ 在JVM被设置为-server模式时是为了线程运行的效率,线程一直在私有堆栈中获取变量的值。

    在-server模式下,公共堆栈的值和线程私有数据栈的值不同步,加了volatile后就会强制从公共堆栈中读写。

    volatile和synchronized的比较:

    (1) volatile只能修饰变量,是轻量级实现,所以性能比synchronized好。

    (2) 多线程访问volatile不会阻塞,而访问synchronized会阻塞。

    (3) volatile能保证数据可见性,但不具备同步性,不支持原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为他会将私有内存和共有内存中的数据做同步。

    (4) 两者功能属性不同,synchronized解决的是多个线程之间访问资源的同步性;

    volatile解决变量在多个线程之间的可见性,即:在多个线程可以感知实例变量被修改了,并且可以获得最新的值引用,也就是用多线程读取共享变量时能获得最新值引用。

    volatile int i

    i++;

    i++有如下三个步骤:

    (1) 从内存中获取i的值;

    (2)计算i的值;

    (3) 将i的值写入内存中。

    这样的操作不是一个原子操作(联想:synchronized修饰的方法或者代码段可以看做一个整体,因此具有原子性),比如线程B要提取i的值时,线程A还未将计算好的i的值放回内存,则线程B取出来的i的值还是线程A计算前的值。--》线程不安全


    8.AtomicInteger(AtomicLong等)

    private AtomicInteger count = new AtomicInteger(0);

    System.out.println(count.incrementAndGet());//自动加1//decrementAndGet()自动减1

    public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }


    Compare And Swap(CAS):首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。

    CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。下面来看一下AtomicInteger是如何利用CAS实现原子性操作的。

    但是CAS也是有问题存在的:

    CAS的ABA问题

    1.进程P1在共享变量中读到值为A

    2.P1被抢占了,进程P2执行

    3.P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占。

    4.P1回来看到共享变量里的值没有被改变,于是继续执行。

    虽然P1以为变量值没有改变,继续执行了,但是这个会引发一些潜在的问题。ABA问题最容易发生在lock free 的算法中的,CAS首当其冲,因为CAS判断的是指针的地址。如果这个地址被重用了呢,问题就很大了。(地址被重用是很经常发生的,一个内存分配后释放了,再分配,很有可能还是原来的地址)


    还有一种情况是:单独一个AtomicInteger.incrementAndGet()是线程安全的,但是同时两个AtomicInteger.incrementAndGet()就不一定是线程安全的了,即两个方法之间不是原子的。

    public static AtomicLong count = new AtomicLong();

    public void addNum(){

    System.out.println(count.addAndGet(100));

    System.out.println(count.addAndGet(1));

    }

    多个线程调用addNum()时,线程A加了100,还没来得及加1,线程B就进来加了100。

    解决方案:

    public static AtomicLong count = new AtomicLong();

    synchronized public void addNum(){

    System.out.println(count.addAndGet(100));

    System.out.println(count.addAndGet(1));

    }


    9.synchronized代码块也具有volatile同步的功能

    线程A调用runMethod(),线程B调用stopMethod(),持有的是同一把锁。

    当线程A调用完runMethod()后,打印不出"停下来了!"的,因为死循环,被A锁死了。

    各线程间的数据值没有可见性。

    private Boolean isContinueRun = true;

    public void runMethod(){

        while(isContinueRun){

        }

        System.out.println("停下来了!");

    }

    public void stopMethod(){

        isContinueRun = false;

    }

    解决方案如下,成功打印"停下来了!"

    private Boolean isContinueRun = true;

    public void runMethod(){

        private anyString = new String();

        while(isContinueRun){

            synchronized(anyString){

            }

        }

        System.out.println("停下来了!");

    }

    public void stopMethod(){

        isContinueRun = false;

    }

    关键字synchronized 保证同一时刻,只有一个线程可以执行某一个方法或某一个代码块。包含两种特性:互斥性和可见性。

     不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或代码块的每个线程,都可以看到由同一个锁保护之前所有的修改结果。(外连互斥,内修可见。)


    二. 锁的使用

    Lock也能实现同步的效果,在使用上更加方便。

    1. ReentrantLock类  -- Lock lock = new ReentrantLock();

    (1) 使用ReentrantLock.lock()获取锁(加锁),线程就拥有了“对象监视器”;

    其他线程只有等待ReentrantLock.unlock()释放锁(解锁),再次争抢获得锁。

    效果和synchronized一致,但线程执行顺序是随机的。

    (2) 关键字与wait()/notify()/notifyAll():实现等待/通知模式,但是notifyAll()的话,需要通知所有处于WAITING状态的线程,会出现相当大的效率问题。

    ReentrantLock和Condition对象也同样可以实现。在一个Lock对象里可以创建多个Condition(即对象监视器)实例,可以实现多路通知功能;实例对象可以注册在指定的Condition中,从而可以有选择地进行线程通知,在调度线程上更加灵活。

    package lock;

    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;

    public class LockService {
     private Lock lock = new ReentrantLock();
     public Condition condition = lock.newCondition();
     public void await(){
      try {
       lock.lock();
       condition.await();//必须在调用await()之前先调用lock()以获得同步监视器
      } catch (InterruptedException e) {
       e.printStackTrace();
      }
     }
     
     public void signal(){
      try {
       lock.lock();
       condition.signal();//还有condition.signalAll();
      } finally{
       lock.unlock();
      }  
     }
    }

    (3) signalAll()

    public Condition conditionA = lock.newCondition();
    public Condition conditionB = lock.newCondition();
    //use:
    conditionA.signal(); //or conditionB.signal();

    唤醒指定种类的线程,如conditionA.signal(); 只有用了conditionA的线程被唤醒。

    (4) 公平锁与非公平锁

    公平锁:线程获取锁的顺序是按照线程加锁的顺序来分配的(FIFO);new ReentrantLock(true);//不一定百分百FIFO,但是基本呈有序。

    非公平锁(默认):锁的抢占机制,随机获得锁。new ReentrantLock(false);


    2.相关方法介绍

    (1) int getHoldCount():查询当前线程保持此锁定的个数,也就是调用lock()的次数。

    (2) int getQueueLength():返回正获取此锁定的线程估计数,如5个线程,一个线程调用了await(),还有4个线程在等待锁的释放。

    (3) int getWaitQueueLength(Condition condition):比如有5个线程,每个线程都执行了同一个condition对象的await(),则结果为5。

    (4) boolean hasQueuedThread(Thread thread):查询指定的线程是否正在等待此锁定。

    (5) boolean hasWaiters(Condition condition):是否有线程正在等待与此锁定有关的condition条件。

    (6) boolean isFair():判断是不是公平锁。

    (7) boolean isHeldByCurrentThread():查询当前线程是否保持此锁定。

    (8) boolean  isLocked():查询此锁定是否由任意线程保持。

    (9) void lockInterruptibly():如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常。

    (10) boolean tryLock():仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定。

    (11) boolean tryLock(long timeout,TimeUnit unit):若在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。

    (12) Condition.awaitUninterruptibly():在WAITING情况下interrupt()不会抛出异常。

    (13) Condition.awaitUntil(time):线程在等待时间到达前,可以被其他线程唤醒。


    3.ReentranReadWriteLock类

    共享锁:读操作相关的锁;排他锁:写操作相关的锁。

    读写,写读,写写都是互斥的;读读是异步的,非互斥的。

    相关文章

      网友评论

          本文标题:Java多线程编程核心技术2——同步

          本文链接:https://www.haomeiwen.com/subject/uzxnittx.html