Java 并发编程阅读笔记(上)

Java 并发编程阅读笔记(上)
1、同步(Synchronous)和异步(Asynchronous):同步方法调用必须等到方法调用返回。而异步则更像一个消息传递,无需等待结果返回。如去商场买东西,和网购东西

2、并发(Concurrency)和并行(Parallelism):并发偏重于多个任务交替执行,而多个任务之间有可能还是串行。并行是真正意义上的“同时执行”。对于并发来说,执行的过程是交替的,系统会不断地进行任务切换。

3、临界区:临界区用来表示一种公共资源或者是共享数据。不是线程私有的,但是要控制只能一个线程使用它。一旦临界区资源被占用,其他线程要使用这个资源,就必须等待。

4、阻塞和非阻塞:用来形容对线程之间的相互影响。比如一个线程对临界区进行访问,另一个线程只能等待,那么这个线程是阻塞。非阻塞反之。

5、死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock):

​ 死锁:彼此之间相互占用资源而不进行释放,那么这个状态将永远维持下去。

​ 饥饿:饥饿是指某一个或者多个线程因为种种原因无法获取所需要的资源,导致一直无法执行。比如它的优先级太低,而高优先级的线程不断抢占它需要的资源,导致低优先级线程无法工作。

​ 活锁:线程主动将资源释放给他人使用,那么就会出现资源不断在两个线程中跳动,而没有一个线程可以同时拿到所有资源而正常执行。

6、并发级别:阻塞(Blocking)、无饥饿(Starvation-Free)、无障碍(Obstruction-Free)、无锁(Lock_Free)、无等待(Wait-Freee)。

7、原子性(Atomicity):指一个操作是不可中断的。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

8、可见性(Visibility):指当一个线程修改了某个共享变量的值,其他线程是否能够立即知道这个修改。对于串行程序来说,不存在可见性问题。

9、有序性:有序性问题的原因是因为程序在执行时,为了优化效率,可能进行指令重排,重拍后的指令与原指令的顺序未必一致。注意:指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。之所以需要做指令重排,就是为了保证尽量少的中断流水线。

10、进程(Process):指计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统的基础。线程是轻量级进程,是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。

11、线程的状态:NEW、RUNNABLE、BLOCKED、WAITING(WAITING + TIMED_WAITING)、TERMINATED。

​ NEW:刚刚创建的线程,这种线程还没开始执行,等到线程的 start() 方法调用才便是线程开始执行。

​ RUNNABLE:一切资源都已准备好,时间片到了即可执行。

​ BLOCKED:线程执行过程中遇到同步块,就会进入阻塞状态。

​ WAITING:WAITING 又可以分为 WAITING 和 TIMED_WAITING,WAITING是进入一个无时间限制的等待,TIMED_WAITING会进行一个有时限的等待。WAITING 的线程是正在等待一些特殊的事件。比如,notify() 方法,而通过 join() 方法等待的线程则会等待目标线程的终止。

​ TERMINATED:线程执行完毕,进入 TERMINATED 状态,线程结束。

12、避免使用 Thread.stop() 方法结束线程。这个方法是强制线程释放所持有的锁,导致操作直接中断。容易导致数据等的丢失。可以采用增加 stopMe() 方法改变状态的方式终止线程。

13、线程中断:线程中断是一种重要的线程协作机制。其不会使线程立即中断退出,不像stop()方法那么粗暴。而是给线程发送一个通知,告知目标线程。至于目标线程接到通知后何时退出,则完全由目标线程自行决定。

线程中断有关的三个方法,比较容易误用,需注意。

public void Thead.interrupt()                                   // 中断线程
public boolean Thread.isInterrupted()                 // 判断是否被中断
public static boolean Thead.interrupted()          // 判断是否被中断,并清除当前中断状态

14、Thread.sleep() 签名定义

public static native void sleep(long millis) throws InterruptedException

Thread.sleep() 方法由于中断而抛出异常(InterruptedException)它的中断标记会被清除,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标记为。

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            while(true) {
                if(Thread.currentThead().isInterrupted()) {
                    System.out.println("Interrupted");
                    break;
                }
                try {
                    Thread.sleep(2000);
                } catch(InterruptedException e) {
                    System.out.printIn("Interrupted where sleep");
                    // 重新设置中断状态
                    Thead.currentThread().isterrupt();
                }
                Thread.yield();
            }
        }
    };
    t1.start();
    Thread.sleep(2000);
    t1.interrupt();
}

15、等待(wait)和通知(notify)

​ 为了支持线程间的协作。JDK提供了两个非常重要的接口线程等待wait() 和通知 notify() 方法,是Object类的方法,意味着任务对象都可以调用这两个方法。

​ 当一个对象实例上调用了 wait() 方法后,当前线程就会在这个对象上等待,只到其他线程调用了对象的 obj.notify() 方法为止。当一个线程调用了 obj.wait() 那么它就会进入 obj 对象的等待队列。这个等待队列中可能有多个线程,因为系统运行多个线程同时等待某一个对象。当 obj.notify() 被调用时,会从等待队列中随机选择一个线程,并将其唤醒。这个选择过程是不公平的,随机的。而 notifyAll() 方法则是唤醒这个等待队列上所有等待的线程。想要调用 wait() 方法,必须是在对应的 synchronized 语句中。

注意:Object.wait() 和 Thread.sleep() 方法都可以让线程等待若干时间。除了 wait() 可以被唤醒外,另一个主要区别就是 wait() 方法会释放目标对象的锁,而 Thread.sleep() 方法不会释放任何资源。

16、挂起(suspend)和继续执行(resume)线程

​ 不推荐使用 suspend() 去挂起线程,因为 suspend() 在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它暂用的锁时,都会获取失败,进入阻塞态,指导 resume() 操作,被挂起的线程才能继续。

17、等待线程结束(join) 和谦让(yield)

​ 很多时候,一个线程的输入可能非常依赖另外一个或多个线程的输出,此时这个线程就需要等待依赖线程执行完毕,才能继续执行。join() 方法可以实现这个功能。

public final void join() throws InterruptedException             // 无限等待
public final synchronized void join(long millis) thorws InterruptedException  // 给出时间等待,超过继续往下执行

join() 的本质是让调用线程 wait() 在当前线程对象实例上。代码

while (isAlive) {
    wait(0);
}

Thead.yield() 的定义

public static native void yield();

作用是使当前线程让出CPU。但让出CPU后,还会进行CPU资源的争夺,至于能否被分配到,看运气。其实就是想休息一下的意思。

18、volatile 关键字

​ volatile 不能保证真正的线程安全,它只能确保一个线程修改数据后,其他线程可见,但是两个线程同时修改某个数据时,却依然会产生冲突。

19、公平锁:在大多数情况下,锁的申请是非公平的,充满了随机性,抢占资源。而公平锁的一大特点是:它不会产生饥饿现象。只要你排队,最终还是可以等到资源的。如果我们使用 synchronized 关键字进行锁控制,那么产生的锁就是非公平的。

20、ReentrantLock 可重入锁

public ReentrantLock(boolean fair)

​ 当参数fair 为 true 时,表示锁时公平的。

ReentrantLock 几个重要的方法

  • lock():获得锁,如果锁已经被占用,则等待。
  • lockInterruptibly:获得锁,但优先响应中断。
  • tryLock():尝试获得锁,如果成功,则返回 true,失败则返回 false。该方法不等待,立即返回。
  • tryLock(long time, TimeUnit unit):在给定时间被尝试获得锁。
  • unlock():释放锁。

就重入锁的实现来看,它主要集中在 Java 层面,在重入锁的实现中,主要包含三个要素:

第一,是原子状态。原子使用 CAS 操作来存储当前锁的状态,判断锁是否已经被线程持有。

第二,是等待队列。所有没有请求到锁的线程。都会进入等待队列进行等待。待有线程释放锁后,系统能从等待队列中唤醒一个线程,继续工作。

第三、是阻塞原语 park() 和 unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。

21、Condition 条件

​ Condition 条件与重入锁关联的。通过 Lock 接口的 Condition newCondition() 方法可以生成一个与当前冲入锁绑定的 Condition 实例。利用 Condition 对象,我们可以让线程在合适的时间等待,或者在某个特定的时刻得到通知,继续执行。

void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline ) throws InterruptedException;
void signal();
void signalAll();

​ 以上方法的含义如下:

  • await() 方法会使当前线程等待,同时释放当前锁,当其他线程中使用 signal() 或者 signalAll() 方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和 Object.wait() 方法很相似。
  • awaitUninterruptibly() 方法会 await() 方法基本相同,但是它并不会在等待过程中相应中断。
  • signal() 方法用于惯性一个在等待中的线程。相对的 signalAll() 方法会唤醒所有在等待中的线程。这和 Object.notify() 方法很类似。
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
.....
    condition.await();
....
    condition.signal();

22、允许多个线程同时访问:信号量(Semaphore)

​ 无论是内部锁 synchronized 还是重入锁 ReentrantLock,一次都只允许线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。

信号量的定义

public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
   
//  主要的逻辑方法
public void acquire()
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release()

​ acquire() 方法尝试获得一个准入的许可。若无法获得,则线程会等待,知道有线程释放一个许可或者当前线程被中断。acquireUninterruptibly 类似,但是不会响应中断。tryAcquire 尝试获得许可,成功则返回 true,失败返回 false,它不会进行等待,立即范围。release 则用于释放线程资源,其他资源可以访问

23、ReadWriteLock 读写锁

​ 特点:线程读不阻塞、边读边写互相阻塞、线程都写时阻塞,一次只能一个线程写。

​ 场景:如果读操作远远大于写操作,name读写锁就能发挥最大的功效,提升系统的性能。

24、倒计时器 CountDownLatch

​ 特点:在计时器倒数完毕之前,可以让某个线程一直处于等待状态。主线程在 CountDownLatch 上的等待,当所有检查任务全部完成后,住线程方能继续执行。

25、循环栅栏 CyclicBarrier

​ CyclicBarrier 功能上和 CountDownLatch 类似,但功能更加复杂强大。CountDownLatch 是一次性的,而 CyclicBarrier 可以循环使用,当凑齐线程数后,计数器会归零,放开通过。接着等待下次线程过来重复操作。

26、ConcurrentHashMap

​ 特点:通过减小锁粒度来实现并发锁。传统的 HashMap 线程不安全,而想要线程安全,我们自然会想到给整个 HashMap 加锁,但是我们认为这样锁的粒度太大了,导致性能不佳。而 ConcurrentHashMap 则引入了 (Segment)桶的概念,即将HashMap 内部进行细分,分段控制加锁。通过 hashcode 得到该表项对应存放桶的位置,然后对该段进行加锁,只要操作的不是同一段即可实现线程间真正的并行。

27、有助于提高“锁”性能的几点建议

  • 减小锁持有的时间,减少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力。
  • 减小锁粒度。
  • 读写分离锁替代独占锁。
  • 锁分离,采用生产消费模型,将锁进行分离
  • 锁粗化,将所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数。

28、锁偏向

​ 核心思想:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无需再做任务同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能。使用 Java 虚拟机参数 -XX:+UseBiasedLocking 可以开启偏向锁。

29、轻量级锁

​ 特点:如果偏向锁失败,虚拟机并不会立即挂起线程。它还会使用一种被称为轻量级锁的优化手段。轻量级锁简单地将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程释放持有对象锁。如果线程获得轻量级锁成功,则可以顺利进入里临界区。如果轻量级锁加锁失败,则表示其他 线程先抢夺到了锁,那么当前线程的锁请求会膨胀为重量级锁。

30、自旋锁

​ 特点锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会在做会后的努力–自旋锁。膨胀锁不知道具体什么时候能获得锁,就乐观地假设他不久之后就能获得锁。因此,虚拟机会让当前线程做几个空循环(自旋的定义),如果成功拿到锁,则进入临界区,如果还是失败,则线程才会真正在操作系统层面挂起。

31、锁消除

​ Java 虚拟机在 JIT 编译时,会通过扫描运行上下文的方法找到不可能存在共享资源竞争的锁,并将其消除,达到减少请求锁时间的目的。如在局部变量中使用类似 StringBuffer 或 Vector 等线程安全的类时,JVM 则会通过逃逸分析,将其中无用锁代码揪出并去除。

32、ThreadLocal

​ 特点:将共享变量备份都本地,只有当前线程可以访问,自然是线程安全的。

​ 场景:如 SimpleDateFormat.parse() 方法是线程不安全的。如果在并发情况下使用该类,会抛出NumberFormatException 异常。要实现线程安全处理前后加锁之外,还能使用 ThreadLocal 为每一个线程都产生一个 SimpleDateFormat 对象实例。

private static ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>();
public ParseDate(int i) {this.i = i};
public void run() {
    try {
        if(t1.get() == null) {
            t1.set(new SimpleDateFormat("yyyyMMdd"));
        }
        Date t = t1.get().parse("2015-03-06 15:30:10")
    }
}

​ 实现原理:值得关注的是 ThreadLocal 的 set() 和 get() 方法。从 set() 说起:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if(null != map) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

​ 在 set 时,首先获得当前线程对象,然后通过 getMap() 拿到线程的 ThreadLocalMap,并将值设置如 ThreadLocalMap 中。ThreadLocalMap 可以理解 为 HashMap,但是它是定义在 Thread 内部的成员。而设置到 ThreadLocal 中的数据,也正是写入 threadLocals 这个 Map。其中,key 为 ThreadLocal 当前对象,value是我们需要的值。而 threadLocals 本身就保存了当前自己所在线程的所有“局部变量”,也就是一个 ThreadLocal 变量的集合。

​ 通过 get() 可以将 Map 中的数据拿出来:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if(map != null) {
        ThreadLocalMapEntry e = map.getEntry(this);
        if(e != null) {
            return (T)e.value;
        }
    }
    return setInitalValue();
}

33、无锁

​ 背景:对于并发控制而言,锁时一种悲观的策略。它总是假设每种操作都会产生冲突,因此,对于每次操作都小心翼翼。而无锁是一种乐观的策略,它会假设对资源的额访问是没有冲突的,故不存在死锁的问题。无锁使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。这种方式没有“锁竞争” 与 “线程间频繁调度” 带来的开销,比基于锁的方式拥有更好的性能。

​ 比较交换(CAS)算法:包含三个参数CAS(V,E,N)。V表示要更新的变量,E便是预期值,N表示新值。仅当 V 值等于 E 值时,才会将 V 的值更新为 N。如果值不等,说明其他线程已经更新了,则当前线程什么也不做。最后,CAS 返回当前 V 的真实值。CAS 总是保持乐观的态度进行的,它总是认为自己可以成功完成操作。多个线程完成操作时,只有一个线程胜出比完成操作,其他线程被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

34、无锁的线程安全整数:AtomicInteger

​ 可以理解为 Integer 整数类型,但是不同的是,它是可变的,并且是线程安全的。对其进行修改等任何操作,都是用 CAS 指令进行的。性能方面,因为是基于 CAS 的,比直接加锁的形式,性能要好一些。推荐在并发整数操作时使用类似的原子类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值