JUC多线程并发编程(二)
读写锁ReadWriteLock
什么是读写锁?
读写锁包括独占锁(写锁)和共享锁(读锁)。
接口:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
实现类:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable
独占锁:表示只有一个线程可以持有这个锁
共享锁:表示可以多个线程可以持有这个锁,适用于读多写少
应用场景:在多线程的情况下,当对存储区(数组,map等等)进行添加操作,会出现一个线程正在添加中,另一个线程也会进行添加,此时需要添加独占锁来确保添加操作的原子性,而读操作可以多线程同时读取的,满足其并发量。也就是说,读写不共存,读读共存,写写不共存。
static class MyCacheLock{
private volatile Map<String,Object> map = new HashMap<>();
//读写锁
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key, Object value){
//写锁
readWriteLock.writeLock().lock();
try {
System.out.println("Thread"+Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println("Thread"+Thread.currentThread().getName()+"写入成功!");
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
//读锁
readWriteLock.readLock().lock();
try{
System.out.println("Thread"+Thread.currentThread().getName()+"读取"+key);
Object result = map.get(key);
System.out.println("Thread"+Thread.currentThread().getName()+"读取结果"+result);
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
}
}
}
阻塞队列BlockingQueue
什么是阻塞队列?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
阻塞队列有三个常用的实现类:
ArrayBlockingQueue:由数组结构组成的有界阻塞队列
LinkedBlockingQueue:由链表结构组成的有界阻塞队列
SynchronousQueue:不存储元素的阻塞队列,即单个元素队列
阻塞队列有四个处理方式以及对应的API:
方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 不可用 不可用
抛出异常是指阻塞队列满或空,再去队列存储或获取,就会抛出异常
返回特殊值是指阻塞队列满或空,再去队列存储或获取,就会返回false或null
一直阻塞是指阻塞队列满或空,再去队列存储或获取,就会使当前操作一直阻塞等待
超时退出是指阻塞队列满或空,再去队列存储或获取,可以设置其超时时间,在时间之内会阻塞。
应用场景:常用于生产者和消费者的场景,生产者—》往队列里添加元素,消费者—》从队列里拿取元素。在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。我们不用担心什么时候需要阻塞线程,什么时候需要唤醒线程。
SychronousQueue同步队列
什么是同步队列?
没有容量,一个生产线程,当它生产产品(put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞。等待一个消费线程调用take操作,take操作会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递)。这样的一个过程称为一次配对过程(也可以先take,后put 原理相同)
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" put 1");
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName()+" put 2");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName()+" put 3");
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"take"+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"take"+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"take"+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
}
结果:
T1 put 1
T2take1
T1 put 2
T2take2
T1 put 3
T2take3
线程池
什么是线程池?
预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接获取,避免多次重复创建、销毁带来的开销。若是每次都是需要创建线程-》执行任务-》销毁线程,这样会大大的性能消耗。线程池就是把一些能复用的东西放到一起,而不是去销毁。
线程池的三大方法:
Executors.newSingleThreadExecutor() 单个线程
newFixedThreadPool(int nThreads) 创建一个固定的线程池大小
newCachedThreadPool() 线程池的大小可以伸缩,遇强则强(要看cpu是几核的)
单个线程:
public class Demo01 {
9 public static void main(String[] args) {
10
11 ExecutorService threadpool = Executors.newSingleThreadExecutor(); //单个线程
12
13 try {
14 for (int i = 0; i < 10; i++) {
15 //使用了线程池之后,使用线程池来创建线程
16 threadpool.execute(()->{
17 System.out.println(Thread.currentThread().getName()+" ok");
18 });
19 }
20 } catch (Exception e) {
21 e.printStackTrace();
22 } finally {
23 //线程池用完,程序结束,关闭线程池
24 threadpool.shutdown(); //(为确保关闭,将关闭方法放入到finally中)
25 }
26 }
27 }
创建一个固定的线程池大小:
public class Demo01 {
9 public static void main(String[] args) {
10
11 //最多5个线程同时执行,从控制台中查看结果
12 ExecutorService threadpool = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小,(5个线程)
13
14 try {
15 for (int i = 0; i < 10; i++) {
16 //使用了线程池之后,使用线程池来创建线程
17 threadpool.execute(()->{
18 System.out.println(Thread.currentThread().getName()+" ok");
19 });
20 }
21 } catch (Exception e) {
22 e.printStackTrace();
23 } finally {
24 //线程池用完,程序结束,关闭线程池
25 threadpool.shutdown(); //(为确保关闭,将关闭方法放入到finally中)
26 }
27 }
28 }
遇强则强(要看cpu是几核的):
public class Demo01 {
9 public static void main(String[] args) {
10
11 ExecutorService threadpool = Executors.newCachedThreadPool(); //缓存池,可伸缩的, 遇强则强,遇弱则弱
12
13 try {
14 for (int i = 0; i < 10; i++) {
15 //使用了线程池之后,使用线程池来创建线程
16 threadpool.execute(()->{
17 System.out.println(Thread.currentThread().getName()+" ok");
18 });
19 }
20 } catch (Exception e) {
21 e.printStackTrace();
22 } finally {
23 //线程池用完,程序结束,关闭线程池
24 threadpool.shutdown(); //(为确保关闭,将关闭方法放入到finally中)
25 }
26 }
27 }
线程池的七大参数:
看三大方法的源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看得出来三大方法都是通过ThreadPoolExecutor这个类来创建的,此时只需要看这个类的构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize:核心线程数
maximumPoolSize:最大线程数
keepAliveTime:存活时间
TimeUnit unit:时间单位
BlockingQueue workQueue:阻塞队列
Executors.defaultThreadFactory():工厂方法
defaultHandler:处理策略
假设有一个银行的场景,总共有五个窗口(线程),三张椅子(阻塞队列),此时只有两个窗口开着的,当有两个人进来办理业务(任务)时,直接办理,再来一个人,就会坐上椅子进行等待,陆陆续续再来时,椅子满了时,此时剩余的窗口就会开启处理任务,要是窗口和椅子都满了,这时要是再有人来银行,这个人会选择离开或则等待;
从这个场景中,开始的两个窗口代表是核心线程数,三张椅子代表是阻塞队列,五个窗口为最大线程数,而最后一个人的选择代表是处理策略;
存活时间是指当线程空闲的时间且数量大于核心线程数超过这个时间就会进行销毁。
阻塞队列的作用?
在银行的场景中,有两个窗口一直是开着的,代表着线程一直都是占用CPU资源的,而当没有任务,这个两个窗口也占用cpu资源,阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源。
为什么是先添加队列而不是创建最大线程?
在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。线程的创建与销毁很影响性能的,当任务量不多时,核心线程数能处理完当前任务,阻塞队列的任务再去处理,这样整体的效率提高,避免了线程的创建与销毁。
四种拒绝策略:
AbortPolicy(抛出异常)
public static void main(String[] args) throws Exception{
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 5;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(10);
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue, handler);
for(int i=0; i<100; i++) {
try {
executor.execute(new Thread(() -> log.info(Thread.currentThread().getName() + " is running")));
} catch (Exception e) {
log.error(e.getMessage(),e);
}
}
executor.shutdown();
}
CallerRunsPolicy(从哪里来到哪里去)
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
控制台会打印“main is running”
DiscardPolicy(丢弃任务)
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
DiscardOldestPolicy(替换旧的)
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
Volatile关键字
在认识Volatile关键字之前,先认识JMM(内存模型)。
什么是JMM?
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
正是因为JMM的规定,在多线程的情况下,一个线程对内存的变量进行改动,另一个线程是不知道的,就会导致可见性问题
public class VolatileExample {
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 开启线程
myThread.start();
// 主线程执行
for (; ; ) {
if (myThread.isFlag()) {
System.out.println("主线程访问到 flag 变量");
}
}
}
}
class MyThread extends Thread {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改变量值
flag = true;
System.out.println("flag = " + flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
在上述程序中,main线程永远也访问不到flag变量。JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。这样就导致了可见性问题.使用volatile关键字就会避免这个问题。使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
**嗅探机制工作原理:**每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
class MyThread extends Thread {
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改变量值
flag = true;
System.out.println("flag = " + flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
Volatile不能保证原子性
原子性就是线程在执行的时候,不能打扰,要么成功,要么失败。
public class VDemo02 {
private static volatile int number = 0;
public static void add(){
number++;
//++ 不是一个原子性操作,是两个~3个操作
}
public static void main(String[] args) {
//理论上number === 20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
//main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num="+number);
}
}
此时输出的结果就是乱的。
使用synchronized和lock是可以解决这个问题的,但不是用锁我们该怎么解决这个问题呢?
使用原子类:
public class VDemo02 {
private static volatile AtomicInteger number = new AtomicInteger();
public static void add(){
number.incrementAndGet(); //底层是CAS保证的原子性
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
//main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+",num="+number);
}
}
看incrementAndGet源码:
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
底层是CAS,是直接操作内存的。所以原子类能保证原子性
volatile禁止指令重排
什么是指令重排?
我们写的程序,计算机并不是按照我们自己写的那样去执行的,源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候,会考虑数据之间的依赖性!
int x=1; //1
int y=2; //2
x=x+5; //3
y=x*x; //4
//我们期望的执行顺序是 1_2_3_4 可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的1234567
可能造成的影响结果:前提:a b x y这四个值 默认都是0
线程A 线程B
x=a y=b
b=1 a=2
正常的结果: x = 0; y =0;
线程A 线程B
b=1 a=2
x=a y=b
可能在线程A中会出现,先执行b=1,然后再执行x=a;
在B线程中可能会出现,先执行a=2,然后执行y=b;
那么就有可能结果如下:x=2; y=1.
volatile可以避免指令重排
单例模式
饿汉式
//饿汉式单例
public class Hungery {
private Hungery(){
}
private final static Hungery hungery =new Hungery();
public static Hungery getInstance(){
return hungery;
}
public static void main(String[] args) {
System.out.println(Hungery.getInstance());
System.out.println(Hungery.getInstance());
}
}
懒汉式
public class Lazy {
private Lazy(){
}
private static Lazy lazy;
public static Lazy getInstance(){
if (lazy==null){
lazy=new Lazy();
}
return lazy;
}
public static void main(String[] args) {
for(int i=1;i<=10;i++){
new Thread(()->{
System.out.println(Lazy.getInstance());
}).start();
}
}
}
在多线程的情况下,这样的单例式不安全的,依然会有多个线程同时执行,创建不同的实例,此时我们需要加把锁
public class Lazy {
private Lazy(){
}
private static Lazy lazy;
public static Lazy getInstance(){
if (lazy==null){
synchronized (Lazy.class){
if (lazy==null){
lazy=new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) {
for(int i=1;i<=10;i++){
new Thread(()->{
System.out.println(Lazy.getInstance());
}).start();
}
}
}
此时还会有一个问题,就是new Lazy实例对象的时候,它不是原子性操作,首先会分配内存空间,再执行构造方法,初始化对象,然后把这个对象指向这个空间。多线程情况下,这样就会导致指令重排的问题,就是在上诉代码中,一个线程调用getInstance方法,另一个因为指令重排会直接跳过if判断语句,就会出现还没构造对象就返回的问题。解决这个问题就只需要在共享变量中加个volatile关键字,
public class Lazy {
private Lazy(){
}
private static volatile Lazy lazy;
public static Lazy getInstance(){
if (lazy==null){
synchronized (Lazy.class){
if (lazy==null){
lazy=new Lazy();
}
}
}
return lazy;
}
public static void main(String[] args) {
for(int i=1;i<=10;i++){
new Thread(()->{
System.out.println(Lazy.getInstance());
}).start();
}
}
}
你以为这样就是最终的单例吗?其实反射也是可以破坏单例模式,通过getDeclaredConstructor()方法得到构造器对象,将其setAccessible()为false,这样类的构造器就拿到了,破坏了单例模式。解决方法为可以在构造器中抛出异常。
查看反射的源码,可以发现,枚举类是无法破坏的,会直接抛出一个异常.
public enum EnumSingle {
Final;
private EnumSingle(){
}
public EnumSingle getInstance(){
return Final;
}
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
EnumSingle instance1 = EnumSingle.Final;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1==instance2);
}
}
此程序会直接抛出异常,IllegalArgumentException: Cannot reflectively create enum objects。这里需要注意的是在通过反射拿到构造器的是有参构造方法,而不是无参构造函数。
CAS
什么是CAS?
CAS : compareAndSet 比较并交换
public class casDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//boolean compareAndSet(int expect, int update)
//期望值、更新值
//如果实际值 和 我的期望值相同,那么就更新
//如果实际值 和 我的期望值不同,那么就不更新
System.out.println(atomicInteger.compareAndSet(2020, 2021));//返回true
System.out.println(atomicInteger.get());//返回2021
//因为期望值是2020 实际值却变成了2021 所以会修改失败
//CAS 是CPU的并发原语
atomicInteger.getAndIncrement(); //++操作
System.out.println(atomicInteger.compareAndSet(2020, 2021));//返回false
System.out.println(atomicInteger.get());//返回2020
}
}
查看源码发现:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe U = Unsafe.getUnsafe();
private static final long VALUE
= U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
里面有一个Unsafe类:这个类是C++写的,里面都是native方法,可以直接操作内存,而Java是无法操作直接内存的,通过这个类间接性的操作内存。
CAS:比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。一次性只能保证一个变量的原子性。
CAS存在ABA问题:就是一个线程期望值为1,更新值为2,另一个线程期望值为1,更新值为3,另一个线程执行,更新值为1,此时另一个线程期望值为3,更新值为1,执行速度很快,就把值更新为1,这样就骗过了第一个线程。
解决ABA问题的思想就是:乐观锁,带有版本号的CAS
public class CASDemo {
/**AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
* 正常在业务操作,这里面比较的都是一个个对象
*/
static AtomicStampedReference<Integer> atomicStampedReference = new
AtomicStampedReference<>(1, 1);
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改操作时,版本号更新 + 1
atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1);
System.out.println("a2=>" + atomicStampedReference.getStamp());
// 重新把值改回去, 版本号更新 + 1
System.out.println(atomicStampedReference.compareAndSet(2, 1,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1));
System.out.println("a3=>" + atomicStampedReference.getStamp());
}, "a").start();
// 乐观锁的原理相同!
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 3,
stamp, stamp + 1));
System.out.println("b2=>" + atomicStampedReference.getStamp());
}, "b").start();
}
}
自旋锁
什么是自旋锁?
一个线程在获取到锁的时间里,就会循环自旋
通过CAS自定义自旋锁:
public class SpinlockDemo {
// 默认
// int 0
//thread null
AtomicReference<Thread> atomicReference=new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"===> mylock");
//自旋锁
while (!atomicReference.compareAndSet(null,thread)){
System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
}
}
//解锁
public void myUnlock(){
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"===> myUnlock");
atomicReference.compareAndSet(thread,null);
}
}
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
reentrantLock.unlock();
//使用CAS实现自旋锁
SpinlockDemo spinlockDemo=new SpinlockDemo();
new Thread(()->{
spinlockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock();
}
},"t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
spinlockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock();
}
},"t2").start();
}
}
t2进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待。
死锁
什么是死锁?
死锁是指A线程持有锁不放,B线程持有锁不放,而A线程却需要B线程的锁,B线程需要A线程的锁,这样就造成了死锁。
public class DeadLock {
public static void main(String[] args) {
String lockA= "lockA";
String lockB= "lockB";
new Thread(new MyThread(lockA,lockB),"t1").start();
new Thread(new MyThread(lockB,lockA),"t2").start();
}
}
class MyThread implements Runnable{
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
}
}
}
}
如何去找出死锁?
1、使用jps定位进程号,jdk的bin目录下: 有一个jps
命令:jps -l
2、使用jstack 进程进程号 找到死锁信息
over
本文借鉴狂神说Java并发编程视频:https://www.bilibili.com/video/BV1B7411L7tE?p=33加上了自己的理解做的笔记