最近学了下多线程,做了一些笔记,整理如下。
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,无外力作用,它们都将无法推进下去。此时称系统处于死锁状态
下面就是一个死锁的例子,线程1持有资源1,线程2持有资源2,他们同时都想申请对方的资源,这两个线程就会互相等待而进入死锁状态。
//资源1
private static final Object resources1 = new Object();
//资源2
private static final Object resources2 = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (resources1){
System.out.println(Thread.currentThread().getName()+"get resources1");
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+"waiting get resources2");
synchronized (resources2){
System.out.println(Thread.currentThread().getName()+"get resources2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"死锁线程1").start();
new Thread(()->{
synchronized (resources2){ //正常运行只需把resources2改为resources1
System.out.println(Thread.currentThread().getName()+"get resources2");
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+"waiting get resources1");
synchronized (resources1){//正常运行只需把resources1改为resources2
System.out.println(Thread.currentThread().getName()+"get resources1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"死锁线程2").start();
}
产生死锁必须具备以下条件:
- 互斥条件:该资源任意一个时刻只有一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对以获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在为使用完之前不能被其他线程强行剥夺,只有自己使用完后自动释放。
- 循环等待条件:若干线程之间形成一种首尾相接的循环等待的资源关系。
如何避免产生死锁:就是破坏产生死锁的条件之一。
volatile关键字
把变量声明为volatile,这就告诉JVM,这个变量是不稳定的,每次使用它到主存中进行读取。volatile关键字主要作用就是保证变量的可见性还有一个作用是防止重排序。通俗一点来讲就是一个变量被volatile修饰,则Java可以确保所有线程看到这个变量的值是一致的。如果某个线程修改其值,那么其他线程立马可以看到更新,这就是内存可见性。
原理:通过屏障指令
不能保证数据的原子性:解决方法synchronized,ReentrantLock,AtomicInteger
ReentrantLock可重入锁
ReentrantLock属于JUC的locks包,是一种递归无阻塞的同步机制,其内部Sync继承了AQS队列同步器
synchronized和volatile区别
- volatile关键字是线程同步的轻量级实现,所以性能比synchronized好,但volatile只能修饰变量,synchronized可以修饰方法和代码块。
- 多线程访问volatile关键字不会发生阻塞,而synchronized可能发生阻塞。
- volatile保证数据的可见性,但不能保证数据的原子性。synchronized都保证了。
- volatile主要解决变量在多个线程中的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
synchronized和ReentrantLock异同
1.相同点:Lock可以完成synchronized的所有功能。
2.不同点:Lock比synchronized语义更精准性能更好,而且不强制要求一定要获取锁。synchronized会自动释放锁,而Lock需要手动释放。具体的来说有以下几点:
- 含义不同
synchronized是关键字,属于JVM层面的,Lock是JUC包下的,是jdk1.5后api层面的锁。
- 使用方法不同
synchronized 不需要用户手动释放锁,代码完成之后系统自动让线程释放锁;ReentrantLock需要用户手动释放锁,没有手动释放可能导致死锁
- 等待是否可以中断
synchronized不可以被中断,除非抛出异常或者运行完成。ReentrantLock可以中断,一种是通过tryLock(long time,TimeUtil util),另一种是lockInterruptibly () 放代码块中,调用interrupt()方法进行中断。
- 加锁是否公平
synchronized是非公平锁,ReentrantLock默认是非公平锁,可以在构造方法中的加入Boolean参数进行设置,true代表公平锁。
synchronized关键字加到static静态方法和synchronized(class)代码块都是给Class类上锁。synchronized关键字加到实例方法上是给对象实例上锁。
JUC之AQS(AbstractQueuedSynchronizer)队列同步器
- 他是构建锁和其他同步组件的基础框架,是一个抽象类
- state状态 AQS维护了一个volatile int类型的同步状态。当state>0表示已经获得了锁,state=0时表示释放了锁
- 资源共享方式:独占(ReenntrantLock) 和 共享(Simaphore/CountDownLatch)
- CLH队列,FIFO队列,AQS依赖他完成同步状态的管理。
JUC之CAS(Compare and Swap)
- 介绍 :比较并替换
- 原理 :三个参数,一个当前内存的值V,旧的预期值A,即将更新的值B,当且仅当旧的预期值A等于当前内存值V相同时,将内存值修改为B并返回true。
- 整个过程中根本就没有获取锁,释放锁的操作,是硬件层面的原子操作。
- JUC下atomic类都是通过CAS来实现的,Unsafe是CAS核心类,其里面方法是native修饰的。
- 多CPU的CAS的处理 总线加锁 缓存加锁
- 缺陷一:如果CAS一直不成功,自旋CAS长时间不成功,会给CPU带来很大的开销,可以限制自选次数。缺陷二:只能保证一个共享变量原子操作,如多个共享变量就只能加锁。缺陷三:ABA问题,CAS检查没有发生值改变但实际发生了变化。解决方案,加版本号,1A,2B,3A。可以通过AtomicStampedReference来解决。
JUC之atomic包
atomic包提供一系列了原子操作的类,而它的原子性实现原理则是基于CAS(compare and swap)技术。就拿AtomicInteger为例:
- AtomicInteger 内部使用 CAS 原子语义来处理加减等操作,CAS 全称 Compare And Swap(比较与交换),通过判断内存某个位置的值是否与预期值相等,如果相等则进行值更新。CAS 是内部是通过 Unsafe 类实现,而 Unsafe 类的方法都是 native 的,在 JNI 里是借助于一个 CPU 指令完成的,属于原子操作。
- 缺点: 循环开销大。如果 CAS 失败,会一直尝试。如果 CAS 长时间不成功,会给 CPU 带来很大的开销;只能保证单个共享变量的原子操作,对于多个共享变量,CAS 无法保证;存在ABA问题。
ABA问题 常见于数据更新的场景,CAS需要在操作时检查内存变化,没有变化才会更新内存值。但如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但实际上是变化的。
解决方案:ABA一般都是通过版本号机制来解决的,A-B-A变成了1A-2B-3A
JUC之工具类
CyclicBarrier(同步屏障)
当指定数量的线程到达后才取消等待,执行后面的方法。
package com.lank.heima.week1;
import java.util.concurrent.CyclicBarrier;
/**
* @author lank
* @date 2020/4/4 15:25
* @desc 同步屏障
* 模拟五个运动员比赛跑步,
* 只有当五个运动员都准备好
* 后开始比赛
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
for (int i =0;i<5;i++){
new Thread(new Ath(cyclicBarrier,"线程"+i)).start();
}
}
static class Ath implements Runnable{
private CyclicBarrier cyclicBarrier;
private String name;
public Ath(CyclicBarrier cyclicBarrier, String name) {
this.cyclicBarrier = cyclicBarrier;
this.name = name;
}
@Override
public void run() {
System.out.println(name+"已就位");
try {
cyclicBarrier.await();
System.out.println(name+"到达终点");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
运行结果如下:
线程0已就位
线程1已就位
线程2已就位
线程3已就位
线程4已就位
线程4到达终点
线程1到达终点
线程3到达终点
线程2到达终点
线程0到达终点
CountDownLatch(倒计数)
用给定的计数初始化CountDownLatch。由于调用了countDown()方法,所以当前计数到达零之前,await()方法一直阻塞。之后会释放所有等待线程,await的所有后续调用会立即返回。这种现象只会出现一次,需要多次重置计数可以使用CyclicBarrier。
package com.lank.heima.week1;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
/**
* @author lank
* @date 2020/4/5 16:02
* @desc 倒计数器
* 接力跑,当起点运动员到达接力点后接力运动员开始跑
*/
public class CountDownLatchDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
for (int i =0;i<5;i++){
CountDownLatch countDownLatch = new CountDownLatch(1);
//起点运动员
new Thread(new Ath(cyclicBarrier,countDownLatch,"起点运动员"+i)).start();
//接力运动员
new Thread(new Ath(countDownLatch,"接力运动员"+i)).start();
}
}
static class Ath implements Runnable{
private CyclicBarrier cyclicBarrier;
private String name;
private CountDownLatch countDownLatch;
//起点运动员
public Ath(CyclicBarrier cyclicBarrier,CountDownLatch countDownLatch, String name) {
this.cyclicBarrier = cyclicBarrier;
this.countDownLatch = countDownLatch;
this.name = name;
}
//接力运动员
public Ath(CountDownLatch countDownLatch, String name) {
this.countDownLatch = countDownLatch;
this.name = name;
}
@Override
public void run() {
//判断是否为起点运动员
if (cyclicBarrier!=null){
System.out.println(name+"已就位");
try {
cyclicBarrier.await();
System.out.println(name+"到达接力点");
//到达接力点,countDownLatch-1
countDownLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}else {//为接力运动员
System.out.println(name+"已就位");
try {
countDownLatch.await();
System.out.println(name+"到达终点");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:
起点运动员0已就位
接力运动员1已就位
起点运动员1已就位
接力运动员0已就位
起点运动员2已就位
接力运动员2已就位
起点运动员3已就位
接力运动员3已就位
起点运动员4已就位
接力运动员4已就位
起点运动员4到达接力点
起点运动员0到达接力点
起点运动员1到达接力点
起点运动员2到达接力点
接力运动员1到达终点
接力运动员4到达终点
接力运动员0到达终点
起点运动员3到达接力点
接力运动员2到达终点
接力运动员3到达终点
Semaphore (信号量)
Semaphore维护了一个信号量许可集,当信号量中有可用的许可时线程能获得该许可,否则线程必须等待,直到有可用的许可为止。
package com.lank.heima.week1;
import java.util.concurrent.Semaphore;
/**
* @author lank
* @date 2020/4/5 16:28
* @desc Semaphore停车场案例
*/
public class SemaphoreDemo {
public static void main(String[] args) {
//3个车位停车场
Parking parking = new Parking(3);
for (int i=0;i<5;i++){
new Thread(new Car(parking)).start();
}
}
//停车场
static class Parking{
//信号量
private Semaphore semaphore;
public Parking(int count) {
semaphore = new Semaphore(count);
}
public void park(){
//获取信号量
try {
semaphore.acquire();
long time = (long) (Math.random()*10+1);
System.out.println(Thread.currentThread().getName()+"进入停车场,停车"+time+"秒......");
Thread.sleep(time);
System.out.println(Thread.currentThread().getName()+"开出停车场......");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放信号量
semaphore.release();
}
}
}
static class Car implements Runnable{
Parking parking;
public Car(Parking parking) {
this.parking = parking;
}
@Override
public void run() {
//进入停车场停车
parking.park();
}
}
}
运行结果如下:
Thread-1进入停车场,停车1秒......
Thread-0进入停车场,停车4秒......
Thread-1开出停车场......
Thread-2进入停车场,停车7秒......
Thread-3进入停车场,停车9秒......
Thread-0开出停车场......
Thread-4进入停车场,停车1秒......
Thread-4开出停车场......
Thread-2开出停车场......
Thread-3开出停车场......
为什么要使用线程池
- 降低资源消耗。通过重复利用已创建的线程降低和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
如何使用线程池
工具类 Executors 提供了静态工厂方法以生成常用的线程池:
- newSingleThreadExecutor:创建一个单线程的线程池。如果该线程因为异常而结束,那么会有一个新的线程来替代它
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大值,一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(默认 60 秒不执行任务)的线程。当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求
- Executors也提供自定义的线程构造方法。
JUC之并发容器
ConcurrentHashMap
jdk1.8以前,ConcurrentHashMap采用分段锁的概念。1.8以后,利用CAS+Synchronized来保证并发安全问题,底层采用数组+链表+红黑树的存储结构。
HashMap
jdk1.7 数组+链表 将新值放入链表的表头(在多线程下可能造成死循环) 先扩容后插入新值
jdk1.7 ConcurreentHashMap 由一个个Segemnt(分段锁)组成,Segment通过继承ReentrantLock进行加锁,每次加锁操作的是一个segment,这就保证每个segement安全。默认16个,初始化后不可扩容,Segment内部的数组可以扩容。
jdk1.8 数组+链表+红黑树,当链表中的元素超过8个,链表会转为红黑树(提高查询效率),先插入新值后扩容
jdk1.8 ConcurrentHashMap CAS+Synchronized,底层采用数组+链表+红黑树的存储结构。
总结
- HashMap线程不安全,ConcurrentHashMap是线程安全的,仅仅是指对容器操作的时候是安全的
- ConcurrentHashMap的get方法不涉及锁
- ConcurrentHashMap允许一边遍历一边更新