第1章 Java线程
系统线程数量上限是多少?
一个线程的栈要预留1M的内存空间 ,而一个进程中可用的内存空间只有2G,所以理论上一个进程中最多可以开2048个线程,但是内存当然不可能完全拿来作线程的栈,所以实际数目要比这个值要小。
可以通过连接时修改默认栈大小,将其改的比较小,这样就可以多开一些线程。 如将默认栈的大小改成512K,这样理论上最多就可以开4096个线程。 即使物理内存再大,线程总数要受到2GB这个内存空间的限制。比方说你的机器装了64GB物理内存,但每个进程的内存空间还是4GB,其中用户态可用的还是2GB。在Windows下写个程序,一个进程Fork出2000个左右线程就会异常退出了,为什么?
这个问题的产生是因为windows32位系统,一个进程所能使用的最大虚拟内存为2G,而一个线程的默认线程栈StackSize为1024K(1M),这样当线程数量逼近2000时,2000*1024K=2G(大约),内存资源就相当于耗尽。
多线程的好处?项目中的实际应用?
- 使用多线程最大的好处是提高系统的资源利用率。
实习项目中:有一个任务场景是消费者去RocketMQ中去消费消息,然后根据获取到的消息去mongodb中拉取流量,并进行流量和规则匹配。由于一个场景对应的流量可能上万条,每个消息可能会有多个场景,所以执行时间比较长。我们这边就用一个线程池去将该任务的请求进行处理。
多线程会带来什么问题?
Java中的线程对应是操作系统级别的线程,线程数量控制不好,频繁的创建、销毁线程和线程间的切换,比较消耗内存和时间。
- 容易带来线程安全问题。如线程的可见性、有序性、原子性问题,会导致程序出现的结果与预期结果不一致。
- 多线程容易造成死锁、活锁、线程饥饿等问题。此类问题往往只能通过手动停止线程、甚至是进程才能解决,影响严重。
什么是线程安全?如何保障线程安全?
线程安全:就是多个线程同时执行代码的结果和单线程执行的结果始终是一致的。线程安全的实现方式:
- 能不能保证操作的原子性,考虑atomic包下的类够不够我们使用。
- 能不能保证操作的可见性,考虑volatile关键字够不够我们使用
- 如果涉及到对线程的控制(比如一次能使用多少个线程,当前线程触发的条件是否依赖其他线程的结果),考虑CountDownLatch/Semaphore等等。
- 如果是集合,考虑java.util.concurrent包下的集合类。
- 如果synchronized无法满足,考虑lock包下的类
谈一下对“守护线程”的理解?
- 守护线程:服务于用户线程,所有的用户线程结束生命周期,守护线程才会结束生命周期,只要有一个用户线程存在,那么守护线程就不会结束。
- 应用场景:垃圾回收线程是典型的守护线程,只有应用程序中所有的线程结束,垃圾回收线程才会结束。
- 守护线程与用户线程的区别:守护线程依赖于创建它的线程,而用户线程则不依赖。举个例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行,直到其运行完毕。
Java实现多线程的几种方式?
1.继承Thread类的run()方法 ```java public class Demo { public static void main(String[] args) { new MyThread().start(); } }
class MyThread extends Thread{ @Override public void run() { synchronized (MyThread.class){ System.out.println(Thread.currentThread().getName() + "进入了同步代码块1"); } } } **2.实现Runable接口**<br />当一个类已经继承其他的类的时候,这时候这个类就不能通过继承Thread类来实现多线程(Java不支持多继承),可以通过实现Runable接口的方式来实现。
java public class Demo { public static void main(String[] args) { new Thread(new MyRunnable()).start(); } }
class MyRunnable implements Runnable{ @Override public void run() { synchronized (MyRunnable.class){ System.out.println(Thread.currentThread().getName() + "进入了同步代码块1"); } } } **3.实现Callable接口并且通过FutureTask包装器来创建Thread进程**
java public class CallableTest { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask task = new FutureTask
(new callable()); Thread thread = new Thread(task); thread.start(); System.out.println(task.get()); } } class callable implements Callable {
@Override
public Integer call() throws Exception {
System.out.println("running....");
Thread.sleep(1000);
return 100;
}
} **如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?**<br />* 1. call()可以返回值的。<br />* 2. call()可以抛出异常,被外面的操作捕获,获取异常的信息<br />* 3. Callable是支持泛型的<br />**4.使用线程池创建,使用线程池接口ExecutorService结合Callable、Future实现有返回结果的多线程。**<br />**好处:提高响应速度、降低资源消耗、便于线程管理。**
java public class Demo { public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue
(20),new ThreadPoolExecutor.AbortPolicy()); executor.setCorePoolSize(5); executor.setMaximumPoolSize(10); executor.setKeepAliveTime(1, TimeUnit.SECONDS); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); executor.execute(new MyRunnable()); executor.shutdown();//关闭连接池,不能接收新的任务,但是可以处理阻塞队列里面的任务 } } class MyRunnable implements Runnable{
@Override
public void run() {
synchronized (MyRunnable.class){
System.out.println(Thread.currentThread().getName() + "进入了同步代码块1");
}
}
Thread和Runnable的区别?
开发中优先选择实现Runnable接口的方式。原因:
- 实现的方式没有类的单继承的限制。
- 实现的方式更适合来处理多线程有共享数据的情况。
- 将线程和任务分离。
相同点:
- 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
- 目前两种方式,要想启动线程,都是调用的Thread类中的start()。
为什么调用start()会执行run(),为什么不能直接调用run()?(start()方法和run()有什么区别?)
通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,等待CPU分配时间片,并没有运行。start()是真正调用底层的代码来创建thread,并从底层调用run()方法,执行我们自定义的任务。run()方法运行结束,此线程终止,CPU再运行其它线程。
如果直接调用线程的run()方法,会将run()方法当成一个主线程中的一个普通方法来执行,程序中依然只有一个主线程,没有其他子线程,所以不能直接调用run()方法来启动线程。
Thread.yield()方法的作用?
它的作用是进程让步,会让出自己的CPU执行权,使当前线程由执行状态变为就绪状态。自己和其他线程重新竞争CPU执行权。此线程可能再次被执行,也有可能优先级更低的进程获得CPU执行权,也有可能是优先级更高的进程获得。例子:有若干个人在排队等待上车,轮到A的时候,A说我现在不想上车,和后面的人说咱们一起跑步看谁快谁上去吧?(这样就有可能导致,A自己上车,也有可能导致在A后面的人上车)
sleep()和yield()的区别?
| | Thread.sleep(1000) | Thread.yield() | | --- | --- | --- | | 执行后的状态 | 线程执行 sleep() 方法后进入阻塞状态,暂时不会获得CPU的使用权 | 线程执行 yield() 方法转入就绪状态,可能会立即获得CPU的使用权 | | 是否需要指定参数 | 需要指定时间参数 | 没有任何参数,直接调用即可 | | 优先级 | 给其他线程运行机会时不考虑线程的优先级 | 让出CPU的使用权,有可能立即会再次得到CPU使用权,也可能不会得到 | | 是否抛出异常 | 需要抛出异常InterruptedException | 不需要抛出任何异常 |
sleep()和wait()的区别?
|
| Thread.sleep(100) | wait() | | --- | --- | --- | | 作用相同 | 都可以让线程进入阻塞状态 | | | 声明位置不同 | Thread类的静态方法 | Object类的普通方法 | | 调用要求不同 | 可以在任何地方使用 | 只能在同步方法或同步代码块中使用(只能配合sychronized来使用) | | 是否释放同步监视器 | 执行方法后不会释放同步监视器 | 执行方法后会释放同步监视器 | | 是否自动苏醒 | 当执行完时间后会自动苏醒 | 需要使用notify()或notifyAll()来唤醒当前进程 |
notify()和notifyAll()有什么区别?
|
| notify() | notifyAll() | | --- | --- | --- | | 是否会出现死锁 | 不会出现死锁 | 可能出现死锁 | | 唤醒对象 | 唤醒等待池中指定的线程去锁池,和锁池中的所有线程一起竞争拿到监视器锁 | 唤醒等待池中的所有线程去锁池,和锁池中的所有线程一起竞争拿到监视器锁 |
拓展(Synconized的两种数据结构):
- 等待池(WaitSet):假设线程B调用的对象A的wait()方法,那么B线程就要进入对象A的等待池。
- 锁池(EntryList):假设对象A已经被线程B锁拥有,那么想得到对象A锁的那些进程需要进入锁池,通过竞争得到对象A的锁。
为什么wait()和notify()只能在同步方法或同步代码块中调用?
调用wait()就是释放锁,释放锁的前提是必须要先获得锁,那么只有在同步代码块中才能获得锁。
notify()和notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢?(本质是让处于入口队列的线程竞争锁)
- wait()作用:执行此方法当前线程会进入阻塞状态,并释放同步监视器(锁),将当前线程放到等待池中。
- notify()作用:执行此方法会唤醒被wait的那个线程,如果有多个线程被wait,那么会唤醒优先级最高的那个线程。唤醒线程进入锁池,和锁池中的所有堵塞的线程一起竞争对象锁的使用权。
为什么wait、notify和notifyAll这些方法不在Thread类里面?
Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。
简单的说,由于wait、notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中,因为锁属于对象。如果wait, notify 和 notifyAll 这些方法在 thread 类里面会有什么问题?
wait()仍然可以使当前线程挂起,但问题是挂起后怎么被其他线程唤醒(唤醒线程时需要知道要唤醒那个线程,参考:LockSupport.unpark(thread)),可以通过共享变量暴露线程但是却存在安全隐患。
notify和notifyall同样需要知道需要唤醒那些线程。这样会使线程间的通信复杂化而且存在安全隐患
如何优雅地停止一个正在运行的线程?
~~说到中断线程,我们java中提供了一个stop方法,不过已经不建议使用了,因为stop方法一剑封喉,线程来不及料理后事。~~所谓优雅是给线程处理后事的机会:释放获得的锁以及其他资源。使用interrupt()方法中断进程【两阶段终止模式】:该方法给受堵塞的线程发出一个中断信号,打断标志设置为true。当前线程可以利用isInterrupted()方法判断是否被打断,如果为true,那么线程可以判断是否要终止运行,在终止之前可以释放持有的共享资源等操作(料理后事)。但是打断正在执行sleep()、wait()、join()的这种线程,会将打断标志重置为false。【两阶段:当在执行sleep()时用interrupt()来打断线程,在捕获异常的块中将打断标志置为true】 ```java public class Tests { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new MyTask()); TwoPhaseTermination tpt = new TwoPhaseTermination(thread); tpt.start(); Thread.sleep(3500); tpt.stop(); } }
class TwoPhaseTermination{ private Thread monitor;
public TwoPhaseTermination(Thread thread){
this.monitor = thread;
}
public void start(){
monitor.start();
}
public void stop(){
monitor.interrupt();
}
}
class MyTask implements Runnable{ @Override public void run() { while (true){ Thread current = Thread.currentThread(); if (current.isInterrupted()){ System.out.println("处理后事"); break; } try { Thread.sleep(1000); System.out.println("执行监控任务"); } catch (InterruptedException e) { e.printStackTrace(); //sleep出现异常后,会重置打断标记 //需要重置打断标记 current.interrupt(); } } } } ``` sleep被打断后会出现异常,会清除打断标记,会返回默认的false。所以需要再调用interrupt方法重置打断标记,返回true。
isInterrupted() 判断是否被打断, 不会清除打断标记。
Thread类中interrupt、interrupted和isInterrupted方法的区别?
- interrupt是给线程设置中断标志;
- interrupted是检测中断并清除中断状态;
- isInterrupted只检测中断。
还有重要的一点就是interrupted()作用于当前线程,interrupt()和isInterrupted()作用于此线程,即代码中调用此方法的实例所代表的线程。
Sleep(0)的作用?
Thread.sleep(0)并非是真的要线程挂起0毫秒,意义在于这次调用Thread.Sleep(0)的当前线程确实的被冻结了一下,让其他线程有机会优先执行。Thread.Sleep(0) 是你的线程暂时放弃CPU,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作。
第2章 Java锁(synchronized、CAS和ReentrantLock)
Java中都有什么锁?
- 乐观锁:它是一种乐观思想,在拿数据的时候相信别的线程不会对数据进行修改,所有不会加锁。但是在读的时候要比较一下数据+版本号是否相同,一般通过CAS来实现乐观锁。适合读多写少的场景。
- 悲观锁:它是一种悲观思想,在拿数据的时候认为别的进程会对数据进行修改,所以在每次读写数据的时候都要进行加锁。一般通过Sychronized来实现悲观锁,适合写多读少的场景。
- 公平锁:等待时间越长的线程越先得到对象的锁。ReentrantLock提供一个参数来决定是否是公平锁,默认是非公平锁。
- 非公平锁:不根据等待时间的长短来分配对象的锁(随机分配)。Sychronized是非公平锁。
- 自旋锁:当一个线程在获取锁的时候,对象的锁被其他线程已经占用,那么该线程将循环等待(自己在原地等一下),然后不断地判断能否成功获取对象的锁。虽然避免了线程切换的开销,但是增加了占用CPU的时间开销。
- 可重入锁:当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。可重入锁的意义在于防止死锁。synchronized和ReentrantLock都是可重入锁。但是两种情况不同,ReentrantLock需要手动释放lock.unlock(),并且加锁次数和释放次数要相同,否则会发生死锁。synchronized通过为每个锁关联一个请求计数和一个拥有资源的线程Owner。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1。如果同一个线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
Java内存模型?
Java内存模型的概念?
JMM 即 Java Memory Model ,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。它从Java层面定义了 主存、工作内存的抽象概念,底层对应着CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下:
- 主内存:所有的共享变量都存储于主内存,所有线程都可访问内存。
- 工作内存:每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的读取操作都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。
Java内存模型的作用?
Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性、和原子性的规则和保障。
CPU缓存、内存和Java内存模型的关系?
并发编程的三个问题?如何解决?实现原理?
原子性问题
概念:指在一次操作或多次操作中,要么所有的操作全部得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。代码演示: java /** * 1.定义一个共享变量number * 2.对number进行1000次的++操作 * 3.使用5个线程来进行 */ public class AtomicityTest { private static int number = 0; public static void main(String[] args) throws InterruptedException{ List<Thread> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Task()); thread.start(); list.add(thread); } //让主线程等待子线程全部执行完 for (Thread thread : list) { thread.join(); } System.out.println(number); } static class Task implements Runnable{ @Override public void run() { for (int i = 0; i < 1000; i++) { number++; } } } }
为什么会出现原子性问题?
通过对上述代码反编译得知,number++在JVM底层是分四条指令来组成。
如果在单线程下面,本程序是没有问题的,但是在多线程环境下就会出现原子性问题。
假设有2个线程都去执行number++来分析此问题:
线程A执行到第9行->第13行,此时number值变成1,还没有写回到number静态变量,但是此时CPU将此线程挂起,去执行线程B,线程B也是和线程A做同样的操作,由于线程A还没有将结果写回静态变量number,那么此时number=0,线程B执行到第13行时,也是将number变成1,并且执行到第14行将number=1写回静态变量。此时CPU时间片再分给线程A,线程A继续执行还没有执行的第14行,将number=1写回静态变量。
通过模拟上面两个线程同时执行number++发现,线程A和B都执行了一次number++操作,但是最后number=1。总结:出现原子性问题是由于线程切换导致的,number++分成4条指令来执行,如果一个线程还没有执行完4条指令,其他线程又来执行,就会将number的值进行覆盖。
如何解决原子性问题?实现原理是什么?
对需要保证原子性的代码块加上synchronized关键字来修饰 java public class AtomicityTest { private static int number = 0; static Object object = new Object(); public static void main(String[] args) throws InterruptedException{ List<Thread> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(new Task()); thread.start(); list.add(thread); } //让主线程等待子线程全部执行完 for (Thread thread : list) { thread.join(); } System.out.println(number); } static class Task implements Runnable{ @Override public void run() { for (int i = 0; i < 1000; i++) { synchronized (object){ number++; } } } } }
反编译结果:两个线程模拟:线程A拿到锁后,先拿到静态变量的值(假设为0),然后让常量1和静态变量的值相加(20行),此时CPU使用权交给线程B,线程B走到14行发现锁已经被其他线程锁占用,这时候就会阻塞等待锁的使用权,最后CPU的使用权切回线程A,线程A继续执行接下来的操作。线程A将静态变量的值写回1后释放锁。整个过程中,对于静态变量number来说是能够保证原子性的,因为执行number++的过程没有其他线程来打扰线程A的执行。原理:对number++,增加同步代码块后,保证同一时间只有一个线程操作number++,所以不会出现线程安全问题。
可见性问题
概念:一个线程对共享变量的修改,另外一个线程能够立刻看到。代码演示: java public class VisibilityTest { private static boolean flag = true; public static void main(String[] args) { new Thread(()->{ while (flag){ } }).start(); //主线程休眠2s再执行下面的线程目的是让flag变量变成热点数据 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ flag = false; System.out.println("flag的值为:false" ); }.start(); } }
执行结果分析:2s后输出“flag的值为:false”,但是程序依旧在执行,如果能够保证可见性的话,flag被修改为false,第一个线程应该执行结束。所以,以上程序不满足可见性。
为什么会出现变量的不可见性?
每个线程都有一个私有的工作内存,工作内存对其他线程是不可见的。JMM规定所有的变量都存在主内存中,主内存对所有线程是共享的。某个线程对变量进行修改时,首先将主内存中的变量拷贝到自己的工作内存,修改完毕后再写回主内存。上述例子分析:有一个静态的布尔类型变量flag=true,子线程利用这个布尔变量进行循环操作。但是主线程休眠2s后,将布尔变量修改为false。理想状态应该是子线程立即感受到flag值变为false直接停止循环,但是现实是没有停止循环。那么上述情景就是变量的不可见性。原因:主线程在子线程运行1s后才修改变量,在这1s时间内flag的值一直都是true(变成热点数据),JIT及时编译器会对子线程执行的变量进行缓存,缓存到子线程的工作内存,这样子线程就不会去主内存中访问flag变量了。当主线程对主内存中的flag变量的值进行修改后,子线程读取的还是自己工作内存中的变量值。
解决不可见性的底层实现原理?
JVM线程工作时的原子性指令有:
- read: 从主存读取一个变量的值的副本到线程的工作内存。
- load:把read来的值赋给工作空间的变量中,然后就可以使用了。
- use:要使用一个变量,先发出这个指令。
- assign:赋值,给变量一个新值。
- store:将工作空间的变量值运送到主存中。
- write:将值写到主存的那个变量中。
- volatile实现可见性的原理:
使用内存屏障可以禁止CPU指令乱序执行:插入一个内存屏障, 相当于告诉CPU和编译器先于这个指令的必须先执行,后于这个命令的必须后执行。
读操作时在读指令use之前插入一条读屏障指令,重新从主存加载最新值进来,保证了load、use指令的执行顺序不乱,即保证使用变量前一定刚刚进行了load操作。
写操作时在写指令assign之后插入一条写屏障指令,将工作内存变量的最新值立刻写入主存变量。
- synchronized实现可见性原理:
线程在加锁时,先清空工作内存 → 从主内存中拷贝最新变量的副本到工作内存 → 执行完代码 → 将更改后的共享变量的值刷新到主内存中 → 释放互斥锁。
由于在带锁期间,没有其他线程能访问本线程正在使用的共享变量,这样就保证了可见性。
有序性问题
概念:有序性是指程序中代码的执行顺序和编写代码的顺序相同。但Java在编译时和运行时会对代码进行优化(代码重排序),重排序能够提高程序的执行效率,但是两者的顺序就不一定一致了。
对as-if-serial语义理解?
不管编译器和CPU如何重排序,必须保证在单线程环境下程序的执行结果是正确的。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
a和c之间存在数据依赖关系,同时b和c之间也存在数据依赖关系。因此在最终执行的指令序列中,c不能被重排序到a和b的前面(c排到a和b的前面,程序的结果将会被改变)。但a和b之间没有数据依赖关系,编译器和处理器可以重排序a和b之间的执行顺序。as if-serial语义使单线程下无需担心重排序的干扰,也无需担心内存可见性问题。
如何解决有序性问题?实现原理?
- 在变量前面用volatile来修饰
使用内存屏障来禁止指令重排,从而实现变量的有序性。
- LoadLoad Barriers:在两个读指令之间插入一个“LoadLoad”的内存屏障,确保Load1的数据装载,先于Load2的数据装载。
- StoreStore Barriers:在两个写指令之间插入一个“StoreStore”的内存屏障。确保Store1的数据先刷新到主内存,并且对其数据可见。Store1的写数据先于Store2的写数据。
- LoadStore Barriers:在读和写指令之间加一个“LoadStore”屏障,确保Load1的数据装载先于Store2的写数据。
- StoreLoad Barriers:在写和读之间加一个“StoreLoad”屏障,确保Store1的数据写入并且刷新到内存先于Load2。
- 在变量前面加上synchronized来修饰
重排序还是会发生,但是因为对代码块加锁后,可以保证只有以一个线程在同步代码块内执行代码,所以能够保证有序性。
因为重排序是必须要保证在单线程环境下的执行结果是正确的,所以加了synchronized后,相当于代码块是在单线程环境下执行
谈一谈对CAS的理解?
CAS的思想?
CAS,Compare-and-Swap,比较并替换,是一条CPU并发原语。因为原语的执行必须是连续的,执行过程不可中断的,所以CAS是一条CPU的原子指令,不会造成数据不一致性,整个比较和替换过程是一个原子操作。
它有3个操作数:内存地址V、旧的预期值A,将要更新的目标值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。否则也不会阻塞当前线程,而是将内存最新值赋值给旧的预期值,然后一直重试,直到内存最新值和旧的预估值一样为止。
CAS的底层原理?(CAS思想+unsafe类)
CAS的实现主要在JUC中的atomic包,里面有AtomicInteger、AtomicBoolean,AtomicLong,AtomicReference。底层原理:自旋+ Unsafe类+volatile(保证变量的可见性)
- state的变量一般都是用volatile修饰的,保证变量的可见性;
- CAS操作都是通过sun包下Unsafe类去修改state的变量;如果修改失败会一直自旋等待;
- Unsafe类中的方法都是native方法,由JVM本地实现,所以最终的实现是基于C、C++在操作系统之上操作;
Unsafe对象不能直接调用,只能通过反射获得。 ```java public class MyLock { private volatile int state; private static Unsafe unsafe; private static long stateOffset; ReentrantLock lock = new ReentrantLock(); static { try { // 使用反射获取Unsafe的成员变量theUnsafe Field field = Unsafe.class.getDeclaredField("theUnsafe"); // 设置为可存取 field.setAccessible(true); unsafe = (Unsafe) field.get(null); stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField("state")); } catch (Exception e) { e.printStackTrace(); } }
public void lock(){ lock.lock(); //CAS将0改成1 while (!unsafe.compareAndSwapInt(this, stateOffset, 0, 1)){ System.out.println(Thread.currentThread().getName() + "被阻塞"); } //执行到此,表示获取到锁 System.out.println(Thread.currentThread().getName() + "成功获取锁"); state = 1; }
public void unlock(){ state = 0; } } ```
CAS的缺点?什么是ABA的问题?
CAS缺点:CAS失败会进行自旋,对CPU的消耗比较大;还会出现ABA问题。ABA问题:线程1从内存位置V中取出A,线程2同时取出内存位置V中的A,线程2经过一些操作将数值变成B,然后线程3又将内存位置V的值变成A,这时候线程1进行CAS操作发现内存位置V的数值仍然是A,操作成功。注意:虽然操作成功,但是是不正确的,此时的A已经不是原来的A了。
如何解决ABA问题?
每次变量改变的时候,把变量的版本号加1,更新的时候再比较一下版本号。例子:线程1从内存位置V拿到数据A的同时获得数据A的版本号I,线程2同时拿到数据A的版本号I,线程2对数据操作后变成数据B,同时版本号变为II,线程3又将内存位置V的值变成A,版本号变成III。此时线程2比较预期的版本号与内存V的版本号不一致,导致CAS操作不成功。
乐观锁和悲观锁的区别?
乐观锁:
乐观锁是一种乐观思想,即认为读多写少,认为遇到并发写的可能性低。Java中的乐观锁基本都是通过CAS操作实现的。每次去拿数据的时候都认为别人不会修改,所以不会上锁。但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,比较期待值和修改值是否一致,如果一致进行修改(说明期间没有其他线程修改数据)。如果期待值和修改值不同,也不会阻塞当前线程,而是循环尝试(自旋),直到为止,这样就能避免由于阻塞线程带来的上下文切换代价,但是需要CPU的支持。
CAS获取共享变量时,为了保证变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于竞争不太激烈、多核CPU的场景下。悲观锁:
悲观锁就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞,直到拿到锁。那么在线程阻塞的时候会引起线程上下文切换。当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
Java中的悲观锁就是synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁【ReentrantLock】
CAS和synchronized的使用场景是什么?
- 对于资源竞争较少(线程冲突较轻)的情况:使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS借助C来调用CPU底层指令实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
- 对于资源竞争严重(线程冲突严重)的情况:CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
synchronized关键字的底层原理?【对synchronized的理解】
synchronized的特性和概念?
- synchronized是一个互斥锁,一次只能允许一个线程进入被锁住的代码块;
- synchronized是一个非公平锁
- synchronized是一个可重入锁
- synchronized是一个Java关键字,它能够将代码块/方法锁起来
通过反编译可以发现,当修饰方法时,编译器会生成 ACC_SYNCHRONIZED *关键字用来标识;当修饰代码块时,会依赖monitorenter和monitorexit指令;
无论synchronized修饰的是方法还是代码块,对应的锁都是一个实例(对象);
在内存中,对象一般由三部分组成,分别是对象头、对象实际数据和对齐填充;对象头又由几部分组成,对象头Mark Word记录对象关于锁的信息。
又因为每个对象都会有一个与之对应的monitor对象,monitor对象中存储着WaitSet、EntryList和Owner(存储拥有此monitor对象的线程)
![image.png](https://i-blog.csdnimg.cn/blog_migrate/374f6c0a0a91ed5255f96e4b1197588d.png)
当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:
- 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1,当前线程成为monitor的owner (所有者)。
- 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数会加1。
- 若其他线程已经占有monitor的所有权,那么当前尝试获取monltor的所有权的线程会被阻塞加入到monitor对象的阻塞队列中,直到monltor的进入数变为0,才会唤醒monitor中阻塞队列中的线程。
释放锁的过程:
当JVM执行某个线程的某个方法内部的monitorexit时,它会尝试去释放当前对象对应的monitor的所有权。其过程如下:
- 执行moiniorexit指令会将monitor的进入数减1,当进入数减到0时,说明当前线程退出monitor,不再拥有monitor的所有权
- monitor中的owner变量设置为null > monitorexit指令会出现在同步代码结束处和异常处,即使同步代码块出现异常,也能够释放锁。
synchronized的加锁是依赖底层操作系统的 mutex 相关指令实现,所以会有用户态和内核态之间的切换,性能损耗十分明显。
synchronized如何实现可重入锁?
synchronized锁对象中有一个计数器会记录线程获得锁的次数,如果锁对象没有被线程所拥有,那么计数器为0。
线程A获取synchronized锁的时候,首先检查锁的状态是否为0,如果锁状态为0,线程A直接获取锁,并且将锁的Owner值设置为线程A。如果锁状态不是0,那么检查当前线程是否和Owner中的线程一致,如果两者一致锁状态+1,否则获取锁失败,线程进入阻塞状态。
线程释放synchronized锁的时候,如果释放锁的线程和Owner中的线程一致,每释放一次,锁的状态-1,直到锁的状态为0时,将Owner中的线程置为null,释放锁成功。
synchronized如何实现非公平锁?
- 偏向锁很好理解,如果当前线程ID与markword存储的不相等,则CAS尝试更换线程ID,CAS成功就获取得到锁了,CAS失败则升级为轻量级锁。
- 轻量级锁实际上也是通过CAS来抢占锁资源(只不过多了拷贝Mark Word到Lock Record的过程),抢占成功到锁就归属给该线程了,但自旋失败一定次数后升级重量级锁。
- 重量级锁通过monitor对象中的队列存储线程,但线程进入队列前,还是会先尝试获取得到锁,如果能获取不到才进入线程等待队列中
综上所述,synchronized无论处理哪种锁,都是先尝试获取,获取不到才升级|| 放到队列上的,所以是非公平的。 > 公平和非公平的区别就是:线程执行同步代码块时,是否会去尝试获取锁
synchronized在JDK1.6中的优化?
sychronized在JDK1.6以后不是刚开始就是用重量级锁(悲观锁),而是采用锁升级策略。锁一共有四种状态:无锁状态、偏向锁、轻量级锁、重量级锁。锁都是可以升级的,锁升级的方向是单向的,只能升级不能降级。
还引入自旋锁、适应性自旋锁、锁消除、锁粗化等技术进行优化。
对象头布局
偏向锁
官方经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。为了让线程获得锁的代价更低,引进了偏向锁。
偏向锁:会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可,实现JVM层面的加锁逻辑。
获取偏向锁的流程:
- 如果当前为可偏向状态(Mark Word中偏向锁的标识为0,锁标志位为01),使用CAS操作将Mark Word中前54位修改成当前线程的ID。
- 如果 CAS 操作成功,则认为已经获取到该对象的偏向锁, 执行同步块代码。
- 如果 CAS 操作失败,则说明有竞争环境,此时会对偏向锁撤销,升级为轻量级锁。
- 如果是已偏向状态(Mark Word中偏向锁的标识为1,锁标志位为01),则检测MarkWord中存储的thread ID 是否等于当前 thread ID。
轻量级锁是JDK1.6加入的新型锁机制,它名字中的"轻量级”是相对于使用monitor的传统锁的"重量级"而言。当关闭了偏向锁或者出现多个线程竞争偏向锁这两种情况,则会尝试获取轻量级锁。引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级为重量级锁,所以轻量级锁的出现并非是要替代重量级锁。轻量级锁的获取过程?
- 首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象的Mark Word的拷贝(hashCode、分代年龄和锁状态),Lock Record中的Owner指向当前锁对象。
- 利用CAS操作尝试将锁对象的Mark Word更新为Lock Record的指针。
- 如果CAS成功,说明竞争得到锁,并且将锁标志改为00,执行同步操作。
- 如果CAS失败,再判断当前对象的Mark Word是否指向当前线程的栈帧
- 如果是,则表示当前线程已经拥有了当前对象的锁,那么在线程内部再添加一条Lock Record作为重入的计数,然后执行同步操作。
- 否则,说明该所对象已经被其他线程抢占了。首先会进行自旋锁,自旋一定次数后,如果还是失败就膨胀为重量级锁,然后锁标志位变为10,后面等待的线程需要进入阻塞状态。
轻量级锁的释放过程?
在当前线程的栈帧中取出Lock Record中存储的数据,用CAS操作将锁对象中Mark Word替换为取出来的数据。
如果获取重量级锁的时候,发现已经被其他线程所占有,那么就会阻塞线程,CPU需要从用户态切换到内核态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给并发性也带来了很大的压力。官方注意到在很多应用上,共享数据的锁定状态只会持续很短的一段时间,如果为了这段时间去阻塞和唤醒线程并不值得。
我们可以让没有获取到锁的线程,先等一等,不放弃处理器的执行时间,不要去阻塞,看看持有锁的线程是否很快就会释放锁。以上这个过程就是“自旋”。自旋锁的优点:避免了线程切换的开销。自旋锁的缺点:增加了占用处理器的时间开销。
自旋次数太少,有可能线程一会就释放锁了,那么会导致线程切换;自旋次数太多,有可能线程很长时间还不释放锁,那么就会白白浪费CPU的性能。所以自旋的次数靠人工很难去确定。所以就出现了适应性自旋锁。
适应性自旋锁
在JDK6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了。而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而将下次自旋的时间变长,比如100次循环。另外,如果对于某个锁,通过自旋很少成功获得过锁,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费CPU资源。
有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。
覆盖率平台中的回调接口中的回调时间间隔就是借鉴这种思想!
锁消除
锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行(同步锁就可以消除了)。
变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定。但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?实际上有许多同步措施并不是程序员自己加入的,有些容器或者工具底层就是有同步锁的。
下面这段非常简单的代码仅仅是输出3个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。 java public class Test { public static void main(String[] args) { thread.start(); String s = contractString("aa", "bb", "cc"); } public static String contractString(String a, String b, String c){ return new StringBuffer().append(a).append(b).append(c).toString(); } }
锁粗化
JVM探测到一连串细小的操作都是用同一个对象的锁,那就会将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。 java public class LockTest { public static void main(String[] args) { StringBuffer stringBuffer = new StringBuffer(); for (int i = 0; i < 100; i++) { stringBuffer.append("aa"); } } } //StringBuffer源码 public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
以上代码,会反复拿到锁100次,释放100次锁。这样效率会降低很多。这时候JVM检测到这种情况后,会将代码变成下面形式,只需要拿到一次锁,然后进行100次循环即可。 ```java public class LockTest { public static void main(String[] args) { StringBuffer stringBuffer = new StringBuffer(); synchronized(this){ for (int i = 0; i < 100; i++) { stringBuffer.append("aa"); } } }
public StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
synchronized使用时如何优化?
- 减少同步代码块的长度
同步代码块中尽量短,减少同步代码块的执行时间,这样有可能不需要膨胀到重量级锁,轻量级锁即可解决。执行时间短,通过自旋来获取锁的可能性增大,也不需要膨胀到重量级锁。
- 降低锁的粒度,提高并发性能
将一个锁拆分成若干个锁来执行,最经典的案例就是Hashable和ConcurrentHashMap,这两个都是线程安全的Map,但是两者的效率截然不同,其原因就是锁的粒度不同。Hashtable:锁整个哈希表,一个操作正在执行,其他操作也需要等待。在增删改的方法前面都加了synchronized修饰,导致一个线程在执行修改的时候,另一个线程想查询也需要等待。而且会将哈希表中所有的桶都会上锁,线程A对Entry0执行操作,如果线程B想对Entry1执行操作也需要阻塞等待线程A执行完毕。
锁整个哈希表和对所有操作方法加锁,会导致并发性很差。ConcurrentHashMap:局部锁定,锁定是以桶为单位。
上面的情况,会导致添加的时候不能取数据,取数据的时候不能添加数据,并发度比较低。
LinkedBlockingQueue将添加数据和取数据的锁分成两个,这样就降低了锁的粒度,添加数据和取数据能够同时进行,提高了并发性。
- 读写分离
读取时不加锁,但是写入和删除的时候加锁。
ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteSet
谈一谈对AQS的理解?
AQS的实现原理?(为什么AQS的底层是CAS+volatile?)
- AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器。我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
独占式锁:ReentrantLock;共享锁:Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLoc;都是基于AQS来实现的。
- AQS使用一个int成员变量state来表示同步状态,state为0表示锁没有被占用, state大于0表示当前已经有线程持有该锁,这里之所以说大于0而不说等于1是因为可能存在可重入的情况,使用CAS对该同步状态进行原子操作实现对其值的修改。private volatile int state; //共享变量,使用volatile修饰保证线程可见性
- AQS通过内置的FIFO队列来完成获取资源线程的排队工作,类似于Monitor的EntryList。
- AQS使用条件变量来实现等待、唤醒机制,支持多个条件变量,单个条件变量类似Monitor的WaitSet
- AQS有两种资源共享方式:独占式、共享式;
AQS使用什么设计模式?如何实现自定义同步器?
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
- isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。为什么这几个方法不声明为抽象方法?
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。如果声明为抽象方法,那么需要实现所有抽象方法,不太方便开发者。 ```java class Mutex implements Lock, java.io.Serializable { // 自定义同步器 private static class Sync extends AbstractQueuedSynchronizer { // 判断是否锁定状态 protected boolean isHeldExclusively() { return getState() == 1; }
// 尝试获取资源,立即返回。成功则返回true,否则false。
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 这里限定只能为1个量
//使用CAS将state设置为1(只有当state为0时),不可重入!
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
return true;
}
return false;
}
// 尝试释放资源,立即返回。成功则为true,否则false。
protected boolean tryRelease(int releases) {
assert releases == 1; // 限定为1个量
if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);//释放资源,放弃占有状态
return true;
}
}
// 真正同步类的实现都依赖继承于AQS的自定义同步器!
private final Sync sync = new Sync();
//lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
public void lock() {
sync.acquire(1);
}
//tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
public boolean tryLock() {
return sync.tryAcquire(1);
}
//unlock<-->release两者语义一样:释放资源。
public void unlock() {
sync.release(1);
}
//锁是否占有状态
public boolean isLocked() {
return sync.isHeldExclusively();
}
AQS是如何保证线程安全的?(AQS加锁逻辑?)
AQS加锁的时候,使用CAS将state从0修改为1,这个过程是原子性的。如果修改成功,将锁的持有线程设置为本线程。
AQS释放锁的时候,不需要使用CAS修改state的值,因为没有其他线程与它竞争,只需要使用set方法来设置即可,并且在此之前先将锁的持有线程设置为null,能够保证线程的可见性。 java // 尝试获取资源,立即返回。成功则返回true,否则false。 public boolean tryAcquire(int acquires) { assert acquires == 1; // 这里限定只能为1个量 //使用CAS将state设置为1(只有当state为0时),不可重入! if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源 return true; } return false; }
谈一谈对ReentrantLock的理解?【ReentrantLock的底层】
ReentrantLock锁的特性?
- 可重入性:一个线程获得锁后还可以再次获得锁,并且不会阻塞线程。
- 支持可中断:ReentrantLock锁提供了一个tryLock(timeout, TimeUnit)方法,此方法可以指定在有限的时间内获得锁,如果在这个时间段内获取不到锁就返回false。
- 锁超时:tryLock方法可以设置指定等待时间,参数为:tryLock(long timeout,TimeUnit unit) , 其中timeout为最长等待时间,TimeUnit为时间单位。使用这种方法可以防止无限等待,减少死锁。
- 支持公平锁和非公平锁,默认使用非公平锁。
支持多个条件变量:Sychronized中也有条件变量,其实就是监视器中的waitSet等待集合(当条件不满足的时候进入waitSet等待);ReentantLock支持多个条件变量,就是它可以有多个waitSet休息室,可以有专门等烟、等早餐的休息室,那么唤醒的时候只需要唤醒需要的休息室就可以(可以避免虚假唤醒)
ReentrantLock是一种可重入锁,基于AQS实现了一个Sync抽象类,该抽象类被NonfairSync和FairSync继承,无参构造方法是使用非公平锁,但是带参数的构造方法可以选择是否为公平锁。
- 存储结构
waitStatus=-1 说明当前Node后面有节点需要唤醒
非公平模式下释放锁
- 外界调用unlock方法时,实际上会调用AQS的release方法,release方法又会调用子类的tryRelease方法
- tryRelease方法会把state一直减(可能存在可重入情况),直到0;
- 从队列的队尾往前找节点状态<0 并离头节点最近的节点进行唤醒;
- 唤醒后,被唤醒的线程则尝试使用CAS获取锁
ReetrantLock如何实现可重入锁?
如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功。
线程释放ReentrantLock锁的时候,如果释放锁的线程和Owner中的线程一致,每释放一次,锁的状态-1,直到锁的状态为0时,将Owner中的线程置为null,释放锁成功。
ReetrantLock如何实现非公平锁和公平锁?
公平锁的实现原理
java //公平锁模式下,线程获取锁过程 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //如果锁的状态为0,还要去判断一下AQS队列中是否有前驱结点。 //如果有前驱结点,则表示有线程比当前线程在更早就请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。 //如果没有前驱结点,才使用CAS去竞争锁。 if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //如果锁的状态不是0,说明已经被线程所获得,那么判断一下是不是自己已经持有这个锁(锁重入) else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } //判断当前节点是否还有前驱节点 public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
非公平锁的实现原理
获取锁的时候只要判断到锁的状态为0,就可以直接占有锁(利用CAS将锁的状态置为1),不会去判断AQS队列中阻塞线程。 java final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //如果还没有获得锁 if (c == 0) { //直接使用CAS尝试获得锁,这里体现了非公平性,不去检查AQS队列 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //如果锁的状态不是0,说明已经被线程所获得,那么判断一下是不是自己已经持有这个锁(锁重入) else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
是否为公平锁的依据是:当state=0时,是否直接CAS获取锁,还是先去双向链表看一下是否有Node在排队,再去CAS获取锁。
为什么有synchronized了还用Lock?
死锁发生的四个条件:互斥条件、循环等待条件、请求和保持条件、不可剥夺条件。
只要破坏以上其中一个条件就不会发生死锁,然而Synchronized无法破坏不可抢占条件,因为Synchronized申请资源的时候,如果申请不到,线程就直接进入阻塞状态,进入阻塞状态就啥事都干不了了,所以也释放不了已经占有的资源。
Lock有3种方法可以破环不可抢占的条件来避免发生死锁。
- void lockInterruptibly();
这是个支持中断的API。Synchronized进入阻塞之后就没办法唤醒它,所以针对这个问题想了个支持响应中断的方法,让线程阻塞(lock下是等待状态)的时候可以响应中断信号,从而有机会释放已占有的资源来破环不可抢占的条件。
- boolean tryLock();
这就是在获取锁的时候,如果获取不到就直接返回,这样也有机会释放已占有的资源来破坏不可抢占的条件。
- boolean tryLock(long time,TimeUnit unit);
这是个支持超时的API,也就是让在一段时间内获取不到资源的线程直接返回一个错误,不进入阻塞状态,那也有机会释放已占有的资源来破坏不可抢占的条件。
再介绍一下ReentrantLock锁
synchronized和ReentrantLock的区别?
| | synchronized | ReentrantLock | | --- | --- | --- | | 实现层面不一样 | synchronized是 Java 关键字,JVM层面实现加锁和释放锁 | ReentrantLock是实现Lock 接口的实现类,在代码层面实现加锁和释放锁 | | 获取锁成功是否可知 | 无法得知是否获取锁成功 | 可以通过 tryLock 获得锁是否成功,如果成功返回true,否则返回false | | 是否自动释放锁 | synchronized会自动释放锁
(线程执行完同步代码会释放锁;线程执行过程中发生异常会释放锁) | 不会自动释放锁,需要在 finally {} 代码块显式地中释放锁 | | 是否可中断 | 不可中断(如果当前线程1获得锁,线程2再想获得锁就会阻塞。) | 可中断(tryLock(long time, TimeUnit unit):如果在规定时间内获取不到锁,就返回获取失败)、可不中断 | | 是否可以锁代码块和方法 | 可以锁方法和代码块 | 只能锁代码块,不能锁方法 | | 是否是公平锁 | 非公平锁 | 可以控制是否为公平锁 |
synchronized与volatile的区别?
| * * | synchronized | volatile | | --- | --- | --- | | 作用范围 | 作用于变量、方法、代码快 | 作用于变量 | | 功能及其实现原理 | 可以保证可见性、原子性和有序性 | 可以保证数据的可见性、有序性,但是不能保证原子性 | | 是否阻塞 | 线程阻塞 | 线程不阻塞 | | 应用场景 | 需要保证线程安全
(同时保证原子性、可见性和有序性) | 一个写线程,多个读线程、
双重校验锁实现单例模式中的单例对象需要使用volatile来保证单例对象的可见性和有序性。 |
CountDownLatch的实现原理?是否在项目中应用?
实现原理:
- CountDownLatch的底层实现使用AQS。
- 在构建CountDownLatch对象时,传入的值会赋值给AQS的state变量。
- 当执行countDown()时,其实就是利用CAS将state-1;
- 当执行await()方法的时候,其实就是判断state是否为0
- 如果不为0则加入到队列中,将该线程阻塞掉。
- 因为头结点会一直自旋等待state为0,当state为0时头结点会把剩余的在队列中阻塞的节点也一并唤醒。
具体应用:
RPC客户端调用的时候使用 CountDownLatch 实现阻塞等待(超时等待)【请求的时候设置为1,当响应成功减1,这时候客户端不再堵塞】
对happens-before语义理解?
- 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见。
synchronized锁,保证了可见性。 java static int x; static Object m = new Object(); new Thread(()->{ synchronized(m) { x = 10; } },"t1").start(); new Thread(()->{ synchronized(m) { System.out.println(x); } },"t2").start(); // 10
- 线程对 volatile变量的写,对接下来其它线程对该变量的读可见。
volatile修饰的变量, 通过写屏障, 共享到主存中, 其他线程通过读屏障, 读取主存的数据。 java volatile static int x; new Thread(()->{ x = 10; },"t1").start(); new Thread(()->{ System.out.println(x); },"t2").start();
线程 start() 前对变量的写,对该线程开始后对该变量的读可见。
java static int x; x = 10; new Thread(()->{ System.out.println(x); },"t2").start();
线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
java static int x; Thread t1 = new Thread(()->{ x = 10; },"t1"); t1.start(); t1.join(); System.out.println(x);
线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后, 对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted) ```java static int x; public static void main(String[] args) { Thread t2 = new Thread(()->{ while(true) { if(Thread.currentThread().isInterrupted()) { System.out.println(x); // 10, 打断了, 读取的也是打断前修改的值 break; } } },"t2"); t2.start(); new Thread(()->{ sleep(1); x = 10; t2.interrupt(); },"t1").start();
while(!t2.isInterrupted()) { Thread.yield(); } System.out.println(x); // 10
} ```
对变量默认值(0,false,null)的写,对其它线程对该变量的 读可见 (最基本)
- 传递性:如果操作A先于操作B、操作B先于操作C,则操作A先于操作C。
第3章 Java线程池
什么是线程池?线程池的好处?
线程池:创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务。线程池的好处:
- 降低资源的消耗:可避免频繁创建和销毁线程带来的巨大开销。
- 提高线程的复用性:处理完任务的线程并不会被销毁,而是仍然在线程池中等待下一个任务。
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就立即执行。
提高线程的可管理性:使用线程池可以进行统一的分配、调优和监控线程。
线程池的七大参数?
corePoolSize(核心线程数):线程池中核心线程数的最大值。
- maximumPoolSize(最大线程数):只有阻塞队列满了的时候,这个参数才会生效,规定线程池最多运行的线程数。【核心线程+非核心线程】
- keepAliveTime(非核心线程空闲时长):超出核心线程数的那些线程的生存时间,如果这些线程长时间没有任务执行并且超过这个时间限制,那么就会消亡。
- unit(时间单位):非核心线程空闲时间的时间单位。
- workQueue(任务队列):推荐使用有界队列,用于缓存任务的阻塞队列。
- threadFactory(线程工厂):创建线程的工厂。
- handler(拒绝策略):默认采用AbortPolicy拒绝策略,阻塞队列满且线程数量超过最大线程数量时,执行的拒绝策略。
Java线程池工作过程?(线程池核心线程大小和线程池最大线程数量的区别?)
- 用户提交一个任务,检查线程池是否还在运行,如果还在运行,判断线程数是否达到核心线程数:
- 如果线程数还没达到核心线程数,创建一个核心线程来执行任务。
- 如果线程数已经达到核心线程数,那么判断阻塞队列是否已满。
- 如果阻塞队列已满,判断当前线程数是否达到最大线程数。
- 如果已经达到最大线程数,执行拒绝策略。
- 如果还没达到最大线程数,创建非核心线程(救急线程)来执行任务。
- 如果阻塞队列未满,将任务放到阻塞队列里面。
- 线程池不处于运行状态,执行拒绝策略。
当高峰过去后,超过核心线程数的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制。
线程池的几种状态?
- RUNNING:线程池一旦被创建,就处于RUNNING状态,任务数为0,能够接收新任务,对已排队的任务进行处理。
- SHUTDOWN:不接收新任务,但能处理已排队的任务。调用线程池的shutdown() 方法,线程池由RUNNING 转变为SHUTDOWN 状态。
- STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的shutdownNow() 方法,线程池由(RUNNING或SHUTDOWN ) 转变为STOP 状态。
- TIDYING:
- SHUTDOWN状态下,任务数为0, 其他所有任务已终止,线程池会变为TIDYING 状态,会执行terminated() 方法。线程池中的terminated() 方法是空实现,可以重写该方法进行相应的处理。
- 线程池在SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由SHUTDOWN 转变为TIDYING 状态。
- 线程池在STOP 状态,线程池中执行中任务为空时,就会由STOP 转变为TIDYING 状态。
- TERMINATED:线程池彻底终止。线程池在TIDYING 状态执行完terminated() 方法就会由TIDYING 转变为TERMINATED 状态。
阻塞队列(BlockingQueue)的作用?使用场景?
阻塞队列的作用:当任务数大于线程池中的核心线程数时,需要使用阻塞队列将待处理的任务存放起来,等待核心线程执行完任务后按照先来先服务的原则从阻塞队列中取出任务来执行。其实这个过程是一种生产者和消费者的思想,调用者提交的任务相当于生产者的产品,核心线程去执行任务相当于消费者去消费产品。使用场景:常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。
线程池中有哪几种阻塞队列?
- ArrayBlockingQueue(数组结构组成的有界阻塞队列):创建队列对象时必须制定容量大小,并且可以指定公平与非公平性,默认情况下非公平(公平性核心线程从阻塞队列中取任务按照先来先服务的原则)。
- LinkedBlockingQueue(链表结构组成的无界阻塞队列):创建时不指定容量大小,默认大小为Integer.MAX_VALUE。
- PriorityBlockingQueue(支持优先级排序的无界阻塞队列):默认情况下元素采用自然顺序,也可以通过自定义类实现compareTo()来指定元素排序规则,或者初始化时通过构造器参数Comparator来指定排序规则。
- DelayQueue(延时无界阻塞队列):只有当其指定的延迟时间到了,才能够从队列中获取到元素。
- SynchronousQueue(不存储元素的阻塞队列):没有容量,只有当线程去任务时,才能将任务放入阻塞队列中,没有线程来取任务是没法将任务放到阻塞队列中的。
线程池中拒绝策略的类型?
当线程池已经关闭或达到饱和(最大线程和队列都已满)状态时,新提交的任务将会被拒绝。
ThreadPoolExecutor 定义了四种拒绝策略:
- AbortPolicy(终止策略):默认策略,放弃任务并抛出RejectedExecutionException异常。
- DiscardPolicy(抛弃策略):放弃任务,但是不抛出异常。
- DiscardOldestPolicy(抛弃旧任务策略):放弃队列最早的未处理任务,本任务取而代之。
- CallerRunsPolicy(调用者运行策略):由调用线程处理该任务。
我们也可以自定义拒绝策略,只需要实现 RejectedExecutionHandler。但是需要注意的是,拒绝策略的运行需要指定线程池和队列的容量。
常见线程池的类型?
| * * | FixedThreadPool | SingleThreadExecutor | ScheduledThreadPool | CachedThreadPool | | --- | --- | --- | --- | --- | | 中文名 | 定长线程池 | 单线程化线程池 | 定时线程池 | 可缓存线程池 | | 核心线程数量 | 只有核心线程
(核心线程数=最大线程数) | 只有1个核心线程 | 核心线程数量固定 | 无核心线程 | | 非核心线程数量 | 无非核心线程 | 无非核心线程 | 非核心线程数量无限 | 非核心线程数量无限 | | 执行完操作 | 执行完立即回收 | 执行完立即回收 | 执行完闲置10ms后回收 | 执行完闲置60s后回收 | | 阻塞队列 | LinkedBlockingQueue链表结构的无界队列 | LinkedBlockingQueue链表结构的无界队列 | DelayQueue
延时阻塞队列 | SynchronousQueue
不存储元素的阻塞队列 | | 应用场景 | 控制线程最大并发数 | 希望多个任务排队执行 | 执行定时或周期性任务 | 任务数较密集,但每个任务执行时间较短 |
SingleThreadExecutor:
- 和Executors.newFixedThreadPool(1)初始时为1时的区别:Executors.newFixedThreadPool(1)初始时为1,以后还可以修改。但是Executors.newSingleThreadExecutor()创建出来的1个线程不能修改。
和直接创建单线程执行任务的区别:自己创建单线程串行执行任务,如果任务出现异常直接结束线程。但是newSingleThreadExecutor线程池还会新建一个线程,保证池的正常工作,保证线程池中始终存在一个核心线程。
禁止使用Executors创建线程池的原因?
Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor():使用无界阻塞队列,所以允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM异常。
- Executors.newCachedThreadPool()和Executors.newScheduledThreadPool():允许创建的线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至抛出OOM异常。
- 阿里巴巴开发手册中提到:线程池不允许使用Executors,而是通过ThreadPoolExecutor的方式创建。这样的处理方法能根据自己机器的性能和业务场景来手动配置线程池的参数(核心线程数、使用的任务队列、拒绝策略)。这样做不仅能够规避资源风险,还可以显示地给线程池进行命名,有助于定位线程的问题。 > * 避免上面的措施:*使用有界队列,同时控制线程创建数量。
如何合理设置线程池的核心线程数?
核心线程数过小会导致程序不能充分地利用系统资源、容易导致饥饿;
核心线程数过大,线程与线程之间会争取CPU资源,会导致更多的线程上下文切换,增加线程执行时间。
综上,所以需要根据业务场景来设置合理的线程数。
- CPU密集型任务:要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在上下文切换的时间就越多,CPU执行任务的效率就越低。所以要最高效地利用CPU,可以设置:核心线程数=CPU核心数+1
- IO密集型任务:涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。所以IO密集型任务越多,CPU效率越高,但也有一个限度。核心线程数=CPU线程数*2
- 混合型任务:将任务分为I/O密集型任务和CPU密集型任务,分别让不同的线程池去处理,从而使每个线程池可以根据各自的工作负载来调整。核心线程数=(线程等待时间 / 线程CPU时间+1)*CPU核心数
怎样停掉一个线程池?
可以调用线程的shutdown或shutdownNow方法来关闭线程池。
| * * | shutdown | shutdownNow | | --- | --- | --- | | 原理 | 遍历线程池中的工作线程,然后逐个调用线程的interrut方法来中断线程,无法响应中断的任务可能永远无法终止 | | | 线程状态 | 将线程池的状态设置成SHUTDOWN | 将线程池的状态设置成STOP | | 是否接收新任务 | 不再接受新任务 | | | 是否执行阻塞队列任务 | 会将阻塞队列中的任务执行完 | 不会在执行阻塞队列中的任务,会将阻塞队列中未执行的任务返回给调用者 | | 打断什么线程 | 仅会打断空闲线程 | 会打断所有线程 | | 调用isShutdown的返回值 | True(状态不在RUNNING的线程池就返回true) | | | 调用isTerminaed的返回值 | True | | | 使用场景 | 通常用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法 | |
线程池的execute和submit的区别?
| * * | execute | submit | | --- | --- | --- | | 共同点 | 将任务提交到线程池中交给线程去处理 | | | 参数 | Runnable command | Runnable task或 Callable task | | 返回值 | 没有返回值 | 会返回一个 FutureTask 对象 | | 捕获异常 | 不可以 | 返回值 Future 调用get方法时,可以捕获处理异常 |
如何设计一个线程池?
- 线程池管理类:定义核心线程数量、任务队列、存放线程的链表或者数组、工作线程类、通知机制和拒绝策略等。
- 任务队列:负责接收用户提交的任务。
- 工作线程类:线程池中用于处理用户提交任务的线程。
- 拒绝策略:当没有空闲线程处理用户提交的任务,并且任务队列已满时,用户还提交任务时的应对策略(让提交者处理、直接抛弃,不报错、直接抛弃、抛出异常、放弃任务队列中最老的任务来执行此任务)。
- 通知机制:如果任务队列中没有任务可以处理,使线程池中所有线程处于阻塞状态。当主线程提交任务后,通知线程来处理任务。
测试类: ```java public class SimpleThreadPool { private int currentSize; //存放任务的任务队列 private static final LinkedList taskQueue = new LinkedList<>(); //存放线程的链表 private static final List workerThreadList = new ArrayList<>();
public SimpleThreadPool(int currentSize) { this.currentSize = currentSize; //对线程组进行初始化 for (int i = 0; i < currentSize; i++) { WorkerThread thread = new WorkerThread(); thread.start(); workerThreadList.add(thread); } }
public void execute(Runnable task){ synchronized (taskQueue){ taskQueue.addLast(task); taskQueue.notifyAll(); } } /**
- 定义工作线程的状态 */ private enum WorkerThreadState{ FREE, BLOCK, RUNNING, DEAD } class WorkerThread extends Thread{ WorkerThreadState state; @Override public void run(){ while(state != WorkerThreadState.DEAD){ synchronized (taskQueue){ while (taskQueue.isEmpty()){ try { taskQueue.wait(); this.state = WorkerThreadState.BLOCK; } catch (InterruptedException e) { e.printStackTrace(); } } } //所有线程并行执行任务 Runnable task = taskQueue.removeFirst(); if(task != null){ this.state = WorkerThreadState.RUNNING; task.run(); this.state = WorkerThreadState.FREE; } } } }
public static void main(String[] args) { SimpleThreadPool pool = new SimpleThreadPool(5);
IntStream.rangeClosed(0, 50).forEach(i->{ pool.execute(()->{ try { Thread.sleep(1000); System.out.println("当前线程:" + Thread.currentThread().getName() + "处理任务" + i); } catch (InterruptedException e) { e.printStackTrace(); } }); });
第4章 ThreadLocal
ThreadLocal是什么?优缺点?
ThreadLocal它提供了线程的局部变量,让每个线程可以通过get/set方法对这个局部变量进行操作,不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。优点:ThreadLocal为每个线程创建单独的空间来存储变量,数据都被封闭在各自的线程之中,各线程操作各自的值,并不会涉及到多线程的同步问题。以空间换时间,线程访问并行化,对象独享化。缺点:会发生内存泄漏。
ThreadLocal怎样使用?
```java public class ThreadLocaTest {
private static ThreadLocal<String> local = new ThreadLocal<>();
static void print(String str) {
System.out.println(str + " :" + local.get());//打印当前线程中本地内存中变量的值
local.remove(); //清除内存中的本地变量
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
ThreadLocaTest.local.set("xdclass_A");
print("A");
//打印本地变量
System.out.println("清除后:" + local.get());
}
},"A").start();
Thread.sleep(1000);
new Thread(new Runnable() {
public void run() {
ThreadLocaTest.local.set("xdclass_B");
print("B");
System.out.println("清除后 " + local.get());
}
},"B").start();
}
ThreadLocal的实现原理,底层数据结构?
ThreadLocal是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,ThreadLocalMap由若干个Entry组成,每个Entry的key是当前ThreadLocal的弱引用,value是使用set方法设置的值。
set()方法执行流程
java // 作用:将当前线程的这个线程局部变量的副本设置为指定的值 public void set(T value) { // 1)拿到当前线程 Thread t = Thread.currentThread(); // 2)通过线程内部的 threadLocals 变量,拿到对应 ThreadLocalMap 对象。对应着分析1 ThreadLocalMap map = getMap(t); // 3)判断如果不为 null ,则直接调用 ThreadLocalMap 中的 set 方法,传入 当前的 ThreadLocal 对象和要指定修改的值 value,对应着分析2 if (map != null) map.set(this, value); else // 4)创建 map 为 null,就创建 map, 对应着分析3 createMap(t, value); }
get()方法执行流程
java // 作用:返回该线程局部变量在当前线程副本中的值 public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { // 通过 getEntry 找到线程对应着的 Entry 对象, 对应着分析1 ThreadLocalMap.Entry e = map.getEntry(this); // 如果不为 null 则直接拿到返回 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 如果为 null , 就会调用 initialValue 方法, 拿到初始值。对应着分析 2 return setInitialValue(); }
为什么ThreadLocal的键是弱引用,如果是强引用会有什么问题呢?
- 如果是强引用的话,即使ThreadLocal的值是为null,但是的话ThreadLocalMap还是会有ThreadLocal的强引用状态,如果没有手动进行删除的话,ThreadLocal就不会被回收,这样就会导致Entry内存的泄漏
- 如果是弱引用的话,弱引用ThreadLocal的对象被回收掉了,即使没有进行手动删除value值。value在下一次的ThreadLocalMap调用set/get/remove方法的时候就会被清除掉。
ThreadLocal为什么会出现内存泄露?如何解决?
每个Thread中都存在一个ThreadLocalMap, ThreadLocalMap中的key为一个ThreadLocal实例.,这个Map的确使用了弱引用,当把ThreadLocal实例置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被gc回收。
但是value却不能回收,因为存在一条从CurrentThread连接过来的强引用。只有当前Thread结束以后, CurrentThread就不会存在栈中,强引用断开。CurrentThread, Map, value将全部被GC回收。
在ThreadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。解决方案:每次用完ThreadLocal都手动调用它的remove()方法清除数据。
为什么建议把ThreadLocal修饰为static?
ThreadLocal能实现线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap。所以,ThreadLocal可以只初始化一次,只分配一块存储空间既可,没必要作为成员变量多次初始化。
ThreadLocal与Thread同步机制的区别?
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化,对象共享化。ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。同步机制:通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序缜密的分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大ThreadLocal:从另一个角度来解决多线程的并发访问。ThreadLocal为每一个线程提供一个独特的变量副本,从而隔离了多个线程对访问数据的冲突。数据都被封闭在各自的线程之中,各线程操作各自的值,并不会涉及到多线程的同步问题。
总而言之,当遇到多线程操作同一个共享变量需要保证线程安全的时候,你应该首先考虑使用ThreadLocal而不是synchronized!
ThreadLocal在项目中的应用?
在手写RPC项目中,使用Kryo进行序列化操作的时候由于Kryo不是线程安全的,所以使用ThreadLocal来让每个线程装载着自己的Kryo对象,以达到在序列化和反序列化的时线程安全的目的。
ThreadLocal的应用场景?Spring里有用到吗?
- 经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现A线程关了B线程正在使用的 Connection;
- Spring事务注解的实现
生产者与消费者模式
1. 使用wait和notify
java public class ProductConsumerTest { public static void main(String[] args) { MessageQueue queue = new MessageQueue(2); for (int i = 0; i < 3; i++) { int id = i; new Thread(() -> { queue.put(new Message(id, "值" + id)); }, "生产者" + i).start(); } new Thread(() -> { while (true) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } Message message = queue.take(); } }, "消费者").start(); } } // 消息队列类,在线程之间通信 class MessageQueue { // 消息的队列集合 private LinkedList<Message> list = new LinkedList<>(); // 队列容量 private int capcity; public MessageQueue(int capcity) { this.capcity = capcity; } // 获取消息 public Message take() { // 检查队列是否为空 synchronized (list) { while (list.isEmpty()) { try { System.out.println("队列为空, 消费者线程等待"); list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 从队列头部获取消息并返回 Message message = list.removeFirst(); System.out.println("已消费消息 " + message); list.notifyAll(); return message; } } // 存入消息 public void put(Message message) { synchronized (list) { // 检查对象是否已满 while (list.size() == capcity) { try { System.out.println("队列已满, 生产者线程等待"); list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 将消息加入队列尾部 list.addLast(message); System.out.println("已生产消息 " + message); list.notifyAll(); } } } @Data @AllArgsConstructor class Message { private int id; private Object value; }
2. 使用ReentrantLock
- Condition提供了await()方法将当前线程阻塞,并提供signal()方法支持另外一个线程将已经阻塞的线程唤醒。
- 需要结合Lock使用。
- 线程调用await()方法前必须获取锁,调用await()方法时,将线程构造成节点加入等待队列,同时释放锁,并挂起当前线程。
- 其他线程调用signal()方法前也必须获取锁,当执行signal()方法时将等待队列的节点移入到同步队列,当线程退出临界区释放锁的时候(ReentrantLock.unlock),唤醒同步队列的首个节点。 ```java public class PC ReentrantLockTest { public static void main(String[] args) { MessageQueue queue = new MessageQueue(2); for (int i = 0; i < 3; i++) { int id = i; new Thread(() -> { queue.put("" + id); }, "生产者" + i).start(); } new Thread(() -> { while (true) { try { Thread.sleep(1); queue.get(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "消费者").start(); } }
class MessageQueue{ private int capacity; private LinkedList list = new LinkedList<>(); private ReentrantLock lock = new ReentrantLock(); //定义锁对象 //定义两个Condtion对象 private Condition productCondition = lock.newCondition(); private Condition consumeCondition = lock.newCondition();
public MessageQueue(int capacity){
this.capacity = capacity;
}
//生产者往队列放消息
public void put(String message){
lock.lock();
try {
while(list.size() == this.capacity){
System.out.println("队列已经满了,等待剩余空间");
productCondition.await();
}
list.addLast(message);
System.out.println(Thread.currentThread().getName() + ":产品" + message);
consumeCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//消费者从队列中取消息
public void get(){
lock.lock();
try {
while(list.isEmpty()){
System.out.println("队列为空,请等待产品!");
consumeCondition.await();
}
String s = list.removeFirst();
System.out.println(Thread.currentThread().getName() + ":产品" + s);
productCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
3. 使用阻塞队列
阻塞队列可以在生成对象时指定容量大小。它用于阻塞操作的是put()和take()方法:
put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。
take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞。 ```java public class PCBlockQueueTest { public static void main(String[] args) { MessageQueue2 queue = new MessageQueue2(2); for (int i = 0; i < 3; i++) { int id = i; new Thread(() -> { queue.put("" + id); }, "生产者" + i).start(); } new Thread(() -> { while (true) { try { Thread.sleep(1); queue.get(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "消费者").start(); } } class MessageQueue2{ private ArrayBlockingQueue
blockingQueue;
public MessageQueue2(int capacity){
blockingQueue = new ArrayBlockingQueue<String>(capacity);
}
//生产者往队列放消息
public void put(String message){
try {
blockingQueue.put(message);
System.out.println(Thread.currentThread().getName() + ":产品" + message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费者从队列中取消息
public void get(){
try {
String s = blockingQueue.take();
System.out.println(Thread.currentThread().getName() + ":产品" + s);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
哲学家进餐问题
有五位哲学家,围坐在圆桌旁。他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。如果筷子被身边的人拿着,自己就得等待
使用ReentrantLock的tryLock()解决死锁问题(破坏不剥夺条件) ```java public class ReentrantLockTest { public static void main(String[] args) { Chopstick chopstick1 = new Chopstick("1"); Chopstick chopstick2 = new Chopstick("2"); Chopstick chopstick3 = new Chopstick("3"); Chopstick chopstick4 = new Chopstick("4"); Chopstick chopstick5 = new Chopstick("5"); new Philosopher("name1", chopstick1, chopstick2).start(); new Philosopher("name2", chopstick2, chopstick3).start(); new Philosopher("name3", chopstick3, chopstick4).start(); new Philosopher("name4", chopstick4, chopstick5).start(); new Philosopher("name5", chopstick5, chopstick1).start(); } }
class Philosopher extends Thread{ Chopstick left, right; public Philosopher(String name, Chopstick left, Chopstick right){ super(name); this.left = left; this.right = right; } @Override public void run() { while (true){ if(left.tryLock()){ try { if(right.tryLock()){ try { eat(); }finally { right.unlock(); } } }finally { left.unlock(); } } } } public void eat(){ System.out.println(Thread.currentThread().getName() + "eating...."); } }
class Chopstick extends ReentrantLock{ String name; public Chopstick(String name){ this.name = name; } } ```
两个线程交替打印奇偶数(0-100)
1.创建两个线程,一个线程负责打印奇数,另一个线程打印偶数,两个线程竞争同一个对象锁,每次打印一个数字后释放锁,然后另一个线程拿到锁打印下一个数字。
```java public class SynchronizedTest { private static int count;
private static Object object = new Object();
public static void main(String[] args) {
new Thread(() ->{
while(count < 100){
synchronized (object){
if(count % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
}
}, "偶数线程").start();
new Thread(() ->{
while(count < 100){
synchronized (object){
if(count % 2 == 1){
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
}
}, "奇数线程").start();
}
2.无需判断数字是否是奇偶数,两个线程通过等待唤醒机制,交替打印数字。
```java public class SynchronizedWaitSignalTest { private static int count; private static Object object = new Object();
public static void main(String[] args) {
new Thread(new Print(), "偶数线程").start();
new Thread(new Print(), "奇数线程").start();
}
static class Print implements Runnable{
@Override
public void run() {
while (count < 100) {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
object.notify();
// 此处判断,是为了打印完了100个数字后,程序能够正常结束,否则程序将一直等待下去,耗费系统资源。
if(count < 100){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
3.使用ReentrantLock的Condition
```java public class ReentrantLockTest { public static void main(String[] args) { PrintOddEven printOddEven = new PrintOddEven(); new Thread(()->{ printOddEven.printEven(); }, "偶数线程").start(); new Thread(()->{ printOddEven.printOdd(); }, "奇数线程").start(); } }
class PrintOddEven{ static int cnt = 0; ReentrantLock lock = new ReentrantLock(); private final Condition odd = lock.newCondition(); private final Condition even = lock.newCondition();
public void printOdd() {
while (cnt < 100) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":" + cnt++);
even.signal();
if(cnt < 100){
odd.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
public void printEven() {
while (cnt < 100) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":" + cnt++);
odd.signal();
if(cnt < 100){
even.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
两个线程交替输出ABABABABABAB...
1.synchronized + 标志位 + wait+ notify
java public class SynchronizedTest { public static void main(String[] args) { PrintAB printAB = new PrintAB(); new Thread(()->{ for (int i = 0; i < 5; i++) { printAB.printA(); } }).start(); new Thread(()->{ for (int i = 0; i < 5; i++) { printAB.printB(); } }).start(); } } class PrintAB{ private boolean flag = true; public synchronized void printA(){ while(!flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("A"); flag = false; this.notify(); } public synchronized void printB(){ while(flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("B"); flag = true; this.notify(); } }
2.ReentrantLock+Condition+await()+signal()
```java public class ReentrantLockTest { public static void main(String[] args) { Print printAB = new Print(); new Thread(()->{ for (int i = 0; i < 5; i++) { printAB.printA(); } }).start(); new Thread(()->{ for (int i = 0; i < 5; i++) { printAB.printB(); } }).start(); } }
class Print{ private boolean flag = true; ReentrantLock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); public void printA(){ lock.lock(); try { while(!flag){ condition.await(); } System.out.println("A"); flag = false; condition.signal(); }catch (InterruptedException e){ e.printStackTrace(); }finally { lock.unlock(); } } public void printB(){ lock.lock(); try { while(flag){ condition.await(); } System.out.println("B"); flag = true; condition.signal(); }catch (InterruptedException e){ e.printStackTrace(); }finally { lock.unlock(); } } } ```
两个线程轮流打印1A 2B 3C
1. ReentrantLock+Condition+await()+signal()
```java public class ReentrantLockTest { public static void main(String[] args) { PrintOddEven printOddEven = new PrintOddEven(); new Thread(()->{ printOddEven.printNum(); }, "数字线程").start(); new Thread(()->{ printOddEven.printChar(); }, "字符线程").start(); } }
class PrintOddEven{ static int num = 1; static char c = 'A'; ReentrantLock lock = new ReentrantLock(); private final Condition numCondition = lock.newCondition(); private final Condition charCondition = lock.newCondition();
public void printNum() {
while (num < 10) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":" + num++);
charCondition.signal();
if(num < 10){
numCondition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
public void printChar() {
while (c < 'K') {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":" + c++);
numCondition.signal();
if(c < 'K'){
charCondition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
如何顺序执行多个线程(有三个线程T1,T2,T3,如何保证顺序执行)
1. 利用join()
在main方法中,先是调用了t1.start方法,启动t1线程,随后调用t1的join方法,main所在的主线程就需要等待t1子线程中的run方法运行完成后才能继续运行,所以主线程卡在t2.start方法之前等待t1程序。等t1运行完后,主线程重新获得主动权,继续运行t2.start和t2.join方法,与t1子线程类似,main主线程等待t2完成后继续执行,如此执行下去,join方法就有效的解决了执行顺序问题。 java public class Test { public static void main(String[] args) { Thread T1 = new Thread(() -> { try { System.out.println("T1 running......"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }, "T1"); Thread T2 = new Thread(() -> { try { T1.join(); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("T2 running......"); }, "T2"); Thread T3 = new Thread(() -> { try { T2.join(); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("T3 running......"); }, "T3"); T1.start(); T2.start(); T3.start(); } }
2.利用CountDownLatch
```java public class CountDownLatchTest { public static void main(String[] args) { CountDownLatch c0 = new CountDownLatch(0); CountDownLatch c1 = new CountDownLatch(1); CountDownLatch c2 = new CountDownLatch(1);
Thread t1 = new Thread(new Work(c0, c1));
Thread t2 = new Thread(new Work(c1, c2));
Thread t3 = new Thread(new Work(c2, c2));
t1.start();
t2.start();
t3.start();
}
}
class Work implements Runnable{ //c1 是前一个线程等待的数量,c2是当前线程等待的数量 CountDownLatch c1, c2; public Work(CountDownLatch c1, CountDownLatch c2){ this.c1 = c1; this.c2 = c2; } @Override public void run() { try { c1.await(); System.out.println(Thread.currentThread().getName()); c2.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } } } ```
3.利用BlockQueue
阻塞队列(BlockingQueue)是JUC包下重要的数据结构,提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。 ```java public class BlockQueueTest { public static void main(String[] args) { BlockingQueue blockingQueue = new LinkedBlockingDeque<>(); Thread thread1 = new Thread(new Work(), "Thread1"); Thread thread2 = new Thread(new Work(), "Thread2"); Thread thread3 = new Thread(new Work(), "Thread3"); blockingQueue.add(thread1); blockingQueue.add(thread2); blockingQueue.add(thread3); for (int i = 0; i < 3; i++) { Thread thread = null; try { thread = blockingQueue.take(); } catch (InterruptedException e) { e.printStackTrace(); } thread.start(); //检测线程是否还活着 while(thread.isAlive()) {} } } }
class Work implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()); } } ```
有什么方法让main等待10个线程执行完后再往下执行?
1. 使用join()
将所有子线程加入到全局的list中,在主线程中循环list,每次都调用thread.join()等待当前子线程执行完后,mian线程才执行。 java public class JoinTest { public static void main(String[] args) { ArrayList<Thread> threads = new ArrayList<>(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(()->{ System.out.println(Thread.currentThread().getName() + ": running..."); }); threads.add(thread); thread.start(); } for (Thread thread : threads){ try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("main running..."); } }
2. 使用JUC下面的CountDownLatch
java public class CountDownLatchTest { public static void main(String[] args) { CountDownLatch latch = new CountDownLatch(10); for (int i = 0; i < 10; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + ":running..."); latch.countDown(); }).start(); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("主线程 runnning..."); } }