Java并发编程之线程(二)

多线程编程的特点

Java并发编程之线程(二)

  • 同一份代码,可以有多个线程执行
    • 既可以在一个CPU核上并发执行
    • 也可以在多个CPU核上并行执行
  • 线程的执行默认是乱序的
    • 程序员不能假定执行次序
  • 线程会共享数据(对象的变量)
    • 需要互斥
  • 线程之间也需要合作(同步)

如何实现互斥 ?

  • 锁 !
    • 只有获得了锁的线程,才能够对共享资源做操作, 换句话说:进入临界区
    • 对共享资源做完操作(即使发生异常),一定要释放锁!

Java并发编程之线程(二)

锁到底是个什么东西?

  • “锁”本身如果是软件, 也没法保证原子性!

    • 多个CPU对“锁”操作的时候也会出错
  • 最底层需要硬件指令的支持,否则谈不上锁

    • TestAndSet
    • Swap
    • CAS

注意:现在的锁都是高层的(应用层),其实底层还是用硬件指令实现的。

硬件指令:TestAndSet

Java并发编程之线程(二)

硬件指令: Swap

Java并发编程之线程(二)

设计“锁”需要考虑的问题

  • 线程申请锁的时候, 发现已经被别的线程持有, 线程该怎么办?

  • 继续尝试,无限循环

    • 时间片用完了, 变为就绪状态,等待下次调度
    • 自旋锁
  • 把线程放到阻塞队列中

注意: 自旋锁:为了让线程等待,让线程陷入一个忙循环(自旋转)。优点:避免线程切换的开销(挂起线程与恢复线程的操作都会装入内核态中完成,内核态切换回给并发性能带来很大的开销)。缺点:自旋时间过长会浪费cpu的资源。但是JVM针对上述的情况做了处理,对程序员是透明的。

可重入性

  • 重入简单可简单理解成重新进入同一个函数。第一次进入函数持有锁当第二次同一个函数请求同一个锁。但这个时候它的锁被第一个函数持有,必须等待第一个函数持有的锁释放才能进入第二个函数,但是第一个函数是递归函数,所以不需要等待第一个函数释放锁,第二个函数就能进行执行。这样就会陷入死循环。

  • 自旋锁无法重入

Java并发编程之线程(二)

  • 解决办法
    • 记录这个锁被谁持有
    • 记录重入的次数
  • 传送门

线程之间的通信: 通过共享变量

Java并发编程之线程(二)

线程之间的通信: wait /notify

Java并发编程之线程(二)

线程之间的通信: join

Java并发编程之线程(二)

Join的实现

Java并发编程之线程(二)

join()的本质就是调用该对象本身的wait()方法直到该线程任务执行完成后在调用该对象的notify()方法唤醒该线程。

线程的状态

Java并发编程之线程(二)

JDK中常用的锁 : 可重入互斥锁

ReentrantLock是并发包中提供的一个更为方便的的控制并发资源的类,且和synchronized语法的效果是一样的。

Java并发编程之线程(二)

优点;
可重入锁
互斥
公平竞争

JDK中常用的锁 : 信号量

Semaphore是并发包中提供的用于控制某资源同时被访问的个数的类。

Java并发编程之线程(二)

上面案例的意思是:在同一时刻,只能有3个线程能够获得锁

JDK中常用的锁 :ReentrantReaderWriterLock

ReentrantReaderWriterLock提供了读锁(ReadLock)和写锁(WriteLock),相比较ReentrantLock只有一把锁的机制而言,读写锁分离的好处是在读多写少的场景中可大幅度提高读的性能。当调用读锁的lock方法时,如果没有线程持有写锁,就可获得读锁,这也意味着只要进行读的时候没有其他线程在进行写操作,读的操作就是无阻塞的;当调用写锁的lock方法时,如果此时没有线程持有读锁或写锁,则可继续执行,这也意味着要进行写动作时,如果有其他线程在读或在写,就会被阻塞,因此写的性能可能会下降。

Java并发编程之线程(二)

JDK中常用的锁 : CountDownLatch

CountDownLatch是并发包中提供的一个可用于控制多个线程同时开始某动作的类,其采用的方式为减计数器的方式。当计数器减至0时,位于latch.await后的代码才会被执行。

Java并发编程之线程(二)

JDK中常用的锁 : CyclicBarrier(栅栏)

CyclicBarrier与CountDownLatch不同, CyclicBarrier是当await的数量达到了设定的数量后,才继续往下执行(线程之间互相等待)。

Java并发编程之线程(二)

与CountDownLatch的区别:前者着一个线程等到指定的线程执行完,后者是线程之间互相等待。

死锁

Java并发编程之线程(二)

Java并发编程之线程(二)

死锁的预防

  • 每个线程申请锁的时候都按照特定的次序

Java并发编程之线程(二)

  • 申请锁的时候加上timeout

Java并发编程之线程(二)

例子: 银行转账

Java并发编程之线程(二)

  • 上面这种实现 当两个客户互相转账时,会出现死锁。

Java并发编程之线程(二)