@[TOC]多线程
线程就是一个“执行流”,每一个线程都能按照自己的代码执行顺序执行代码,能够实现多个线程同时执行多分代码,实现并发编程。
(1)并发编程的需要。单核CUP的发展已经到了瓶颈,我们想要提高CPU的算力已经非常困难,所以我们就需要多核CPU。而并发编程能更好的利用多核CPU
(2)有一些任务场景要等待IO,为了让等待IO的时间能够做其他工作,就需要并发编程
(3)虽然说,进程也能实现并发编程,创建和删除进程的过程比较消耗资源,反而线程比较轻量,更好实现并发编程。
(1)CPU密集的场景下,能够更充分利用多核资源
(2)IO密集的场景下,可以充分利用IO的等待时间,干点别的事情
(1)进程包含线程,即线程是进程的一部分
(2)一个进程崩溃了,一般不会影响其他进程。而同一个进程中的一个线程崩溃了,一般会影响到其他线程,进而是该进程崩溃了。
(3)一个进程有自己的虚拟地址空间和文件描述符表,而同一个进程的多个线程则是共用相同的虚拟地址空间和文件描述符表。
(4)进程是操作系统资源分配的基本单位,而线程是操作系统调度的基本单位。
【方法一】创建一个类,继承Thread类,实现run()方法
【方法二】创建一个类,实现Runnable接口,重写run()方法,来创建线程
【方法三】使用匿名内部类,创建Thread对象,重写run()方法,来创建线程
【方法四】使用匿名内部类,创建Runnable对象,重写run()方法,来创建线程
【方法六】创建Callable对象,实现call方法,在通过FutureTask对象辅助,来创建线程
【方法七】通过创建线程池,来实现多线程
Thread常见的方法
ID 是线程的唯一标识,不同线程不会重复
名称是各种调试工具用到
状态表示线程当前所处的一个情况,下面我们会进一步说明
优先级高的线程理论上来说更容易被调度到
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活,即简单的理解,为 run 方法是否运行结束了
线程的中断问题,下面我们进一步说明
注意:线程可以分为前台线程和后台线程两种。前台线程:前台线程会阻止进程的结束,直到所有的线程都执行完后,进程才结束。例如:我们创建的线程和main线程都是前台线程。
后台线程:后台线程不会阻止进程结束,进程结束不管其他线程是否都完成自己的代码。我们可以通过Thread对象.setDaemon()来将线程设置为后台线程,注意:将线程设置为后台线程这一步要在启动线程之前,否则会报错。Thread对象.setDaemon(true) 表示设置为后台线程
很多人会认为创建好一个Thread对象后,一个线程就启动了。这是一个很大的误区。创建好一个Thread对象只是将一个线程给创建好,并不是将一个线程启动了。我们想要启动一个线程 还要调用***start()***方法将线程启动。
【方法二】使用Thread对象自己带的标识位 来中断线程
Thread.currentThread.isInterrupted()方法是判断该线程是否中断,true表示中断,false表示没有中断
Thread对象.interrupt()方法是将该线程中断。
Thread对象.interrupt()的行为:
(1)中断线程的时候,线程处在阻塞状态,那么将会抛出InterruptedException异常,所以,我们要进行异常处理。
(2)中断线程的时候,线程不处在阻塞状态,那么就会将线程里面的标识位的值设置位true;
我们说线程的调度是随机的,我们无法规定哪个线程先执行,但是,我们有办法哪个线程先执行完,哪个线程后执行完。使用join()方法就行。
join()方法的行为:在哪一个线程里调用 Thread对象,join(),则该线程就要等待调用join()方法的线程先执行完,该线程才能执行完。
例题:有三个线程A,B,C,我想让线程A在线程B之前完成,线程B在线程A之前完成。
我们通过Thread.currentThread()来获取当前线程的引用
Thread.currentThread()与类的this的用法是一样的。
在不同的线程中使用Thread.currentThread(),Thread.currentThread()就表示该线程的引用
我们想要休眠某个线程,就在该线程里调用Thread.sleep(休眠时间(单位ms))
Thread.sleep(休眠时间(单位ms))行为:将线程从就绪状态转变为阻塞状态,到达我们设置的时间后,线程的状态从阻塞状态变为就绪状态
线程的状态有六种:
(1)NEW:还没有安排工作
(2)RUNNABLE:可工作的,分为正在工作和即将工作
(3)TERMINATED:工作完成了
(4)TIMED_WAITING:阻塞状态,使用了sleep()方法
(5)WAITING:阻塞状态,使用了wait()方法
(6)BLOCKED:阻塞状态,使用了锁
线程状态的关系转化图(简单版本)
线程安全的概念:如果在多线程的环境下运行程序,其结果与我们预期的结果一致,则我们说该程序是线程安全的。
程序产生线程不安全的原因
(1)操作系统对线程的随机调度(抢占式执行)(最根本的问题,我们无法避免)
(2)多个线程修改同一个变量
(3)有些修改操作不是原子的。
(4)内存的可见性,引起的线程安全
(5)指令的重排序问题,引起的线程安全
【解决多个线程修改同一个变量 和 有些操作不是原子的 所引起的线程安全问题】
多个线程修改同一个变量 和 有些操作不是原子的 所引起的线程安全问题,我们可以通过加锁的方式进行解决
加锁使用到的关键字:synchronized
锁(synchronized)的使用:
加锁的关键是要选好加锁的对象,任何对象都可以成为锁对象
锁(synchronized)可以修饰方法:
【修饰实例方法】锁对象是该类的实例
public synchronized void sleep(){
代码
}
【修饰静态方法】锁对象是该类的类对象
public static synchronized void sleep(){
代码
}
锁(synchronized)可以修饰代码块
synchronized(锁对象) {
代码
}
注意:每一个类的类对象只有一个。我们加锁的时候,比较关注锁对象,多线程要发生竞争,只有多个线程竞争同一把锁的时候才能发生竞争,也就是说多个线程竞争相同的锁对象的时候才发生竞争。多个线程针对不同的锁,则不会发生竞争
【解决 内存可见性和指令重排序引起的线程不安全问题】
方法很简单,就是在变量前面加一个 volatile 关键字 即可
操作系统对线程的调度的随机的,所以,就导致了我们无法预料哪个线程先执行,哪个线程后执行。但是,我们也有一些协调多线程顺序的办法。我们之前有说过 join()方法可以控制哪个线程先结束哪个线程后结束。sleep()方法可以让线程进入阻塞状态一定的时间,间接地是多线程执行顺序往自己预期的方向执行。当时join()和sleep()方法有时候不会达到我们想要的效果,于是就有wait()和notify()方法或者wait()和notifyAll()方法 来控制多线程的顺序。
wait()方法和sleep()方的区别:
(1)wait()方法是Object类里面的方法,而sleep()方法是Thread类里面的静态方法
(2)sleep方法的效果:sleep()方法使线程进入阻塞状态,到达我们设置的阻塞时间,线程就从阻塞状态转变为就绪状态
而wait()方法有两种效果:【第一种】wait()方法没有参数的时候,让线程进入阻塞状态,想要唤醒该线程就必须适应notify()方法或者notifyAll()方法。【第二种】wait(等待时间)方法有参数的时候,参数表示线程阻塞的时间,效果跟sleep()方法是一样的。如果想要提前唤醒该线程,就必须适应notify()方法或者notifyAll()方法。
(3)wait()方法要在有锁的情况下使用,而sleep()方法在任何地方都能使用
注意:wait()方法和notify()方法 或者wait()方法和notifyAll()方法是搭配使用的,并且他们都是Object类里面的方法,并且他们使用的时候,锁对象必须保持一致,简单的说就是锁要相同。
单例模式能够保证某个类在程序中只有唯一一个实例,而不会出现创建多个实例的情况
单例模式的实现方式有两种:饿汉模式和懒汉模式
饿汉模式:程序在运行的时候,类加载的时候创建实例
懒汉模式:程序启动的时候,先不着急创建实例,等到使用的时候在创建实例
单例模式Java代码实现(线程安全)
【饿汉模式】
【懒汉模式】
【阻塞队列是什么】
阻塞队列是一个数据结构,满足“先进先出”的原则,与我们之前学的队列有所不同的是阻塞队列的满足线程安全的。
【生产者消费者模型】
阻塞队列经典的应用场景是生产者消费者模型
生产者消费者模型:生产者和消费者之间的信息通信并不是直接通信,而是以阻塞队列为容器,通过阻塞队列来实现生产者和消费者之间的信息通。例如:生产者想要给消费者发生信息,则生产者就会将信息发给阻塞队列,而消费者则可以通过获取阻塞队列里的信息 进而获取生产者发送过来的信息。
生产者消费者模型最明显的两条优点:
(1)阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
(2)阻塞队列也能使生产者和消费者之间解耦.
【Java中实现的阻塞队列】
BlockingQueue 是一个接口.
BlockingQueue的实现类有很多,这里介绍几个:
(1)LinkedBlockingQueue类,普通的队列,满足“先进先出”,底层是有链表实现
(2)ArrayBlockingQueue类,普通的队列,满足“先进先出”,底层是有数字实现
(3)PriorityBlockingQueue类,优先级队列
注意:
没有线程安全的队列里面的方法 在阻塞队列里面都有。在阻塞队列里面调用没有线程安全的队列里面的方法是不能保证线程安全的。只有调用put()方法和take()方法才会线程安全. put()方法是进队列,take()方法是出队列
【使用Java代码简单地使用阻塞队列】
定时器是软件开发中非常重要的组件。 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.
定时器是多线程引用的重要例子
【Java中实现的定时器】
Timer类 实现类定时器。Timer 类的核心方法为 schedule .
schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒). 可以多次提交任务,根据时间的先后顺序来执行代码。
【Timer类的使用】
【悲观锁和乐观锁】
悲观锁:发生冲突的概率大。总是假设最坏的情况,每一次拿出的数据都会觉得会被修改,所以每一次在拿数据的时候都要上锁。
乐观锁:发生冲突的概率小。总是假设最好的情况,只有在数据更新的时候,才会正式对数据是否产生并发冲突进行检查,如果发生并发冲突,则让返回用户错误的信息,让用户决定如何去做。
注意:乐观锁往往以纯用户态执行,而悲观锁往往要进入操作系统内核,对当前线程进行挂起(阻塞)等待
【读写锁和普通锁】
普通锁:多个线程想要获取同一把锁,他们会发生竞争
读写锁是对锁进行了细化,将锁分为读锁和写锁。
读锁:读取数据的部分使用读锁
写锁:修改或者更新数据的部分使用写锁
使用读写锁,多线程竞争情况:
(1)多个线程读数据,并不会引发线程安全问题,读锁不加锁,并发读取
(2)有的线程读数据,有的线程写数据,会引发线程安全问题,读锁和写锁发挥作用
(2)多个线程写数据,会引发线程安全问题,写锁发挥作用。
读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁
注意:
读加锁和读加锁之间,不互斥.
写加锁和写加锁之间,互斥.
读加锁和写加锁之间,互斥.
【重量级锁和轻量级锁】
锁的核心特性 “原子性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
CPU 提供了 “原子操作指令”.
操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类
重量级锁:锁的开销比较大,做的工作比较多。重量级锁主要依赖操作系统里提供的锁,使用这种锁线程比较容易发生阻塞等待
轻量级锁:锁开销比较小,做的工作不是很多。轻量级锁尽量避开使用操作系统里面提供的锁,而是尽量在用户态来完成功能。尽量避免用户态和内核态的切换,尽量避免挂起(阻塞)等待。
【自旋锁和挂起等待锁】
自旋锁:是轻量级锁的具体表现,是一个轻量级锁。
自旋锁,一旦获取失败,并不会挂起等待,会迅速再次尝试能不能获取到锁
自旋锁的优点:
(1)没有放弃CPU,不涉及线程的阻塞和调度,能第一时间获取到锁
自旋锁的缺点:
(1)线程一直获取不到锁,对CPU的消耗会比较大
挂起等待锁:是重量级锁的具体表现,是一个重量级锁。
重量级锁,一旦获取失败,线程会挂起等待,直到获取到锁为止
重量级锁的优点:
(1)如果锁被其他线程占用,不会占用CPU
重量级锁的缺点:
(1)锁释放的时候,不能第一时间获取到锁
【公平锁和非公平锁】
任何判断是否公平?
遵循“先来后到”原则
公平锁:遵循“先来后到”原则,例如:B线程比C线程先到,A线程释放锁后,B线程获取到锁,然后再是C线程获取到锁。
非公平锁:不遵循“先来后到”原则。
【可重入锁和不可重入锁】
可重入锁:允许线程可以多次获取同一把锁,并且不发生死锁的情况
不可重入锁:不允许线程可以多次获取同一把锁,如果某线程多次获取同一把锁,则会发生死锁的情况。
【什么是CAS】
CAS:字面意思是“比较并交换”。 相当于通过一个原子的操作, 同时完成 “读取内存, 比较是否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的支撑
CAS是操作系统/硬件,给JVM提供的另一种更为轻量的原子操作机制
【CAS是怎么实现的】
一个CAS涉及以下内容:
我们设内存中源数据为A,旧的预期数据为C,要修改的新数据为B
(1)比较A与C值是否相同
(2)A与C的值不同,将A的值赋给C,再重新比较
(3)A与C的值相同,将B的值赋给A,内存中的数据就改成B了
(4)返回操作是否成功
CAS的伪代码(上面步骤的简单描述):
【CAS的应用】
CAS有两个比较经典的应用:
(1)CAS实现自旋锁
(2)CAS实现原子类
【CAS的ABA问题】
ABA问题:假设存在两个线程t1和t2,他们共享一个资源A,线程t1想通过CAS将资源A的值改了,过程:
(1)读取资源A的值到,oldNum里面
(2)比较oldNum里的值和内存里A的值
再这过程中线程t2将资源A里的值改成了B,然后有改成了A,这样就有问题了?线程t1是修改了资源A的值但是并不是之前的值,,而是由线程B修改后等到的值。这就是CAS的ABA问题。
【ABA问题的解决方案】
解决ABA问题的方案:给修改的值,引入版本号,通过比较版本号是否相同,相同则修改,不相同则不修改。
【Synchronized锁的基本特点】
(1)Synchronized既是悲观锁,也是乐观锁
(2)Synchronized既是重量级锁,也是轻量级锁
(3)Synchronized不是读写锁
(4)Synchronized是不公平锁
(5)Synchronized是可重入锁
(6)Synchronized轻量级锁部分是由自旋锁实现,重量级锁部分是挂起等待锁实现的。
【Synchronized锁的加锁过程】
(1)无锁(不加锁)
(2)偏向锁
(3)轻量级锁
(4)重量级锁
Synchronized锁在加锁的时候,一般会经历以上阶段,但是,不一定4个阶段都会经历,这要根据具体的情况来定。
介绍一下偏向锁的概念:偏向锁不是真正的加锁,一个线程在获取锁后并不是马上的进行加锁,偏向锁更像一个标记表示这个锁是我的,等到由其他线程来竞争锁的时候在加锁。
【Synchronized锁的其他优化操作】
【优化一:锁消除】
在某些程序中,有使用synchronized,但是程序并不处于多线程的情况下,是处在单线程,加不加锁结果都是一样的,反而涉及到加锁和解锁操作,都比较消耗资源,所以编译器就会自动消除锁。
【优化二:锁粗化】
一段逻辑中如果出现多次加锁解锁,编译器+ JVM会自动进行锁的粗化
实际开发过程中,使用细粒度锁,是期望释放锁的时候其他线程能使用锁.
但是实际上可能并没有其他线程来抢占这个锁.这种情况JVM就会自动把锁粗化,避免频繁申请释放锁.
【Callable 接口创建线程】
Callable是一个接口,与Runnable接口在作用是相同的,都是编写线程执行的任务,不同的是Callable接口重写的方法是由返回值的,Runnable则没有。
Callable接口创建线程的过程:
(1)创建匿名的Callable对象,重写call()方法
(2)使用辅助类FutureTask类,将Callable对象当作构造方法的参数,创建FutureTask对象,FutureTask对象是对Callable对象重写call()方法的返回值进行控制
(3)创建Thread对象,将FutureTask对象当作构造方法的参数,然后启动线程。
代码例子:
理解 Callable
Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务, Runnable 描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定. FutureTask 就可以负责这个等待结果出来的工作.
理解 FutureTask
想象去吃麻辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你一张 “小票” . 这个小票就是FutureTask. 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没
【ReentrantLock类的使用(可重入锁)】
ReentrantLock类实现了可重入锁,与Synchronized锁定位类似,都是用来实现互斥效果,保证线程安全。
(1)synchronized是关键字,以代码块为单位进行解锁,
ReentrantLock则是一个类,提供lock方法加锁,unlock解锁
(2)ReentrantLock默认是非公平锁,但是 在构造实例的时候,可以指定参数,将锁变为公平锁。
synchronized是非公平锁,ReentrantLock提供了公平锁和非公平锁两种实现,可以通过构造方法进行切换。
(3)ReentrantLock还提供了一个特殊的加锁操作,tryLock()
默认的lock加锁失败,就阻塞
而tryLock()加锁失败,则不阻塞,直接往下执行,并且返回false
除了立即失败外,tryLock()还能设定一定的等待时间
(4)ReentrantLock提供了更强大的等待/唤醒机制
synchronized搭配的是Object的wait和notify,唤醒的时候,随机唤醒一个线程
ReentrantLock搭配的是Condition类来实现等待唤醒,可以做到随机唤醒一个线程,也可以指定某个线程唤醒
lock(): 加锁, 如果获取不到锁就死等.
trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
unlock(): 解锁
在加锁与解锁之间写代码
<代码例子>
<如何选择使用哪个锁?>
(1)锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
(2)锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
(3)如果需要使用公平锁, 使用 ReentrantLock.
【原子类】
原子类是一个线程安全的类。
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多
原子类满足了简单的递增或者递减的需求场景
原子类有以下几个:
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicReference
AtomicStampedReference
以 AtomicInteger 举例,常见方法有
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i–;
incrementAndGet(); ++i;
getAndIncrement(); i++;
【线程池】
<为什么要有线程池>
虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.
线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 “池子” 中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.
<线程池的使用>
<方式一>通过ExecutorService类 和 Executors类 创建线程池
ExecutorService 表示一个线程池实例.
Executors 是一个工厂类, 能够创建出几种不同风格的线程池.
ExecutorService 的 submit 方法能够向线程池中提交若干个任务
<代码例子>
<方法二>使用ThreadPoolExecutor类创建实例,进而创建线程池(核心)
ThreadPoolExecutor类的构造方法:
ThreadPoolExecutor类的构造方法的构造方法的解释:
把创建一个线程池想象成开个公司. 每个员工相当于一个线程.
corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)
maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).
keepAliveTime: 临时工允许的空闲时间.
unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
workQueue: 传递任务的阻塞队列
threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.
AbortPolicy(): 超过负荷, 直接抛出异常.
CallerRunsPolicy(): 调用者负责处理
DiscardOldestPolicy(): 丢弃队列中最老的任务.
DiscardPolicy(): 丢弃新来的任务.
<通过ThreadPoolExecutor类创建线程池>
【信号量 Semaphore】
信号量,用来表示“有效资源的个数”,本质上是一个计数器
获取的资源超过了我们设定的资源数量,程序会阻塞
<理解信号量>
可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源. 当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源
Java中,Semaphore类实现了信号量
Semaphore类的构造方法:
1.使用给定数量的许可和非空中公平设置创建信号量
2.使用给定的许可数量和给定的公平性设置创建信号量。
Semaphore类主要的方法:
acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
【CountDownLatch】
CountDownLatch类是倒计时锁存器的实现。
倒计时锁存器是一种同步辅助,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
CountDownLatch类可以实现同时等待N个线程结束。
CountDownLatch类的构造方法:
构造方法的参数count表示给倒计时锁存器设置一个初始值,可以等待count个线程结束
CountDownLatch类的方法:
(1)await()方法:导致当前线程等待,直到锁存器已倒计时到零,除非线程被中断
(2)await(long timeout, TimeUnit unit)方法:导致当前线程等待,直到锁存器已倒计时为零,除非线程中断或指定的等待时间已过。
(3)countDown()方法:递减锁存器的计数,如果计数达到零,则释放所有等待线程
(4)getCount()方法:返回当前计数
构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了
我们之前学的基本数据结构基本都是线程不安全的类
<线程不安全的集合类>
ArrayList,LinkedList,PriorityQueue,TreeSet,HashSet,TreeMap,HashMap…
【多线程环境使用 ArrayList】
<方法一>自己使用同步机制 (synchronized 或者 ReentrantLock)
<方法二>使用Collections.synchronizedList(new ArrayList<>())返回一个线程安全的List对象;
<方法三>使用 CopyOnWriteArrayList类
CopyOnWrite容器即写时复制的容器。
【多线程环境使用队列】
(1) ArrayBlockingQueue
基于数组实现的阻塞队列
(2) LinkedBlockingQueue
基于链表实现的阻塞队列
(3) PriorityBlockingQueue
基于堆实现的带优先级的阻塞队列
(4) TransferQueue
最多只包含一个元素的阻塞队列
【多线程环境使用哈希表】
HashMap本身是线程不安全的。
在多线程环境下使用哈希表可以使用:
Hashtable
ConcurrentHashMap(建议使用)
Hashtable类和ConcurrentHashMap类加锁的方式是不一样的
Hashtable类只是简单的将关键的方法加锁,就相当于对Hashtable对象加锁,每一次的读或者写都要发生竞争。
ConcurrentHashMap类则是Hashtable类的改进版本:
(1)读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然
是是用 synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率.
(2)充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况
(3)优化了扩容方式: 化整为零.就是说 在扩容的时候,并不是一次性全部扩容好,每一次扩容一些
【什么是死锁】
死锁:多个线程同时阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
【出现死锁的情况】
(1)一个线程多次获取同一把锁
(2)两个线程两把锁
(3)N个线程M把锁
【死锁的四个必要条件】
(1)互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
(2)不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
(3)请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
(4)循环等待(最重要),即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样
就形成了一个等待环路
【如何避免死锁】
产生死锁的必要条件前三个,互斥使用、不可抢占、请求和保持,都是锁的基本特点,我们无法改变,而循环等待是通过我们写出的代码决定的,所以,我们可以从循环等待这一方面入手。
<破坏循环等待>
最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号(1, 2, 3…M).
N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.
我们在编写多线程代码的时候,要加锁的部分,我们要注意死锁情况