volatile 是一种轻量级的同步机制。java
JMM(Java 内存模型)是一种抽象的概念,描述了一组规则或规范,定义了程序中各个变量的访问方式。linux
JVM运行程序的实体是线程,每一个线程建立时 JVM 都会为其建立一个工做内存,是线程的私有数据区域。JMM中规定全部变量都存储在主内存,主内存是共享内存。线程对变量的操做在工做内存中进行,首先将变量从主内存拷贝到工做内存,操做完成后写会主内存。不一样线程间没法访问对方的工做内存,线程通讯(传值)经过主内存来完成。小程序
JMM 对于同步的规定:api
原子性是不可分割,某个线程正在作某个具体业务时,中间不能够被分割,要么所有成功,要么所有失败。数组
重排序:计算机在执行程序时,为了提升性能,编译器和处理器经常对指令作重排序,源代码通过编译器优化重排序、指令并行重排序、内存系统的重排序以后获得最终执行的指令。缓存
在单线程中保证程序最终执行结果和代码执行顺序执行结果一致。安全
多线程中线程交替执行,因为重排序,两个线程中使用的变量可否保证一致性没法肯定,结果没法肯定。bash
处理器在处理重排序时须要考虑数据的依赖性。服务器
volatile 实现禁止指令重排序,避免多线程环境下程序乱序执行。是经过内存屏障指令来执行的,经过插入内存屏障禁止在内存屏障后的指令执行重排序优化,并强制刷出缓存数据,保证线程能读取到这些数据的最新版本。多线程
class MyData {
//volatile int number = 0;//case2
//int number=0; //case1
public void change() {
number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData data=new MyData();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
try{ TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
data.change();
System.out.println(Thread.currentThread().getName()+"\t updated number value:"+data.number);
},"A").start();
while(data.number==0){}
System.out.println(Thread.currentThread().getName()+"\t over, get number:"+data.number);
}
}
复制代码
当咱们使用case1的时候,也就是number没有volatile修饰的时候,运行结果:
A come in
A updated number value:60
复制代码
而且程序没有执行结束,说明在main线程中因为不能保证可见性,一直在死循环。
当执行case2的时候:
A come in
A updated number value:60
main over, get number:60
复制代码
保证了可见性,所以main成功结束。
class MyData {
volatile int number = 0;
public void change() {
number = 60;
}
public void addOne() {
number++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
case2();
}
//验证原子性
public static void case2() {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addOne();
}
}, String.valueOf(i)).start();
}
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t number value:"+myData.number);
}
}
复制代码
最终输出结果能够发现并非 20000,且屡次输出结果并不一致,所以说明 volatile 不能保证原子性。
DCL模式的单例模式
public class Singleton {
private static Singleton instance=null;
private Singleton(){
System.out.println(Thread.currentThread().getName()+" constructor");
}
//DCL 双端检锁机制
public static Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null)
instance=new Singleton();
}
}
return instance;
}
}
复制代码
DCL 机制不能彻底保证线程安全,由于有指令重排序的存在。
缘由在于instance = new Singleton(); 能够分为三步:
1. memory=allocate();//分配内存空间
2. instance(memory);//初始化对象
3. instance=memory;//设置instance指向分配的内存地址,分配成功后,instance!=null
复制代码
因为步骤2和步骤3不存在数据依赖关系,且不管重排序与否执行结果在单线程中没有改变,所以这两个步骤的重排序是容许的。也就是说指令重排序只会保证单线程串行语义的一致性(as-if-serial),可是不会关心多线程间的语义一致性。
所以,重排序以后,先执行3会致使instance!=null,可是对象还未被初始化。此时,别的线程在调用时,获取了一个未初始化的对象。
所以,在声明 instance 时,使用 volatile 进行修饰,禁止指令重排序。
private static volatile Singleton instance = null;
复制代码
CAS 的全程是 CompareAndSwap,是一条 CPU 并发原语。它的功能是判断内存某个位置的值是否为预期值,若是是则更新为新的值,这个过程是原子的。
CAS 的做用是比较当前工做内存中的值和主内存中的值,若是相同则执行操做,不然继续比较直到主内存和工做内存中的值一致为止。主内存值为V,工做内存中的预期值为A,要修改的更新值为B,当且仅当A和V相同,将V修改成B,不然什么都不作。
在原子类中,CAS 操做都是经过 Unsafe 类来完成的。
//AtomicInteger i++
public final int getAndIncrement(){
return unsafe.getAndAddInt(this,valueoffset,1);
}
复制代码
其中 this 是当前对象, valueoffset 是一个 long ,表明地址的偏移量。
//AtomicInteger.java
private static final Unsafe unsfae=Unsafe.getUnsafe();//unsafe对象
private static final long valueOffset;//地址偏移量
static{
try{
valueoffset=unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value");
}catch(Excepthion ex){throw new Error(ex);}
}
private volatile int value;//存储的数值
复制代码
Unsafe 类是 rt.jar 下的 sun.misc 包下的一个类,基于该类能够直接操做特定内存的数据。
Java方法没法直接访问底层系统,须要使用 native 方法访问,Unsafe 类的内部方法都是 native 方法,其中的方法能够像C的指针同样直接操做内存,Java 中的 CAS 操做的执行都依赖于 Unsafe 类的方法。
该变量表示变量值在内存中的偏移地址, Unsafe 就是根据内存偏移地址获取数据的。
CAS 并发源于体如今 Java 中就是 Unsafe 类的各个方法。调用该类中的 CAS 方法,JVM会帮咱们实现出 CAS 汇编指令,这是一种彻底依赖于硬件的功能。
原语是由若干条指令组成的,用于完成某个功能的过程。原语的执行必须是连续的,执行过程不容许被中断。因此 CAS 是一条 CPU 的原子指令,不会形成数据不一致问题。
下边是 AtomicInteger 中实现 i++ 功能所调用的 Unsafe 类的函数。
//unsafe.getAndAddInt
public final int getAndAddInt(Object var1,long var2,int var4){
int var5;
do{
//获取当前的值的地址
var5=this.getIntVolatile(var1,var2);
//var1表明对象,var2和var5分别表明当前对象的真实值和指望值,若是两者相等,更新为var5+var4
}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4);
return var5;
}
复制代码
在 getAndAddInt 函数中,var1 表明了 AtomicInteger 对象, var2 表明了该对象在内存中的地址, var4 表明了指望增长的数值。
首先经过 var1 和 var2 获取到当前的主内存中真实的 int 值,也就是 var5。
而后经过循环来进行数据更改,当比较到真实值和对象的当前值相等,则更新,退出循环;不然再次获取当前的真实值,继续尝试,直到成功。
在 CAS 中经过自旋而不是加锁来保证一致性,同时和加锁相比,提升了并发性。
具体情境来讲:线程A和线程B并发执行 AtomicInteger 的自增操做:
CAS 实现一个重要前提须要取出内存中某个时刻的数据并在当下时刻比较并替换,这个时间差会致使数据的变化。
线程1从内存位置V中取出A,线程2也从V中取出A,而后线程2经过一些操做将A变成B,而后又把V位置的数据变成A,此时线程1进行CAS操做发现V中仍然是A,操做成功。尽管线程1的CAS操做成功,可是不表明这个过程没有问题。
这个问题相似于幻读问题,经过新增版本号的机制来解决。在这里可使用 AtomicStampedReference 来解决。
经过 AtomicStampedReference 来解决这个问题。
public class SolveABADemo {
static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<>(100,1);
new Thread(()->{
int stamp=atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 版本号:"+stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 版本号:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 版本号:"+atomicStampedReference.getStamp());
},"t1").start();
new Thread(()->{
int stamp=atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 版本号:"+stamp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean ret=atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);
System.out.println(Thread.currentThread().getName()+"\t"+ret
+" stamp:"+atomicStampedReference.getStamp()
+" value:"+atomicStampedReference.getReference());
},"t2").start();
}
}
复制代码
t1 版本号:1
t2 版本号:1
t1 版本号:2
t1 版本号:3
t2 false stamp:3 value:100
复制代码
这个异常也就是并发修改异常,java.util.ConcurrentModificationException。
致使这个异常的缘由,是集合类自己是线程不安全的。
解决方案:
底层使用了 private transient volatile Object[] array;
CopyOnWriteArrayList 采用了写时复制、读写分离的思想。
public boolean add(E e){
final ReentrantLock lock=this.lock;
try{
//旧数组
Object[] elements = getArray();
int len = elements.length;
//复制新数组
Object[] newElements = Arrays.copyOf(elements, len+1);
//修改新数组
newElements[len] = e;
//更改旧数组引用指向新数组
setArray(newElements);
return true;
}finally{
lock.unlock();
}
}
复制代码
添加元素时,不是直接添加到当前容器数组,而是复制到新的容器数组,向新的数组中添加元素,添加完以后将原容器引用指向新的容器。
这样作的好处是能够对该容器进行并发的读,而不须要加锁,由于读时容器不会添加任何元素。
CopyOnWriteArraySet 自己就是使用 CopyOnWriteArrayList 来实现的。
ReentrantLock 能够指定构造函数的 boolean 类型获得公平或非公平锁,默认是非公平锁,synchronized也是非公平锁。
公平锁是多个线程按照申请锁的顺序获取锁,是 FIFO 的。并发环境中,每一个线程在获取锁时先查看锁维护的等待队列,为空则战友,不然加入队列。
非公平锁是指多个线程不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。高并发状况下可能致使优先级反转或者饥饿现象。并发环境中,上来尝试占有锁,尝试失败,再加入等待队列。
可冲入锁指的是同一线程外层函数获取锁以后,内层递归函数自动获取锁。也就是线程能进入任何一个它已经拥有的锁所同步着的代码块。
ReentrantLock 和 synchronized 都是可重入锁。
可重入锁最大的做用用来避免死锁。
自旋锁是指尝试获取锁的线程不会当即阻塞,而是采用循环的方式尝试获取锁。好处是减小线程上下文切换的消耗,缺点是循环时会消耗CPU资源。
实现自旋锁:
public class SpinLockDemo {
//使用AtomicReference<Thread>来更新当前占用的 Thread
AtomicReference<Thread> threadAtomicReference=new AtomicReference<>();
public static void main(String[] args) {
SpinLockDemo demo=new SpinLockDemo();
new Thread(()->{
demo.myLock();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.myUnlock();
},"t1").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
demo.myLock();
demo.myUnlock();
},"t2").start();
}
public void myLock(){
Thread thread=Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t come in");
//若是当前占用的线程为null,则尝试获取更新
while(!threadAtomicReference.compareAndSet(null,thread)){
}
}
public void myUnlock(){
Thread thread=Thread.currentThread();
//释放锁,将占用的线程设置为null
threadAtomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t unlocked");
}
}
复制代码
独占锁:该锁一次只能被一个线程持有,如 ReentrantLock 和 synchronized。
共享锁:该锁能够被多个线程持有。
ReentrantReadWriteLock 中,读锁是共享锁,写锁时独占锁。读读共享保证并发性,读写互斥。
CountDownLatch 的做用是让一些线程阻塞直到另一些线程完成一系列操做后才被唤醒。
CountDownLatch 在初始时设置一个数值,当一个或者多个线程使用 await() 方法时,这些线程会被阻塞。其他线程调用 countDown() 方法,将计数器减去1,当计数器为0时,调用 await() 方法被阻塞的线程会被唤醒,继续执行。
能够理解为,等你们都走了,保安锁门。
CyclicBarrier 是指能够循环使用的屏障,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障,屏障才会开门,被屏障拦截的线程才会继续工做,线程进入屏障经过 await() 方法。
能够理解为,你们都到齐了,才能开会。
信号量用于:
能够理解为,多个车抢停车场的多个车位。当进入车位时,调用 acquire() 方法占用资源。当离开时,调用 release() 方法释放资源。
阻塞队列首先是一个队列,所起的做用以下:
试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其余线程向空的队列中插入新的元素。一样的,试图向已满的阻塞队列中添加新元素的线程一样会被阻塞,直到其余线程从队列中移除元素使得队列从新变得空闲起来并后序新增。
阻塞:阻塞是指在某些状况下会挂起线程,即阻塞,一旦条件知足,被挂起的线程又会自动被唤醒。
优势:BlockingQueue 能帮助咱们进行线程的阻塞和唤醒,而无需关心什么时候须要阻塞线程,什么时候须要唤醒线程。同时兼顾了效率和线程安全。
BlokcingQueue 接口实现了 Queue 接口,该接口有以下的实现类:
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 无 | 无 |
add(e)
会抛出异常IllegalStateException: Queue full
;当队列空,remove()
和element()
会抛出异常NoSuchElementException
offer(e)
会返回 true/false。peek()
会返回队列元素或者null。put(e)
会阻塞直到成功或中断;队列空take()
会阻塞直到成功。class ShareData {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws Exception {
lock.lock();
try {
//判断
while (number != 0) {
condition.await();
}
//干活
number++;
System.out.println(Thread.currentThread().getName() + " produce\t" + number);
//通知唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement()throws Exception{
lock.lock();
try {
//判断
while (number == 0) {
condition.await();
}
//干活
number--;
System.out.println(Thread.currentThread().getName() + " consume\t" + number);
//通知唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/** * 一个初始值为0的变量,两个线程交替操做,一个加1一个减1,重复5次 * 1. 线程 操做 资源类 * 2. 判断 干活 通知 * 3. 防止虚假唤醒机制:判断的时候要用while而不是用if */
public class ProduceConsumeTraditionalDemo {
public static void main(String[] args) {
ShareData data=new ShareData();
new Thread(()->{
for (int i = 0; i < 5 ; i++) {
try {
data.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5 ; i++) {
try {
data.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"B").start();
}
}
复制代码
打印结果
A produce 1
B consume 0
A produce 1
B consume 0
A produce 1
B consume 0
A produce 1
B consume 0
A produce 1
B consume 0
复制代码
public class ProduceConsumeBlockingQueueDemo {
public static void main(String[] args) {
SharedData data=new SharedData(new ArrayBlockingQueue<>(10));
new Thread(()-> {
System.out.println(Thread.currentThread().getName() + "\t生产线程启动");
try {
data.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Producer").start();
new Thread(()-> {
System.out.println(Thread.currentThread().getName() + "\t消费线程启动");
try {
data.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Consumer").start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.stop();
System.out.println("中止");
}
}
class SharedData{
private volatile boolean FLAG=true;
private AtomicInteger atomicInteger=new AtomicInteger();
BlockingQueue<String> blockingQueue=null;
public SharedData(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
public void produce() throws InterruptedException {
String data=null;
boolean ret;
while(FLAG){
data=""+atomicInteger.incrementAndGet();
ret=blockingQueue.offer(data,2L,TimeUnit.SECONDS);
if(ret){
System.out.println(Thread.currentThread().getName()+"\t插入"+data+"成功");
}else{
System.out.println(Thread.currentThread().getName()+"\t插入"+data+"失败");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println("生产结束,FLAG=false");
}
public void consume() throws InterruptedException {
String ret=null;
while(FLAG){
ret=blockingQueue.poll(2L,TimeUnit.SECONDS);
if(null==ret||ret.equalsIgnoreCase("")){
System.out.println(FLAG=false);
System.out.println(Thread.currentThread().getName()+"\t消费等待超时退出");
return;
}
System.out.println(Thread.currentThread().getName() + "\t消费" + ret + "成功");
}
}
public void stop(){
FLAG=false;
}
}
复制代码
使用阻塞队列+原子类+volatile变量的方式。 打印结果以下:
java.util.concurrent.ArrayBlockingQueue
Producer 生产线程启动
Consumer 消费线程启动
Producer 插入1成功
Consumer 消费1成功
Producer 插入2成功
Consumer 消费2成功
Producer 插入3成功
Consumer 消费3成功
中止
生产结束,FLAG=false
false
Consumer 消费等待超时退出
复制代码
tryLock(long timeout, TimeUnit unit)
,另外一种是lockInterruptibly()
放代码块中,调用interrupt()
方法进行中断。class ShareData{
private int number=1;
private Lock lock=new ReentrantLock();
public void printA(){
lock.lock();
Condition conditionA=lock.newCondition();
try{
while(number!=1){
conditionA.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
number=2;
conditionA.signal();
}catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
Condition conditionB=lock.newCondition();
try{
while(number!=2){
conditionB.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
number=3;
conditionB.signal();
}catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
Condition conditionC=lock.newCondition();
try{
//判断
while(number!=3){
conditionC.await();
}
//干活
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
number=1;
//通知
conditionC.signal();
}catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ShareData data=new ShareData();
new Thread(() -> data.printA(),"A").start();
new Thread(() -> data.printB(),"B").start();
new Thread(() -> data.printC(),"C").start();
}
}
复制代码
Thread的构造函数中并无传入 Callable 的方式,可是能够传入 Runnable 接口: Thread thread=new Thread(Runnable runnable, String name);
。为了使用 Callable 接口,咱们须要使用到 FutureTask 类。 FutureTask 类实现了 RunnableFuture 这一接口,而 RunnableFutre 又是 Future 的子接口,所以 FutureTask 能够做为参数使用上述的 Thread 构造函数。同时, FutureTask 自己构造函数能够传入 Callable 。
class MyThread implements Callable<Integer>{
@Override
public Integer call() {
System.out.println("come in callable");
return 2019;
}
}
class Main{
public static void main(String [] args){
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
Thread t1=new Thread(futureTask,"A");
}
}
复制代码
线程池有七大参数:
public ThreadPoolExecutor( int corePoolSize,//线程池常驻核心线程数 int maximumPoolSize,//线程池能容纳同时执行最大线程数 long keepAliveTime,//多余的空闲线程的存活时间,当前线程池线程数量超过core,空闲时间达到keepAliveTime,多余空闲线程会被销毁直到只剩下core个 TimeUnit unit, BlockingQueue<Runnable> workQueue,//被提交还没有被执行的任务队列 ThreadFactory threadFactory,//建立线程的线程工厂 RejectedExecutionHandler handler//拒绝策略 ) {...}
复制代码
处理流程以下:
在 JDK 中有四种内置的拒绝策略,均实现了 RejectedExecutionHandler 接口。
建立固定容量的线程池,控制最大并发数,超出的线程在队列中等待。
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
复制代码
其中 corePoolSize 和 maximumPoolSize 值是相等的,而且使用的是 LinkedBlockingQueue。
适用于执行长期的任务,性能比较高。
建立了一个单线程的线程池,只会用惟一的工做线程来执行任务,保证全部任务按照顺序执行。
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
复制代码
其中 corePoolSize 和 maximumPoolSize 都设置为1,使用的也是 LinkedBlockingQueue。
适用于一个任务一个任务执行的场景。
建立了一个可缓存的线程池,若是线程池长度超过处理须要,能够灵活回收空闲线程,没有能够回收的,则新建线程。
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
复制代码
设置 corePoolSize 为0, maximumPoolSize 设置为 Integer.MAX_VALUE,使用的是 SynchronousQueue。来了任务就建立线程执行,线程空闲超过60秒后销毁。
适用于执行不少短时间异步的小程序或者负载比较轻的服务器。
在阿里巴巴Java开发手册中有以下规定:
Runtime.getRuntime().availableProcessors()
获取当前设备的CPU个数。
CPU 核心数 + 1
CPU核心数 * 2
,或者采用 CPU 核心数 / (1 - 阻塞系数)
,阻塞系数在0.8 ~ 0.9之间死锁是指两个或两个以上的进程在执行过程当中,由于争夺资源形成的互相等待的现象。
死锁须要满族的四大条件以下:
产生死锁的主要缘由有:
class HoldLockThread implements Runnable{
private String lock1;
private String lock2;
public HoldLockThread(String lock1, String lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
synchronized (lock1){
System.out.println(Thread.currentThread().getName()+"\t持有"+lock1+"\t尝试获取"+lock2);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+"\t持有"+lock1+"\t尝试获取"+lock2);
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
String lockA="lockA";
String lockB="lockB";
new Thread(new HoldLockThread(lockA,lockB),"Thread1").start();
new Thread(new HoldLockThread(lockB,lockA),"Thread2").start();
}
}
复制代码
输出以下结果,程序并无终止。
Thread2 持有lockB 尝试获取lockA
Thread1 持有lockA 尝试获取lockB
复制代码
使用 jps ,相似于 linux 中的 ps 命令。
在上述 java 文件中,使用 IDEA 中的 open In Terminal,或者在该文件目录下使用 cmd 命令行工具。
首先使用 jps -l
命令,相似于ls -l
命令,输出当前运行的 java 线程,从中能得知 DeadLockDemo 线程的线程号。
而后,使用jstack threadId
来查看栈信息。输出以下:
Java stack information for the threads listed above:
===================================================
"Thread2":
at interview.jvm.deadlock.HoldLockThread.run(DeadLockDemo.java:22)
- waiting to lock <0x00000000d6240328> (a java.lang.String)
- locked <0x00000000d6240360> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"Thread1":
at interview.jvm.deadlock.HoldLockThread.run(DeadLockDemo.java:22)
- waiting to lock <0x00000000d6240360> (a java.lang.String)
- locked <0x00000000d6240328> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
复制代码