Java并发编程实战

  • 一.概念
  • 进程和线程
  • 进程和线程的区别?
  • 多进程
  • 多线程
  • 并发
  • 要考虑的问题
  • 二.线程安全性
  • 概念
  • 竞态条件
  • 三.对象的共享
  • 可见性
  • volatile
  • 线程封闭(ThreadLocal)
  • Final
  • 四.Java内存模型(JMM)
  • happenBefore
  • ?Unsafe
  • ?CAS
  • CAS 全称是 compare and swap,即比较并交换,它是一种原子操作,同时 CAS 是一种乐观机制。
  • ABA 问题
  • 五. 线程安全的对象
  • 我们常用的线程安全的对象
  • concurrent下的Atomic :非阻塞方法来实现并发控制。
  • 同步容器类Vector和HashTable
  • BlockingQueue 和BlockingDeque
  • concurrentHashMap
  • 六.闭锁
  • CountDownLatch
  • FutureTasK
  • 信号量Semaphore
  • 七.终端和取消任务
  • 生产者消费者关闭
  • 八.线程池
  • 九.锁
  • 解决
  • Synchronzied
  • LOCK


一.概念

进程和线程

1.什么是进程? 进程简单来说就是系统中一个应用程序,进程是程序的基本实体,是系统进行资源和分配调度的一个独立单位。
2.什么是线程? 线程是进程的一个实体(执行单元), 是CPU调度和分配的基本单位,比进程小。

进程和线程的区别?

1.一个进程可以有多个线程(至少一个),一个线程只能属于一个线程
2.同一进程内的线程共享进程的资源
3.线程是CPU调度的单位,不拥有线程的资源,进程是拥有资源的基本单位。

多进程

在操作系统上能运行多个程序,系统为每个进程分配资源,如果需要的话进程需要相互通信,这就需要进程的通信。
进程的通信分为
socket套接字
信号
信号量
共享内存
管道

多线程

现在大部分操作系统都是用线程做基本的调度单位。

并发

多个程序同时执行。
作用:提高资源利用率,让资源的使用更公平,开发更容易。

要考虑的问题

安全性:线程交替运行同时操作会存在安全性的问题。
活跃性:可能会产生死锁,饥饿,以及活锁。
性能: 线程的上下文切换会造成极大开销。引入同步机制的清华下,往往会抑制编译器的不断优化。

二.线程安全性

要编写线程安全的代码,其核心在于对状态访问(例如共享可变的状态变量)的操作管理。

概念

多个线程访问某个类,无论调度方式和交替运行,都能保证正确的行为。

竞态条件

不正确的执行时序得到不正确的结果。

三.对象的共享

可见性

通常我们不能确保执行读操作的线程都实时的看到其他线程写入的值,所以为了确保多个线程对内存的写入操作,必须保证可见性。

volatile

说到可见性就不得不说volatile 这个关键字,当然加锁同步的方式也能达到内存可见性的目的
volatile是Java提供的一种弱于锁的同步机制,把变量声明为volatile类型后,编译器和运行时都能注意到这个变量是共享的,都不会使这个变量的操作进行“重排序”。并且不会缓存在寄存器中。但是volatile不能保证原子性,如果想让操作保证原子性需要加锁。

线程封闭(ThreadLocal)

当然访问共享数据时需要同步,但是还有一种方式避免同步那就是不共享数据。
volatile就存在了一种线程封闭,是一种内存屏障。还有一种更规范的方法那就是ThreadLocal。
ThreadLocal是线程的局部变量随着线程生灭。

Final

不变的对象一定是线程安全的。
final修饰的变量会在编译期就获取到值并且不可变。

四.Java内存模型(JMM)

在上一节中我们说到了volatile和ThreadLocal,这一节就说一下Java的内存模型
JVM是Java虚拟机:方法区,虚拟机栈,本地方法栈,堆,程序计数器。其中堆和方法区是共享的其他是私有的。

JMM是Java的内存模型
调用栈和本地变量存放在线程栈上,对象存放在堆上。

java并发线程控制框架 java线程与并发编程实践_多线程


如图,每一个线程都有一个自己的工作空间(也叫栈空间),线程共享主内存当中的内容,线程对共享变量的读写操作都会在线程的私有内存中拷贝一个共享变量副本,操作完成回写到主存中。

happenBefore

JMM对所有操作定义了一个偏序关系也就是happenBefore。但是同步操作,volatile和锁机制都是全序关系。
AQS就是说明了如何使用借助这种技能。AQS自己维护了一个同步器状态的证书,Future用这个整数保存用户的状态。
缺少Happens-before关系时 就可能出现重排许问题

?Unsafe


在解读CAS之前先说一个类Unsafe

首先unsafe可以直接操作内存,惯例看下源码

public final class Unsafe {
  // 单例对象
  private static final Unsafe theUnsafe;

  private Unsafe() {
  }
  @CallerSensitive
  public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    // 仅在引导类加载器`BootstrapClassLoader`加载时才合法
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {    
      throw new SecurityException("Unsafe");
    } else {
      return theUnsafe;
    }
  }
}

通常,我们在Java中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM所管控的Java进程内存,JVM会采用垃圾回收机制统一管理堆内存。堆外内存,存在于JVM管控之外的内存区域,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法。

堆外内存通常在通信中做缓冲池,Netty之类的NIO框架。

线程调度
Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。


?CAS

CAS 全称是 compare and swap,即比较并交换,它是一种原子操作,同时 CAS 是一种乐观机制。

CAS 的思想很简单:三个参数,一个当前内存值 V、旧的预期值 A、即将更新的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false。
一般在源码中,cas操作一般都伴随着unsafe(硬件级别的从内存拿值的东西)。unsafe的一段代码解释下CAS。

public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
    var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));// 自旋
  return var5;
}

compareAndSwapInt 就是实现 CAS 的核心方法,其原理是如果 var1 中的 value 值和 var5 相等,就证明没有其他线程改变过这个变量,那么就把 value 值更新为 var5 + var4,其中 var4 是更新的增量值;反之如果没有更新,那么 CAS 就一直采用自旋的方式继续进行操作(其实就是个 while 循环),这一步也是一个原子操作。

ABA 问题

CAS 看起来很爽,但它也有缺点,那就是“ABA”问题。
例如线程 1 从内存位置 V 取出 A,这时候线程 2 也从内存位置 V 取出 A,此时线程 1 处于挂起状态,线程 2 将位置 V 的值改成 B,最后再改成 A,这时候线程 1 再执行,发现位置 V 的值没有变化,尽管线程 1 也更改成功了,但是不代表这个过程就是没有问题的。

五. 线程安全的对象

我们常用的线程安全的对象

concurrent下的Atomic :非阻塞方法来实现并发控制。

看源码

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
//unsafe	 就是java提供的获得对象内存地址访问的类,作用就是CAS(比较并交换) 
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

...
    /**
     * Atomically sets to the given value and returns the old value.
     * @param newValue the new value
     * @return the previous value
     */  我们可以看到是采用Cas的方式
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
}
    /**

同步容器类Vector和HashTable

add()和put()方法用synchronized关键字修饰,

BlockingQueue 和BlockingDeque

BlockingQueue 使用了显式锁Lock
BlockingDeque 是双端阻塞队列

public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
       public E takeFirst() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E x;
            while ( (x = unlinkFirst()) == null)
                notEmpty.await();
            return x;
        } finally {
            lock.unlock();
        }
    }

concurrentHashMap

在1.8中:

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());//计算hash
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;

从源码可以看出,1.8中的concurrentHashMap使用了synchronized和CAS的方式进行的,扩容使用了Cas

六.闭锁

CountDownLatch

private static final class Sync extends AbstractQueuedSynchronizer

从源码上看CountDownLatch继承了AQS,在AtQS中维护了一个volatile类型的整数state,volatile可以保证多线程环境下该变量的修改对每个线程都可见,并且由于该属性为整型,因而对该变量的修改也是原子的。创建一个CountDownLatch对象时,所传入的整数n就会赋值给state属性,当countDown()方法调用时,该线程就会尝试对state减一,而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在await()方法中等待的线程。

FutureTasK

java并发线程控制框架 java线程与并发编程实践_线程安全_02


源码上来看

private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;
    
protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

从源码看出也是CAS操作,unsafe是Jvm定义的获取内存中引用值的对象。

信号量Semaphore

final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

轮训CAS,

七.终端和取消任务

1.使用volatile类型的变量保存取消状态,但是当调用的时候阻塞了 ,任务可能检测不到取消标志,永远不会结束。

volatile boolean cancelled;
。。。
public void cancel(){
    cancelled = true;
};

2.使用interrupt 但是sleep和wait不一定能检测到清除中断状态抛出异常。
通常终端是取消的最合理方式。

public Class Thread{
public boolean interrupted(){}
public static boolean interrupted(){}
public void boolean interrupted(){}
}

3.Future.cancel

生产者消费者关闭

1.ExecutorService shutdown和shutdownNow
2.毒丸 --也就是一个对象,得到这个对象时候停止

八.线程池

前文说到线程的创建是非常消耗资源的一件事,所以我们要使用线程池,它还能将任务提交和执行解耦。
饥饿死锁:线程一直等待线程池的任务完成。刚巧线程池中的完成需要等待线程的返回值。
线程池构造属性:

publicThreadPoolExecutor(int corePoolSize,//线程池基本大小
你太maximumPoosize,//最大大小
ThreadFactory,线程工厂
BlockingWueue,阻塞队列
                                           )

九.锁

死锁:多线程锁相互等待。单线程重复申请锁。
锁顺序死锁,循环死锁。

解决

1.定时锁
2.中断或者回滚

Synchronzied

对象锁,互斥性,和可见性,可以修饰方法类代码块
锁的是对象,在对象头中插入锁状态

  1. 根据修饰对象分类
修饰代码块
   synchronized(this|object) {}
   synchronized(类.class) {}
   
修饰方法
   修饰非静态方法
   修饰静态方法
获取对象锁
synchronized(this|object) {}
修饰非静态方法

获取类锁
synchronized(类.class) {}
修饰静态方法

synchronized关键字不能继承。可重入
对于父类中的 synchronized 修饰方法,子类在覆盖该方法时,默认情况下不是同步的,必须显示的使用 synchronized 关键字修饰才行。
在定义接口方法时不能使用synchronized关键字。
构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
不同的线程可以访问一个带有synchronized方法的类其他方法
不同的线程可以访问一个带有synchronized代码的类其他操作


LOCK

显式锁,可以定时可以手动加锁解锁。

参考资料
Java魔法类:Unsafe应用解析 深入了解Java虚拟机
Java并发编程实战