多线程编程的特点
- 同一份代码,可以有多个线程执行
- 既可以在一个CPU核上并发执行
- 也可以在多个CPU核上并行执行
- 线程的执行默认是乱序的
- 程序员不能假定执行次序
- 线程会共享数据(对象的变量)
- 需要互斥
- 线程之间也需要合作(同步)
如何实现互斥 ?
- 锁 !
- 只有获得了锁的线程,才能够对共享资源做操作, 换句话说:进入临界区
- 对共享资源做完操作(即使发生异常),一定要释放锁!
锁到底是个什么东西?
“锁”本身如果是软件, 也没法保证原子性!
- 多个CPU对“锁”操作的时候也会出错
最底层需要硬件指令的支持,否则谈不上锁
- TestAndSet
- Swap
- CAS
注意:现在的锁都是高层的(应用层),其实底层还是用硬件指令实现的。
硬件指令:TestAndSet
硬件指令: Swap
设计“锁”需要考虑的问题
线程申请锁的时候, 发现已经被别的线程持有, 线程该怎么办?
继续尝试,无限循环
- 时间片用完了, 变为就绪状态,等待下次调度
- 自旋锁
- 把线程放到阻塞队列中
注意: 自旋锁:为了让线程等待,让线程陷入一个忙循环(自旋转)。优点:避免线程切换的开销(挂起线程与恢复线程的操作都会装入内核态中完成,内核态切换回给并发性能带来很大的开销)。缺点:自旋时间过长会浪费cpu的资源。但是JVM针对上述的情况做了处理,对程序员是透明的。
可重入性
重入简单可简单理解成重新进入同一个函数。第一次进入函数持有锁当第二次同一个函数请求同一个锁。但这个时候它的锁被第一个函数持有,必须等待第一个函数持有的锁释放才能进入第二个函数,但是第一个函数是递归函数,所以不需要等待第一个函数释放锁,第二个函数就能进行执行。这样就会陷入死循环。
自旋锁无法重入
- 解决办法
- 记录这个锁被谁持有
- 记录重入的次数
- 传送门
线程之间的通信: 通过共享变量
线程之间的通信: wait /notify
线程之间的通信: join
Join的实现
join()的本质就是调用该对象本身的wait()方法直到该线程任务执行完成后在调用该对象的notify()方法唤醒该线程。
线程的状态
JDK中常用的锁 : 可重入互斥锁
ReentrantLock是并发包中提供的一个更为方便的的控制并发资源的类,且和synchronized语法的效果是一样的。
优点;
可重入锁
互斥
公平竞争
JDK中常用的锁 : 信号量
Semaphore是并发包中提供的用于控制某资源同时被访问的个数的类。
上面案例的意思是:在同一时刻,只能有3个线程能够获得锁
JDK中常用的锁 :ReentrantReaderWriterLock
ReentrantReaderWriterLock提供了读锁(ReadLock)和写锁(WriteLock),相比较ReentrantLock只有一把锁的机制而言,读写锁分离的好处是在读多写少的场景中可大幅度提高读的性能。当调用读锁的lock方法时,如果没有线程持有写锁,就可获得读锁,这也意味着只要进行读的时候没有其他线程在进行写操作,读的操作就是无阻塞的;当调用写锁的lock方法时,如果此时没有线程持有读锁或写锁,则可继续执行,这也意味着要进行写动作时,如果有其他线程在读或在写,就会被阻塞,因此写的性能可能会下降。
JDK中常用的锁 : CountDownLatch
CountDownLatch是并发包中提供的一个可用于控制多个线程同时开始某动作的类,其采用的方式为减计数器的方式。当计数器减至0时,位于latch.await后的代码才会被执行。
JDK中常用的锁 : CyclicBarrier(栅栏)
CyclicBarrier与CountDownLatch不同, CyclicBarrier是当await的数量达到了设定的数量后,才继续往下执行(线程之间互相等待)。
与CountDownLatch的区别:前者着一个线程等到指定的线程执行完,后者是线程之间互相等待。
死锁
死锁的预防
- 每个线程申请锁的时候都按照特定的次序
- 申请锁的时候加上timeout
例子: 银行转账
- 上面这种实现 当两个客户互相转账时,会出现死锁。