一 并发编程简介
什么是并发编程
- 并发历史
早期计算机从头到尾执行一个程序,浪费资源,效率低 操作系统出现后,计算机能运行多个程序,不同程序在不同的单独进程中运行
一个进程有多个线程,提高了资源利用率
- 串行与并行的区别
好处:可以缩短整个流程的时间,提高效率 - 并发编程的目的
更加充分利用计算机资源,加快程序响应速度,简化异步事件的处理
- 什么时候时候并发编程
任务会阻塞线程,导致之后的代码不能执行:例如边从文件读取边进行大量计算
任务执行时间长,可划分为分工明确的子任务:例如分段下载
任务间断性执行:日志打印
任务本身需要协作执行:例如生产者消费者问题
上下文切换
CPU为线程分配时间片,不停切换线程执行,在切换前会保存上一个任务的状态,以便切换回该任务时可以再次加载其状态。
上下文切换为什么有较大性能开销:
- CPU从用户态要切换到内核态
- 要保存切换前线程的执行指针到内存块,再切换到新线程,在新线程执行完后要从内存块读回中断线程的指针继续完成执行
如何减少上下文切换的开销?
- 无锁并发编程,多线程竞争锁时会引起上下文切换,所以多线程处理数据时,可以用一些办法避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同的数据
- CAS算法 Java的Atomic包使用CAS算法更新数据,而不需要加锁
- 是用最少的线程 避免创建不需要的线程,如任务状态很少但创建多线程处理,会造成大量线程处于等待状态
- 协程 在单线程中实现多任务调度,并在单线程里维持多任务切换。(Java很少用)
死锁
线程A获得了资源1,需要获得资源2,而线程B获得了资源2,需要获得资源1,两个线程都不释放资源又在等待对方释放资源,此时就发生了死锁。
public class DeadLock {
private static final Object resource1=new Object();
private static final Object resource2=new Object();
public static void main(String[] args) {
Thread threadA=new Thread(()->{
synchronized (resource1){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2){
System.out.println("...");
}
}
});
Thread threadB=new Thread(()->{
synchronized (resource2){
synchronized (resource1){
System.out.println("...");
}
}
});
threadA.start();
threadB.start();
}
}
运行程序时发生了死锁,我们在控制台输入:jps
可以找到发生死锁的java进程
然后输入jstack 7340
可以查看到具体的死锁信息
可以看到线程1在等待线程0的资源,而线程0也在等待线程1的资源,互不相让所以死锁了。
线程安全
死锁容易通过jdk的工具定位发现问题,如上述的jps,jstack,但线程安全问题运行时并无明显特征,然而结果可能不是你的预期结果。
public class UnsafeThread {
private static int num;
public static void inCreate(){
num++;
}
public static void main(String[] args) throws InterruptedException{
//创建10000个线程,执行对num的加1操作
for(int i=0;i<10000;i++){
new Thread(UnsafeThread::inCreate).start();
}
Thread.sleep(1000);//保证线程执行完毕
System.out.println(num);//结果可能不为10000
}
}
在我们的预期中,创建10000个线程,每个线程对静态变量num执行一次++操作,结果应为10000,但输出的结果很可能不是10000,这就说明存在线程安全问题。
有可能线程1读取了num,此时num为0,然后对它加1,由于不是同步的,在加1之前,线程2又读取到了num,由于线程1还未完成加1操作,所以线程2读取到的值也是0,然后各自完成加1操作,结果num还是为1,正常的预期结果应该是2,由此一来最终结果就未必是10000了。
资源限制
- 硬件资源
如服务器带宽、硬盘读写速度和CPU的处理速度
- 软件资源
如数据库连接(500个连接,1000个查询线程,速度并不会加快),socket
二 线程基础
进程与线程的区别
- 进程:系统进行分配和管理资源的基本单位
- 线程:进程的一个执行单元,是进程内调度的实体,是CPU调度和分配的基本单位,是比进程更小的独立运行的基本单位,线程也被称为轻量级进程,是程序执行的最小单位。
- 一个程序至少有一个进程,一个进程至少有一个线程。
- 进程有独立而都地址空间,每启动一个进程系统就会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,开销较大。
- 线程共享进程中的数据,使用相同的地址空间,因此CPU切换线程的开销远小于进程,同时创建和撤销一个线程的开销也比进程小。
- 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式进行。
- 多进程程序更健壮,由于进程有独立地址空间,一个进程崩溃后在保护模式下不会对其他进程产生影响,而线程只是一个进程中不同的执行路径,线程有自己的堆栈和局部变量,但线程没有独立的地址空间,因此一个线程出现问题可能会影响整个程序。
线程的状态及其相互转换
- 1 NEW 初始
- Thread state for a thread which has not yet started.
- 新创建了一个线程对象,但还未调用start()方法。
- 2 RUNNABLE 可运行
- Thread state for a runnable thread.
A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.- 处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统(如处理器)的其他资源。
- 3 BLOCK 阻塞
- Thread state for a thread blocked waiting for a monitor lock.
A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling Object.wait.- 等待监视器锁定时被阻止的线程的线程状态。
处于阻塞状态的线程正在等待监视器锁进入同步块/方法,或者在调用Object.wait之后进入同步块/方法。
- 4 WAITING 等待
- A thread is in the waiting state due to calling one of the following methods: Object.wait 、Thread.join、LockSupport.park
A thread in the waiting state is waiting for another thread to perform a particular action.
For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() orObject.notifyAll() on that object.
A thread that has called Thread.join() is waiting for a specified thread to terminate.- 线程处于等待状态,原因是调用了以下某个方法: Object.wait、Thread.join、LockSupport.park
处于等待状态的线程正在等待另一个线程执行特别行动。
例如,在对象上调用Object.wait()的线程正在等待另一个线程在该对象上调用Object.notify()或Object.notifyAll()。
调用thread.join()的线程正在等待指定线程终止。
- 5 TIME_WAITING 计时等待
- Thread state for a waiting thread with a specified waiting time.
A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:
Thread.sleep、 Object.wait、Thread.join、LockSupport.parkNanos、 LockSupport.parkUntil- 具有指定等待时间的等待线程的线程状态。
由于使用指定的正等待时间调用以下方法之一,线程处于定时等待状态:
Thread.sleep、Object.wait、Thread.join、LockSupport.parknos、LockSupport.parkUntil
- 6 TERMINATED 终止
- Thread state for a terminated thread. The thread has completed execution.
- 终止线程的线程状态。 线程已完成执行。
创建线程的方式
- 第一种 继承Thread类并重写run()方法
public class MyThread extends Thread{
@Override
public void run(){...}
public static void main(String[] args){
new MyThread().start();
}
}
- 第二种 实现Runnable接口并重写run()方法,将该类实例作为参数传入Thread
public class MyThread implements Runnable {
@Override
public void run() {...}
public static void main(String[] args) {
new Thread(new MyThread()).start();
}
}
//补充:也可使用线程池 将实现Runnable接口的类的实例作为参数传入executorService
public class MyThread implements Runnable {
@Override
public void run() { ...}
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new MyThread());
}
}
实际开发中主要使用第二种。
第一,因为Java只允许单继承,如果继承了Thread类就无法继承其他类,但Java允许实现多接口,可以继承其他类并实现Runnable接口。
第二,可以增加程序的健壮性,降低了线程对象和线程任务的耦合度。因为继承Thread类的话那该线程的任务就定死了,而实现Runnable接口的话,可以通过传入不同的实例来让线程实现不同的任务。
注意:直接调用run()方法不能创建一个新的线程,当于在当前线程调用了run()方法,创建线程需要使用start()方法。
线程的挂起和恢复
- 什么是挂起线程
实质上是指使线程进入非RUNNABLE状态,该状态下CPU不会给线程分配时间片,进入该状态可用来暂停一个线程的运行。
线程挂起后,可通过重新唤醒线程使其恢复运行。
- 为什么要挂起线程
CPU分配时间片非常短,资源很珍贵,挂起无用线程可避免资源的浪费。
- 如何挂起线程
被废弃的方法:
suspend() 该方法不会释放线程所占资源,如果使用该方法将某个线程挂起,则可能会使其他资源的线程死锁 resume() 方法本身无问题,但不能独立于suspend()方法存在
可以使用的方法:
wait() 暂停执行,放弃已获得的锁,进入等待状态
notift()/notifyAll() 随即唤醒一个等待锁的线程/唤醒所有等待锁的线程,自行抢占CPU资源
public class WaitTest implements Runnable {
private static final Object lock=new Object();
@Override
public void run() {
synchronized (lock){
System.out.println("开始,线程准备挂起");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程恢复运行,结束");
}
}
public static void main(String[] args) throws InterruptedException {
Thread waitThread = new Thread(new WaitTest());
waitThread.start();
Thread.sleep(100);//保证线程挂起发生于唤醒前
synchronized (lock) {//挂起和唤醒需要持有相同的锁
System.out.println("准备唤醒挂起线程");
lock.notify();
}
}
}
输出结果:
开始,线程准备挂起
准备唤醒挂起线程
线程恢复运行,结束
- 什么时候适合挂起线程
等待某些未就绪的资源,直到被唤醒
线程的中断操作
废弃方法:
stop() 因为一旦调用线程就立刻停止,可能引发线程安全问题。
interrupt() 向线程发送中断请求,将中断状态置为true,如果线程仍在运行并不会中断
interrupted() 静态方法,检测当前线程是否被中断,如中断状态为true则中断
public class InterruptTest implements Runnable {
@Override
public void run() {
while (!Thread.interrupted()){//若为while(true)则interrupt不会中断程序
System.out.println("123");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptTest());
thread.start();
Thread.sleep(100);//保证执行先于中断
thread.interrupt();//程序在此处结束
}
}
自行定义一个标志,需要用volatile修饰
public class InterruptTest implements Runnable {
private static volatile boolean flag=true;//不加volatile可能出错,保证修改可见性
@Override
public void run() {
while (flag){
System.out.println("123");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptTest());
thread.start();
Thread.sleep(100);
flag=false;//程序中断
}
}
线程优先级
当大量线程在等待运行时,CPU优先将时间片分配给优先级高的线程,这不代表优先级低的线程不会运行,只是它被运行的机会会小一点。
线程优先级可以为1-10的任意数值,Thread中定义了三个线程优先级,MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10),一般情况推荐使用这三个常量,不要自行设置。
使用 setPriority()
设置优先级。
不同平台对优先级的支持不同,尽量不要依赖优先级。
守护线程
- 线程分类:
用户线程 用户线程是独立存在的,不会因为其他用户线程退出而退出
守护线程 依赖于用户线程,只要有活着的用户线程,守护线程就活着。当JVM中最后一个非守护线程结束时,就随JVM一起退出。 - 用处:
JVM垃圾清理线程 - 建议:
尽量不使用守护线程,因其不可控,不要在守护线程中读写文件,执行计算逻辑 - 设置守护线程
setDaemon(true)
,需要在调用start()方法前设置。
三 线程安全性
什么是线程安全性
当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
多线程并发访问时,得不到正确的预期结果即线程不安全。
从字节码角度剖析线程不安全操作
在命令行窗口对先前线程安全一节中的示例文件反编译
使用javac -encoding UTF-8 UnsafeThread.java
编程成.class文件
使用javap -c UnsafeThread.class
进行反编译,得到相应的字节码指令
得到对inCreate方法中num++对应的反编译结果:
public static void inCreate();
Code:
0: getstatic 获取指定类的静态域,并将其压入栈顶
3: iconst_1 将int型的1压入栈顶
4: iadd 将栈顶两个int型相加,将结果压入栈顶
5: putstatic 为指定静态域赋值
8: return
线程不安全的原因,num++不是原子性操作,而是被拆分如上3个步骤,在多线程并发执行情况下,由于CPU调度多线程快速切换,有可能两个线程同一时刻读取了同一个num值,对其进行了加1操作,导致线程不安全。
原子性操作
- 什么是原子性操作
一个操作或多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
- 如何把非原子性操作变为原子性
可使用synchronized关键字修饰方法
synchronized关键字
synchronized是JVM内置锁,通过内部对象monitor(监视器锁)实现,基于进入与退出monitor对象实现方法与代码块同步,监视器锁的实现依赖于底层操作系统的互斥锁。
synchronized在字节码中译为指令:monitorenter、monitorexit
- Monitor
每个同步对象都有一个自己的monitor(监视器锁)- 内置锁
每个Java对象都可以用作一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。
获得内置锁的唯一途径就是进入这个锁保护的同步代码块或方法。- 互斥锁
内置锁是一个互斥锁,这意味着最多只有一个线程能获得该锁,当线程A尝试获得线程B持有的内置锁时,线程A必须等待或阻塞,直到线程B释放这个锁,如果线程B不释放,那么线程A将永远等待下去。
- 修饰实例方法:锁住的是对象的实例
public class SynTest implements Runnable {
@Override
public synchronized void run() {
System.out.println("run执行了");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new SynTest()).start();
new Thread(new SynTest()).start();//两句话几乎是同时输出的
}
}
- 修饰类方法:锁住的是整个类
public class SynTest implements Runnable {
@Override
public void run() {
test();
}
private synchronized static void test(){
System.out.println("run执行了");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new SynTest()).start();
new Thread(new SynTest()).start();//两句输出间隔了3秒
}
}
- 修饰代码块:锁是括号内的对象
public class SynTest implements Runnable {
@Override
public void run() {
synchronized (SynTest.class){//锁住的是整个类
System.out.println("run执行了");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(new SynTest()).start();
new Thread(new SynTest()).start();//两句输出间隔3秒
}
}
-------------------------------------------------------------
public class SynTest implements Runnable {
private final Object lock=new Object();
@Override
public void run() {
synchronized (lock){//锁住的是实例对象 各自不影响
System.out.println("run执行了");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(new SynTest()).start();
new Thread(new SynTest()).start();//两句几乎同时输出
}
}
volatile关键字
- 作用范围
仅能修饰变量 ,保证其可见性,不可保证原子性,禁止指令重排序
两个线程同时读取volatile关键字修饰的变量,A读取后修改了变量的值,该操作对B是可见的
- 使用场景:
作为线程开关 /单例,修饰对象实例,禁止指令重排
public class SynTest implements Runnable {
private volatile static boolean flag=true;
@Override
public void run() {
while (flag) {
System.out.println("true");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new SynTest()).start();
Thread.sleep(1000);
flag=false;//输出两次true后停止运行
}
}
单例与线程安全
- 饿汉式 本身线程安全,在类加载时无论之后是否使用都会实例化,可能会浪费资源。
//饿汉式 线程安全
public class Hungry{
private static Hungry hungry=new Hungry();
private Hungry(){}
public static Hungry getHungry(){
return hungry;
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
System.out.println(Hungry.getHungry());//打印出的10个对象地址相同
}).start();
}
}
}
- 懒汉式 在需要的时候在实例化,但线程不安全。
public class Lazy {
private static Lazy lazy=null;
private Lazy(){}
public static Lazy getLazy(){
if(lazy==null){
//模拟实例化对象耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazy=new Lazy();
}
return lazy;
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
System.out.println(Lazy.getLazy());//打印出的10个对象地址都不同
}).start();
}
}
}
--------------------------------------------------------------------
public class Lazy {
private static volatile Lazy lazy=null;//禁止指令重排 懒汉式必须加volatile
private Lazy(){}
public static Lazy getLazy(){//此处加synchronized也可线程安全 但效率低
if(lazy==null){
//模拟实例化对象耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//1
synchronized (Lazy.class) {
if(lazy==null)//不加这行 线程仍不安全 有可能线程都执行到1处 阻塞后恢复时lazy已经不为空 然后依次仍创建多个实例
lazy = new Lazy();//所以需要再次判断lazy不为空
}
}
return lazy;
}
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
System.out.println(Lazy.getLazy());//打印出的10个对象地址都相同
}).start();
}
}
}
如何避免线程安全问题
- 线程安全问题产生原因
1 多线程环境
2 多线程操作同一共享资源
3 对该共享资源的操作是非原子性的
- 如何避免
1 将多线程改为单线程 (必要的代码加锁访问)
2 不共享资源 (ThreadLocal、不共享、共享资源不可变–final)
3 将非原子性操作改为原子性操作 (加锁、使用JDK自带的原子性操作的类、JUC提供的并发工具)
四 锁
锁的分类
线程是否要锁住同步资源
- 乐观锁
总是假设最好的情况,每次获取数据都假设别人不会修改,所以不会上锁,但是在更新时会判断在此期间别人是否更新了该数据
实现:CAS算法,例如AtomicInteger类的原子自增通过CAS自旋实现
适用场景:读操作较多,不加锁的特点使其性能大幅提高- 悲观锁
总是假设最坏的情况,认为自己在使用数据时一定有别的线程修改数据,在获取数据时先加锁
实现:synchronized和Lock接口的实现类
适用场景:写操作比较多,先加锁保证写操作数据正确
补充:CAS算法
Compare And Swap 比较与交换
无锁算法:基于硬件原语实现,在不使用锁(没有线程被阻塞)的情况下实现多线程间的变量同步
算法涉及三个操作数:
- 需要读写的内存值V
- 进行比较的值A
- 要写入的新值B
A即读取到的旧V值,然后在更新V为B前,检查A值是否与V值相等,如果相等则进行更新,否则将自己的A更新为新的V值,继续自旋等待,直至更新V为B前检查到A值与V值相等。
举个例子,线程1和2同时读取到了V=1,各自的A=V=1,线程1先比较了A=V=1,将V更新为2,此时V=2,然后线程2进行检查,发现自己的A=1!=V=2,于是将自己的A改为2,自旋,下一次继续检测,如果A=V就更新V为B,否则继续自旋。
存在的问题:
- ABA问题
借用上面的例子,假设线程2读取到A=1后,线程1更新完了值,这时线程3又进行了操作,将V改回了1,接下来当线程2执行更新前检查时,发现V=1,然后进行了更新。
也就是说线程2并未发现V被操作过,这种问题就是ABA问题,解决的方式是给变量加上版本号,例如开始时是1-0,线程1更新后是2-1,线程3更新后是3-0,这样线程2读取到的A是1-0,再次判断时是3-0,就解决了ABA问题。- 只能保证一个共享变量的原子操作
- 循环时间长,开销大
锁住同步资源失败,线程是否要阻塞,若要阻塞
- 自旋锁
当线程获取锁失败时不会被挂起,而是循环等待,不断地判断锁能否被成功获取,直到获得锁才会退出循环,适用于同步代码块逻辑简单执行时间很短的情况。- 适应性自旋锁
假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,可根据上一次自旋时间与结果调制下一次自旋时间。- JDK1.7后自旋锁的参数被取消,自旋锁总是会执行,自旋次数也由JVM自动调整。
多个线程竞争时是否要排队
- 公平锁
公平锁指多个线程按照申请锁的顺序获取锁。- 非公平锁
与公平锁相反,有可能后申请的线程比先申请的线程优先获得锁,可能造成优先级反转或饥饿现象。
一个线程的多个流程能不能获得同一把锁
- 可重入锁
指可重复递归调用的锁,在外层使用锁后内层仍然可以使用并不会发生死锁(前提是同一个对象或类),ReentrantLock和synchronized都是可重入锁。- 不可重入锁
与可重入锁则相反,递归调用时会发生死锁。
多个线程能不能共享一把锁
- 共享锁
该锁可被多个线程持有,如ReentrantReadWriteLock里的读锁是可被共享的。- 互斥锁
在访问共享资源前进行加锁操作,当访问完成后解锁,加锁后其他任何试图再次加锁的线程会被阻塞直到当前线程解锁。
- 读写锁
读写锁既是互斥锁又是共享锁,读锁是共享的,写锁是互斥的。 读写锁有三种状态,读加锁,写加锁和不加锁。
在Java中的具体实现是ReentrantReadWriteLock。
多个线程竞争同步资源的流程细节差别
锁的状态是通过对象监视器在对象头中的字段表明的,这些状态都不是锁而是JVM为了提高锁的使用效率所做的优化(使用synchronized时)。
- 无锁
不锁住资源,多个线程只有一个能修改资源成功,其他会重试- 偏向锁
指一段同步代码一直被一个线程访问,那么该线程会自动获取锁。- 轻量级锁
指锁是偏向锁时,被另一个线程访问,偏向锁会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。- 重量级锁
指当锁为轻量级锁时,另一个线程虽然自旋但不会一直持续,但自旋一定次数时若仍未获取到锁,就会阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,降低性能。
LOCK接口
- Lock与synchronized的区别
1.用法: lock对锁的操作需要手动控制,显示地指定起始位置和终止位置,一般使用ReetrantLock类作为锁,加锁使用lock(),解锁使用unlock(),一般将unlock()写在finallly中防止死锁。
synchronized加在方法上或代码块中,括号内表示需要锁的对象。
2.性能: synchronized是托管给JVM执行的,在JDK1.5中性能较低,在JDK1.6中进行了很多优化,提升了性能。synchronized采用的是悲观锁机制,Lock采用的是乐观锁机制,乐观锁的实现机制就是CAS操作,Compare
and Swap。
未来可能更会提升synchronized的属性,因为它是JVM的内置属性,它能执行一些优化。
3. 用途
当需要一些高级功能时才应该使用ReentrantLock,如可定时的、可轮询的与可中断的锁获取操作,公平队列以及非块结构的锁。否则还是应该优先使用synchronized。
还是第一节中线程不安全的num++示例,把synchronized关键字改为锁也可解决问题:
public class UnsafeThread {
private static int num;
private static final Lock lock=new ReentrantLock();//创建锁
private static void inCreate(){
lock.lock();//加锁
num++;
lock.unlock();//解锁
}
public static void main(String[] args) throws InterruptedException {
//创建10000个线程,执行对num的加1操作
for(int i=0;i<10000;i++){
new Thread(UnsafeThread::inCreate).start();
}
Thread.sleep(1000);//保证10000个线程运行完毕
System.out.println(num);//结果可能为10000
}
}
- 实现了Lock接口的锁
- Lock的方法
lock()加锁,lockinterruptibly()允许中断的加锁,tryLock()尝试加锁及带超时参数的尝试加锁,unlock()解锁,newContidition()返回一个条件对象,用来进行类似wait/notify的操作。
实现自己的锁
先看第一个版本:
public class MyLock implements Lock {
private boolean flag;
@Override
public void lock() {
if(flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag=true;
}
@Override
public void unlock() {
notify();
flag=false;
}
主要实现lock和unlock方法 其他就先省略了
}
----------------------------------------------
public class Test {
private Lock lock=new MyLock();
public static void main(String[] args) {
new Test().methodA();
}
private void methodA(){
lock.lock();
System.out.println("进入方法A");
methodB();
lock.unlock();
}
private void methodB(){
lock.lock();
System.out.println("进入方法B");
lock.unlock();
}
}
运行后会发现抛出异常java.lang.IllegalMonitorStateException,当我们使用wait和notify时要保证调用wait或notify方法的线程含有的锁资源是同一个,所以必须使用synchronized修饰lock和unlock方法。
当我们用synchronized修饰了lock和unlock方法后,异常消失,但只能输出“进入方法A”,然后程序一直卡住,这说明了我们实现的锁并非可重入锁,需要继续改进。
public class MyLock implements Lock {
private boolean flag;//判断锁是否被持有
private int count;//记录重入次数
private Thread nowThread;//记录当前持有锁的线程
@Override
public synchronized void lock() {
if(flag&&Thread.currentThread()!=nowThread){//如果锁被持有并且当前线程不是持有锁的那个线程则等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果锁未被持有或当前线程就是持有锁的那个线程
flag=true;
count++;//重入次数加1
nowThread=Thread.currentThread();
}
@Override
public synchronized void unlock() {
if(Thread.currentThread()==nowThread) {//若当前线程是持有锁线程
count--;//可重入次数减1
if(count==0){//重入次数为0,即无线程持有锁时再解锁,设置标志为false
notify();
flag = false;
}
}
}
.....
}
代码正常输出“进入方法A”->“进入方法B”。
private void methodA(){
lock.lock(); //flag设为true count=1 now线程为该线程
System.out.println("进入方法A");
methodB();
lock.unlock();//该线程就是持有锁线程 count减一后为0 falg为false并唤醒(本例中无等待线程)
}
private void methodB(){
lock.lock();//flag仍为true count=2 now线程仍为该线程
System.out.println("进入方法B");
lock.unlock();//该线程就是持有锁线程 count减一后为1
}
AbstractQueuedSynchronizer
- 隐式锁 synchronized 基于JVM的内置锁,加锁与解锁过程不需要在代码中人为控制,而是托管给JVM自动执行
- 显式锁 主要围绕AQS抽象队列同步器实现,加锁和解锁过程需要手动编写代码控制
AQS是一个用于构建锁和同步器的框架,基于FIFO双向队列实现。
基于AQS构建的锁和同步器包括:ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask。
CLH队列
一种基于双向链表的队列,Java中的CLH队列是原CLH的变种,线程由自旋机制改为阻塞机制。
AQS中队列的结点类型为Node,用来存放被阻塞的线程。
Node结点属性
- EXCLUSIVE 独占模式
悲观锁使用,如JUC中的ReentrantLock- SHARED 共享模式
乐观锁使用,如JUC中的CountDownLatch- head 队首
公平锁,队首的下一个元素出队获得锁,head总为null。
非公平锁,线程直接尝试获得锁,失败才进入队列。- tail 队尾
- thread 被阻塞的线程
阻塞线程有4种状态
– CANCELLED = 1 将该状态线程剔除队列
– SINGAL = -1 该状态线程等待唤醒,正常竞争锁
– CONDITION = -2 该状态线程处于条件队列中,不在同步队列中
– PROPAGATE = -3 共享模式下,当前结点执行release操作后,需要传播通知给后面所有节点
AQS其他属性
- state
表示同步状态值,由volatile修饰保证可见性,大于0表示获取了锁,等于0表示无锁- exclusiveOwnerThread
持有独占锁的线程
主要方法
- acquire()
调用tryAcquire (),若失败则通过addWaiter()加入等待队列,acquireQueued()再次自旋尝试获得锁,若再次失败通过shouldParkAfterFailedAcquire()判断是否需要阻塞该线程,若需要则阻塞。- tryAcquire ()
尝试以独占模式获取锁,成功返回true,失败返回false。- tryRelease()
尝试释放锁- getState()
获取state值,final方法- setState()
修改state值,final方法
ReentrantLock源码解析
-ReentrantLock中有一个抽象内部类Sync,两个内部类NonfairSync和FairSync继承了Sync,重写了lock()方法和tryAcquire()方法,分别实现了非公平锁和公平锁。
ReentrantLock默认为非公平锁,如果想创建公平锁,可给构造方法传入参数true
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
- 非公平锁的lock()方法
弊端:可能导致排队的线程一直无法得到CPU资源的饥饿现象
public void lock() {
sync.lock();
}
1.调用NofairSync中的lock方法
final void lock() {
if (compareAndSetState(0, 1))//AQS类的方法 使用CAS算法更新state的值
setExclusiveOwnerThread(Thread.currentThread());//若更新成功设置当前线程为独占线程c
else
acquire(1);//若CAS更新失败,执行是AQS类的acquire()方法
}
2.AQS中的acquire()方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&//尝试获取锁 失败则调用addWaiter方法创建结点并追加到队列尾部
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//然后调用acquireQueued阻塞或者自旋尝试获取锁
selfInterrupt();//在 acquireQueued 中,如果线程是因为中断而退出的阻塞状态会返回true
}
3.Nofair中的tryAcquire()重写
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//如果当前state值为0 并CAS操作成功 独占锁 返回true
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}//否则判断当前线程是否是持有锁的那个独占线程
else if (current == getExclusiveOwnerThread()) {//相当于重入锁
int nextc = c + acquires;//是将state值更新
if (nextc < 0) // 假如超过最大可重入次数
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;//state不为0,并且不是那个持有锁的线程 返回false
}
- 非公平锁的unlock()方法
public void unlock() {
sync.release(1);
}
1.调用AQS的release方法
public final boolean release(int arg) {
if (tryRelease(arg)) {//如果尝试释放锁成功
Node h = head;
if (h != null && h.waitStatus != 0)//队头不为空或waitStatus不为0说明有等待线程
unparkSuccessor(h);//唤醒后续线程
return true;
}
return false;//释放失败直接返回false,独占模式下释放失败表示线程不持有锁
}
2.ReentrantLock重写了AQS的tryRelease()方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//保存state减去释放次数后的值
if (Thread.currentThread() != getExclusiveOwnerThread())//假如当前线程不是持有锁的那个线程
throw new IllegalMonitorStateException();//抛出非法监视器状态异常
boolean free = false;//设置标志位,表示释放可以释放锁
if (c == 0) {//如果state减去释放次数后为0
free = true;//可以释放锁
setExclusiveOwnerThread(null);//设置持有锁线程为空
}
setState(c);//若仍处于重入状态,更新state值
return free;//返回释放可释放锁
}
- 公平锁的lock()方法(unlock()方法和非公平锁相同)
public void lock() {
sync.lock();
}
1. 调用FairSync中的lock方法
final void lock() {
acquire(1);
}
2. AQS中的acquire()方法
3. Fair中的tryAcquire()重写
跟非公平锁的唯一区别是多了!hasQueuedPredecessors()的条件判断:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&//如果之前没有其他线程等待获取锁
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
多线程debug
1.在debug里点击这两个红色圆圈图标
2.设置suspend为thread即可
ReentrantReadWriteLock
首先不加读写锁锁,举一个例子:
public class Test {
private int i;
private int j;
public void read(){
System.out.println(Thread.currentThread().getName()+"-> i="+i+"->j="+j);
}
public void write(){
i++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
j++;
}
public static void main(String[] args) {
Test test = new Test();
for (int i = 0; i < 3; i++) {
new Thread(()->{
test.write();
test.read();
}).start();
}
}
}
输出为:
Thread-0-> i=3->j=1
Thread-1-> i=3->j=1
Thread-2-> i=3->j=1
可见在write()还未执行完时,read()方法就执行了
因此我们分别添加读写锁(读读共享、读写和写写互斥):
public class Test {
private int i;
private int j;
private ReadWriteLock lock=new ReentrantReadWriteLock();
private Lock readLock=lock.readLock();
private Lock writeLock=lock.writeLock();
public void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"-> i="+i+"->j="+j);
}finally {
readLock.unlock();
}
}
public void write(){
writeLock.lock();
try{
i++;
Thread.sleep(100);
j++;
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
Test test = new Test();
for (int i = 0; i < 3; i++) {
new Thread(()->{
test.write();
test.read();
}).start();
}
}
}
输出为:
Thread-0-> i=3->j=3
Thread-2-> i=3->j=3
Thread-1-> i=3->j=3
因为给write()方法加了写锁,所以保证i和j的值相等
注意一个习惯:要在finally中unlock()
- 如何用单一int值表示读写两种状态
将32位int值拆分为2部分,高位(前16位)表示读锁,地位(后16位)表示写锁。
两种锁的最大次数均为65535即16位全为1。
高位(读锁) 低位(写锁)
0000000000000000 0000000000000000
读锁每次加1,相当于加65536(左移16位)
0000000000000001 0000000000000000
如果要获取读锁的个数,就将其无符号右移16位
0000000000000001 0000000000000000 右移16位 👇
0000000000000000 0000000000000001
-----------------------------------------------------
写锁每次加1,相当于加1
0000000000000000 0000000000000001=1
如果要获取写锁的个数,就将其与65535进行与运算
0000000000000000 0000000000000001
0000000000000000 1111111111111111 与运算 👇
0000000000000000 0000000000000001
- 锁降级
写线程在获取写锁后可以获取读锁,然后释放写锁,这样就从写锁变成了读锁。
锁降级后,写锁不会直接降为读锁,必须显式地释放写锁。
应用场景:对数据敏感,需要在修改数据后,获取到修改后的值
public class ReadAndWrite {
private int count;
public void work(){
count++;
try {//模拟耗时操作,如从数据库获取数据
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第"+count+"次获取数据");
}
public static void main(String[] args) {
ReadAndWrite readAndWrite = new ReadAndWrite();
for (int i = 0; i <3 ; i++) {
new Thread(readAndWrite::work).start();
}
}
}
输出结果为:
第3次获取数据
第3次获取数据
第3次获取数据
显然不是我们想要的数据,加上读写锁,实现锁降级:
public class ReadAndWrite {
private int count;
private ReadWriteLock lock=new ReentrantReadWriteLock();
private Lock readLock=lock.readLock();
private Lock writeLock=lock.writeLock();
public void work(){
try{
writeLock.lock();
count++;
readLock.lock();
}finally {
writeLock.unlock();
}
try {//模拟耗时操作,如从数据库获取数据
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try{
System.out.println("第"+count+"次获取数据");
}finally {
readLock.unlock();
}
}
public static void main(String[] args) {
ReadAndWrite readAndWrite = new ReadAndWrite();
for (int i = 0; i <3 ; i++) {
new Thread(readAndWrite::work).start();
}
}
}
输出结果为:
第1次获取数据
第2次获取数据
第3次获取数据
StampedLock
- JDK1.8前已经有很多锁,为什么还要有StampedLock?
一般的应用都是读多写少的,ReentrantReadWriteLock中读写互斥,因此性能低,可能会使写线程饥饿。
特点
- 不可重入锁,同时支持锁升级和锁降级。
- 三种访问模式:
1 Reading读模式 功能类似ReentrantReadWriteLock的读锁
2 Writing 写模式 功能类似ReentrantReadWriteLock的写锁
3 Optimistic Reading 乐观读 优化的读模式,不阻塞写线程,可能会导致数据不一致问题,因此乐观读时必须对结果进行检验。- 自旋次数有限,增加锁获得的几率,减少上下文切换的开销。
- 不支持Condition等待
- 优点: 相比ReentrantReadWriteLock吞吐量高
- 缺点: 实现复杂,不易使用
原理
- 所有获取锁的方法都返回一个邮戳Stamp,为0表示获取失败,其余表示成功。
- 所有释放锁的方法都需要一个Stamp,这个Stamp必须和成功获取锁时得到的Stamp一致
五 线程间的通信
wait()、notify()、notifyAll()
使用场景
多线程环境下,某个线程依赖于其他线程的状态的改变
注意
- 必须放在同步代码块中
- 必须持有当前对象的锁
- 哪个对象调用了wait,哪个对象调用notify/notifyAll
notify()随机唤醒一个线程,notifyAll()唤醒在该对象上等待的所有线程
跟sleep的区别
- wait会释放锁
- sleep不释放锁,只是在指定时间内不去抢占CPU资源
生产者消费者模型
- 食物类
public class Food {
private int count=0;
synchronized void makeFood(){
while(count>10){//食物数量大于最大库存 等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName()+"制作食物完成");
notifyAll();
}
synchronized void EatFood(){
while(count<1){//食物数量小于最小值 等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println(Thread.currentThread().getName()+"享用食物完成");
System.out.println("-----------------");
notifyAll();
}
}
- 厨师类
public class Cook implements Runnable {
private Food food;
public Cook(Food food){
this.food=food;
}
@Override
public void run() {
while (true){
food.makeFood();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 顾客类
public class Customer implements Runnable {
private Food food;
public Customer(Food food){
this.food=food;
}
@Override
public void run() {
while (true){
food.EatFood();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 测试类
public class Test {
public static void main(String[] args) {
Food food = new Food();
Customer customer = new Customer(food);
Cook cook = new Cook(food);
new Thread(cook,"厨师A").start();
new Thread(cook,"厨师B").start();
new Thread(cook,"厨师C").start();
new Thread(customer,"顾客D").start();
new Thread(customer,"顾客E").start();
}
}
管道通信
- 写线程
class Writer implements Runnable {
private PipedOutputStream pos;
Writer(PipedOutputStream pos) {
this.pos = pos;
}
public void run() {
PrintStream p = null;
try{
p=new PrintStream(pos);
for (int i = 0; i < 3; i++) {
Thread.sleep(250);
p.println(i);
System.out.println("写入:" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("写入完毕");
assert p != null;
p.flush();
p.close();
}
}
}
- 读线程
class Reader implements Runnable {
private PipedInputStream pis;
Reader(PipedInputStream pis) {
this.pis = pis;
}
public void run() {
BufferedReader r = null;
try {
r=new BufferedReader(new InputStreamReader(pis));
String line;
do {
line = r.readLine();
if (line != null)
System.out.println("读取:" + line);
else
System.out.println("读取完毕");
Thread.sleep(500);
} while (line != null);
} catch (Exception ignored) {
}
}
}
- 测试类
public class PipeTest {
public static void main(String[] args) throws IOException {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
new Thread(new Writer(pos)).start();
new Thread(new Reader(pis)).start();
}
}
- 输出
写入:0
写入:1
写入:2
写入完毕
读取:0
读取:1
读取:2
读取完毕
join
使用场景:线程A执行到一半需要另一个线程B先执行完再进行后续操作
public class JoinTest {
public static void main(String[] args) {
Thread thread1=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"线程加入进来");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程执行完毕");
});
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"线程开启");
thread1.start();//启动后才能join
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程结束");
}).start();
}
}
运行结果:
Thread-1线程开启
Thread-0线程加入进来
Thread-0线程执行完毕
Thread-1线程结束
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();//当前时间
long now = 0;
if (millis < 0) {//等待时间<0抛出异常
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {//等于0代表无限制 线程存活进入死循环
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {//如果线程存活
long delay = millis - now;//更新等待时间
if (delay <= 0) {//等待时间结束则break
break;
}
wait(delay);//否则继续等待
now = System.currentTimeMillis() - base;//更新now
}
}
}
ThreadLocal
- ThreadLocal不是一个Thread,而是Thread的局部变量。
- 每个Thread维护一个ThreadLocalMap,其中存储的是一个Entry[] table数组,Entry中key是ThreadLocal,value是ThreadLocal中设置的值
- 以空间换时间,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。
主要方法
- get()
public T get() {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取线程的map
if (map != null) {//如果map不为空
ThreadLocalMap.Entry e = map.getEntry(this);//获取其Entry数组
if (e != null) {//不为空就获取值并返回
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//否则设置并返回初始值
}
- set()
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//获取对应map
if (map != null)//map不为空 更新值
map.set(this, value);
else//map为空 创建并赋值
createMap(t, value);
}
- setInitialValue()
private T setInitialValue() {//设置初始值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
- remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)//如果当前线程map不为空则移除
m.remove(this);
}
该方法是JDK 5.0新增的方法。
当线程结束后,对应该线程的局部变量将自动被垃圾回收,
所以显式调用该方法清除线程的局部变量并不必需,但它可以加快内存回收的速度。
一个实例
public class ThreadLocalTest {
private ThreadLocal<Integer> shared= ThreadLocal.withInitial(() -> 0);
public int getNum(){
int oldNum=shared.get();
shared.set(oldNum+1);
return oldNum;
}
public static void main(String[] args) {
ThreadLocalTest threadLocalTest = new ThreadLocalTest();
for (int i = 0; i <3 ; i++) {
new Thread(()->{
for(int j=0;j<5;j++)
System.out.println(Thread.currentThread().getName()+"->"+"sharedNum:"+threadLocalTest.getNum());
}).start();
}
}
}
输出结果:
Thread-0->sharedNum:0
Thread-1->sharedNum:0
Thread-2->sharedNum:0
Thread-1->sharedNum:1
Thread-0->sharedNum:1
Thread-1->sharedNum:2
Thread-2->sharedNum:1
Thread-1->sharedNum:3
Thread-0->sharedNum:2
Thread-1->sharedNum:4
Thread-2->sharedNum:2
Thread-0->sharedNum:3
Thread-2->sharedNum:3
Thread-0->sharedNum:4
Thread-2->sharedNum:4
虽然各线程同用一个ThreadLocal对象shared,但我们发现其中存储的值并未互相干扰,所有线程各自独立输出了0-4。
- ThreadLocal 与 Synchronized区别
- 相同:ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
- 不同:Synchronized同步机制采用了“以时间换空间”的方式,仅提供一份变量,让不同的线程排队访问;而ThreadLocal采用了“以空间换时间”的方式,每一个线程都提供了一份变量,因此可以同时访问而互不影响。
- 以时间换空间->即加锁,节省了内存,但是会造成很多线程等待现象,因此浪费了时间而节省了空间。
- 以空间换时间->为每一个线程提供一份变量,多开销一些内存,但是线程不用等待,可以一起执行而相互之间没有影响。
- 小结:ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。
在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
Condition
条件对象,当线程进入临界区时,如果需要使其满足某一条件后才可执行,可以使用条件对象来管理那些已经获得一个锁但是不能有效工作的线程。
搭配锁使用,一个锁可拥有多个条件对象。
主要方法
- await() 类似wait(),把线程放入条件的等待集中
- signal() 类似notify(),从等待集中随机解除一个线程的阻塞
- signalAll() 类似notifyAll(),解除等待集中所有线程的阻塞
这里还是之前生成者消费者模型的例子,只需改动Food类
public class Food {
private int count=0;
private Lock lock=new ReentrantLock();
private Condition customerCondition=lock.newCondition();//消费者等待条件对象
private Condition cookCondition=lock.newCondition();//生产者等待条件对象
void makeFood(){
try{
lock.lock();
if(count>10){//食物数量大于最大库存 等待
try {
cookCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName()+"制作食物完成");
customerCondition.signalAll();
}finally {
lock.unlock();
}
}
void EatFood(){
try{
lock.lock();
if(count<1){//食物数量小于最小值 等待
try {
customerCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println(Thread.currentThread().getName()+"享用食物完成");
System.out.println("-----------------");
cookCondition.signalAll();
}finally {
lock.unlock();
}
}
}