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

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

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


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类

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

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

推荐阅读更多精彩内容