文章目录
- 1、谈谈你对volatile的理解?
- 2、JMM(Java内存模型)
- 3、什么是CAS(compareAndSet()比较并交换)?
- 4、原子类AtomicInteger的ABA问题?原子更新引用?
- 5、ArrayList是线程不安全的,请编写一个不安全的案例并给出解决方案。
- 6、公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁。
- 7、CountDownLath/CyclicBarrier/Semaphore使用过吗?
- 8、阻塞队列BlockingQueue(接口)
- 9、synchronized和Lock有什么区别?用新的lock有什么好处?举例说明
- 10、线程通信之生产者消费者阻塞队列版
- 11、Callable接口
- 12、线程池使用过吗?ThreadPoolExecutor谈谈你的理解?
- 13、生产上如何设置合理的线程池参数
- 14、死锁编码及定位分析
1、谈谈你对volatile的理解?
volatile是Java虚拟机提供的轻量级的同步机制(详见JMM)
- 保证可见性
- 不保证原子性
- 静止指令重排
2、JMM(Java内存模型)
Java内存模型本身是一种抽象的概念并不真实存在,他描述的是一组规则或者规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享内存变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称之为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
-
JMM可见性
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存中进行操作后再写回到主内存中的,即工作内存与主内存同步延迟现象就造成了可见性问题。
-
JMM原子性
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整,要么同时成功,要么同时失败。
volatile不保证原子性
-
VolatileDemo代码演示可见性
- 假如int number = 0; 如果number变量之前没有添加volatile关键字修饰
/**
* @author YLY
* @ClassName Test
* @Date 2020/4/29
* @Version 1.0.2
* 1、验证volatile的可见性
* 1.1 假如int number = 0; 如果number变量之前没有添加volatile关键字修饰
*/
class MyData {
int number = 0;
public void addT060() {
this.number = 60;
}
}
public class Test {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
}
myData.addT060();
System.out.println(Thread.currentThread().getName() + "\t update number value:"+myData.number);
}, "AAA").start();
while (myData.number==0){
}
System.out.println(Thread.currentThread().getName() + "\t mission is over ,get number value"+myData.number);
}
}
现象:系统3秒后线程AAA将主内存中的number值修改为60,但是main线程对其不可见,所以一直等待循环,直到number值不在等于零。
将myData修改成下面代码
class MyData {
volatile int number = 0;
public void addT060() {
this.number = 60;
}
}
现象:系统3秒后线程AAA将主内存中的number值修改为60,main线程对number具有可见性,不满足while的循环条件,即跳出循环结束程序。
- VolatileDemo代码演示不保证原子性
public class Test {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <=20 ; i++) {
new Thread(()->{
for (int j = 1; j <=1000 ; j++) {
myData.addPlusPlus();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程全部计算完毕,再用main线程取得最终的结果
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t finally number value :"+myData.number);
}
}
class MyData {
volatile int number = 0;
public void addPlusPlus(){
number++;
}
}
现象:每次执行完之后都会少于20000,即number++ 在多线程环境下是非线程安全的,会出现数据丢失的情况。
-
JMM有序性
计算机在执行程序的时候,编译器和处理器常常会对指令做重排,在多线程环境下能否保证一致性是无法确定的,结果无法预测。可以使用synchronized或者volatile关键字解决,他们都可以使一个线程修改后的变量立即对其他线程可见。而且volatile还有一个作用就是静止指令重排。class ReSortSeqDemo{ int a = 0; boolean flag = false; public void method01(){ a = 1; flag = true; } /** * 多线程环境中线程交替执行,由于编译器优化重排的存在, * 两个线程中使用的变量能保证一致性是无法确定的,结果无法预测 */ public void method02(){ if (flag){ a = a+5; System.out.println("******retValue: "+a); } } }
-
多线程下单例模式推荐使用双重检测保证效率和线程安全
-
/** * @author yly * @ClassName IdlerSingleton3 懒汉式3 双重检查(加入synchronized,和volatile线程安全,同时保证效率,推荐使用) * @Date 2020/2/8 16:04 * @Version 1.0 **/ public class IdlerSingleton3 { private IdlerSingleton3() { } private static volatile IdlerSingleton3 Instance; public static IdlerSingleton3 getInstance() { if (Instance == null) { synchronized (IdlerSingleton3.class) { if (Instance == null) { Instance = new IdlerSingleton3(); } } } return Instance; } }
-
3、什么是CAS(compareAndSet()比较并交换)?
compareAndSet(int expectedValue, int newValue)
compareAndSet第一个参数是期望值,第二个参数是修改值,
假如初始化值为5,初始化值和期望值相等,则重新赋值为2019,并返回true
假如初始化值为5,初始化值和期望值不相等,则不重新赋值,返回false
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data "+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2020)+"\t current data "+atomicInteger.get());
}
}
结果:
true current data 2019
false current data 2019
- CAS底层原理(jdk11)
public class AtomicInteger extends Number implements Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe U = Unsafe.getUnsafe();//初始化Unsafe
private static final long VALUE;//内存地址
private volatile int value;//初始化值
public final boolean compareAndSet(int expectedValue, int newValue) {
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
}
//实际上底层调用的是Unsafe.class下的compareAndSetInt(Object var1, long var2, int var4, int var5)
public final class Unsafe {
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object var1, long var2, int var4, int var5);
}
Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于 jdk.internal.misc包中,(Unsafe中jdk8在sun.misc包中),其内部方法操作可以向C一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新至B,当且仅当预期值A和内存值V相等时,将内存值V修改成B,否则什么都不做。
- CAS引发的缺点:
- 循环时间长开销大:如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
- 只能保证一个共享变量的原子操作。
- 引发出来ABA问题
4、原子类AtomicInteger的ABA问题?原子更新引用?
ABA问题(狸猫换太子):CAS算法实现一个重要的前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么这个时间差类会导致数据的变化,比如说一个线程one从内存位置V中取出A,这个时候另一个线程two也从内存中取出A,并且线程two进行一些操作将值变化B,然后线程two又将V位置的数据变成A,这个时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但是并不代表这个过程就是没有问题的。
- 原子引用
class User{
private String username;
private int age;
public User(String username, int age) {
this.username = username;
this.age = age;
}
//忽略getter/setter/toString方法
public static void main(String[] args) {
User zhangsan = new User("张三", 15);
User lisi = new User("李四", 20);
AtomicReference<User> userAtomicReference = new AtomicReference<>();
userAtomicReference.set(zhangsan);
System.out.println(userAtomicReference.compareAndSet(zhangsan, lisi)+"\t"+userAtomicReference.get().toString());
System.out.println(userAtomicReference.compareAndSet(zhangsan, lisi)+"\t"+userAtomicReference.get().toString());
}
}
//结果时
true User{username='李四', age=20}
false User{username='李四', age=20}
- ABA问题的解决方案:原子引用+修改版本号(类似于时间戳)
/**
* @author YLY
* @ClassName ABADemo
* @Date 2020/4/30
* @Version 1.0.2
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("==============以下是ABA问题的产生=================");
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019)+"\t"+atomicReference.get());
},"t2").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==============以下是ABA问题的解决=================");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第1次版本号:"+stamp);
//暂停1秒钟t3线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t第2次版本号:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t第3次版本号:"+atomicStampedReference.getStamp());
},"t3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t第1次版本号:"+stamp);
//暂停3秒钟t4线程,保证上面的t3线程完成一次ABA操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName()+"\t修改成功与否:"+result+"当前版本号为:"+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"\t当前实际最新值为:"+atomicStampedReference.getReference());
},"t4").start();
}
}
//结果为
==============以下是ABA问题的产生=================
true 2019
==============以下是ABA问题的解决=================
t3 第1次版本号:1
t4 第1次版本号:1
t3 第2次版本号:2
t3 第3次版本号:3
t4 修改成功与否:false 当前版本号为:3
t4 当前实际最新值为:100
5、ArrayList是线程不安全的,请编写一个不安全的案例并给出解决方案。
线程不安全示例:
public class ContainerNotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i <300 ; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
//抛出java.util.ConcurrentModificationExceptionJava高并发修改异常
-
导致原因
并发争抢修改导致,参考我们的花名册签名情况。一个人正在写入,另一个同学过来抢夺,导致数据不一致异常,并发修改异常。
CopyOnWriteArrayList写时复制,CopyOnWrite容器即写时复制,往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行拷贝,复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里面添加元素,添加完元素之后,再将原容器的引用指向新的容器 setArray(newElements);这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
jdk1.8源码:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.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();
}
}
jdk11源码:
public boolean add(E e) {
synchronized(this.lock) {
Object[] es = this.getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
this.setArray(es);
return true;
}
}
-
解决方案:
-
1、new Vector<>(); 2、Collections.synchronizedList(new ArrayList<>()); 3、new CopyOnWriteArrayList<>();
- 线程不安全类HashSet高并发异常也是java.util.ConcurrentModificationException
-
-
解决方案
1、Collections.synchronizedSet(new HashSet<>());
2、Set<String> set = new CopyOnWriteArraySet<>();
其中CopyOnWriteArraySet时换汤不换药,底层是
new CopyOnWriteArrayList<E>();
//HashSet的底层是HashMap,初始值是16,负载因子是0.75
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//HashSet的add()方法调用HashMap的put方法,其中add的值是put的key,value是一个new Object()的常量。
-
线程不安全类HashMap高并发异常java.util.ConcurrentModificationException
-
解决方案
1、Collections.synchronizedMap(new HashMap<>());
2、new ConcurrentHashMap<>();
- 测试小练习
public class TestTransFerValue {
public void changeValue1(int age){
age = 30;
}
public void changeValue2(Person person){
person.setPersonName("xxxx");
}
public void changeValue3(String name){
name = "xxx";
}
public static void main(String[] args) {
TestTransFerValue transFerValue = new TestTransFerValue();
int age = 20;
transFerValue.changeValue1(age);
System.out.println("age--------"+age);
Person person = new Person("aaaaa");
transFerValue.changeValue2(person);
System.out.println("personName-----"+person.getPersonName());
String str = "abc";
transFerValue.changeValue3(str);
System.out.println("str -------"+ str);
}
}
class Person{
private int age;
private String personName;
public Person(String personName) {
this.personName = personName;
}
public int getAge() {
return age;
}
结果:
age--------20
personName-----xxxx
str -------abc
6、公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁。
- 公平和非公平锁
- 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
- 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。
并发包中ReentrantLock的创建可以指定构造函数的Boolean类型来得到公平锁或者非公平锁,默认是非公平锁,非公平锁的优点在于吞吐量比公平锁大,对于synchronization而言,也是一种非公平锁。
- 公平锁和非公平锁的区别
- 公平锁:在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后按照FIFO的规则从队列中取到自己。
- 非公平锁:非公平锁上来周直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
- 可重入锁(递归锁)
可重入锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,即线程可以进入任何一个它已经拥有的锁所同步着的代码块。synchronization/ReentrantLock就是一个典型的可重入锁,可重入锁最大的作用是避免死锁。
public synchronized void method1(){
method2();
}
public synchronized void method2(){
}
public class SynchronizedLockDemo {
public static void main(String[] args) throws Exception{
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"t2").start();
}
}
class Phone {
public synchronized void sendSMS() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
sendEmail();
}
public synchronized void sendEmail() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail()");
}
}
public class ReentrantLockDemo {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
Thread t3 = new Thread(phone,"t3");
Thread t4 = new Thread(phone,"t4");
t3.start();
t4.start();
}
}
class Phone implements Runnable {
@Override
public void run() {
get();
}
Lock lock = new ReentrantLock();
public void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t invoked get()");
set();
} finally {
lock.unlock();
}
}
public void set() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t invoked set()");
} finally {
lock.unlock();
}
}
}
lock.lock();和lock.unlock();需要配对,无论加几次锁都可以运行成功。
- 自旋锁
尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
自定义自旋锁:
public class SpinLockDemo {
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock();
}, "AA").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
spinLockDemo.myLock();
spinLockDemo.myUnLock();
}, "BB").start();
}
AtomicReference<Thread> atomicThread = new AtomicReference<>();
public void myLock() {
Thread thread =Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in myLock()");
while (!atomicThread.compareAndSet(null, thread)){
}
}
public void myUnLock() {
Thread thread = Thread.currentThread();
atomicThread.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t invoked myLock()");
}
}
- 独占锁和共享锁
- 独占锁:指该锁一次只能有一个线程所持有,对synchronization和ReentrantLock而言都是独占锁
- 共享锁:指该锁可被多个线程所持有
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁,读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
/**
* @author YLY
* @ClassName ReentrantReadWriteLockDemo
* @Date 2020/5/6
* @Version 1.0.2
* 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行
* 但是
* 如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或者写
* 小总结:
* 读---读 能共存
* 读---写 不能共存
* 写---写 不能共存
* 写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断
*/
public class ReentrantReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i < 5; i++) {
final int timeInt = i;
new Thread(() -> {
myCache.put(timeInt + "", timeInt + "");
}, String.valueOf(i)).start();
}
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 1; i < 5; i++) {
final int timeInt = i;
new Thread(() -> {
myCache.get(timeInt + "");
}, String.valueOf(i)).start();
}
}
}
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成:");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
public void get(String key) {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
}
7、CountDownLath/CyclicBarrier/Semaphore使用过吗?
- CountDwonLatch用来等待一个或者多个线程完成操作,常常用来控制线程等待,它可以让某个线程等待,直到其他线程执行任务结束,才开始执行。CountDwonLatch只要有两个方法,当一个或多个线程调用await()方法时,调用线程会被阻塞,其他线程调用countDown()方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用await()方法被阻塞的线程会被唤醒,继续执行。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"国\t被灭 ");
countDownLatch.countDown();
},CountryEnum.forEachEnum(i).getRetMessage()).start();
}
countDownLatch.await();
System.out.println("秦国 \t一统天下 ");
}
private static void closeDoor() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t 号同学离开教室");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t 同学走完班长关门");
}
}
枚举:
public enum CountryEnum {
ONE(1,"齐"),
TWO(2,"楚"),
THREE(3,"燕"),
FOUR(4,"韩"),
FIVE(5,"赵"),
SIX(6,"魏");
private Integer retCode;
private String retMessage;
CountryEnum(Integer retCode, String retMessage) {
this.retCode = retCode;
this.retMessage = retMessage;
}
public Integer getRetCode() {
return retCode;
}
public void setRetCode(Integer retCode) {
this.retCode = retCode;
}
public String getRetMessage() {
return retMessage;
}
public void setRetMessage(String retMessage) {
this.retMessage = retMessage;
}
public static CountryEnum forEachEnum(int index){
CountryEnum[] values = CountryEnum.values();
for (CountryEnum value : values) {
if (index==value.getRetCode()){
return value;
}
}
return null;
}
}
closeDoor()运行结果:
2 号同学离开教室
6 号同学离开教室
1 号同学离开教室
3 号同学离开教室
4 号同学离开教室
5 号同学离开教室
main 同学走完班长关门
main运行结果
齐国 被灭
楚国 被灭
燕国 被灭
魏国 被灭
韩国 被灭
赵国 被灭
秦国 一统天下
- CyclicBarrier通俗易懂为(机器7颗龙珠才能召唤神龙)。它是可循环使用的屏障,它要做的事情是,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的wait()方法。
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("集齐龙珠召唤神龙");
});
for (int i = 1; i <=7 ; i++) {
final int tempInt = i;
new Thread(()->{
System.out.println("收集到第:"+tempInt+"颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
结果为:
收集到第:1颗龙珠
收集到第:5颗龙珠
收集到第:6颗龙珠
收集到第:7颗龙珠
收集到第:2颗龙珠
收集到第:3颗龙珠
收集到第:4颗龙珠
集齐龙珠召唤神龙
- Semaphore信号灯,主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
public class SemaphoreDemo {
public static void main(String[] args) {
/**
* 3个车位
*/
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
try {
semaphore.acquire();//抢占车位
System.out.println(Thread.currentThread().getName()+"\t 抢到车位");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"\t 停车3秒后离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();//离开车位,通知其他线程继续抢占
}
},String.valueOf(i)).start();
}
}
}
结果展示:
1 抢到车位
2 抢到车位
6 抢到车位
1 停车3秒后离开车位
3 抢到车位
2 停车3秒后离开车位
4 抢到车位
6 停车3秒后离开车位
5 抢到车位
3 停车3秒后离开车位
4 停车3秒后离开车位
5 停车3秒后离开车位
8、阻塞队列BlockingQueue(接口)
阻塞队列有两种情况:
- 当阻塞队列是空的时候,从队列中获取元素的操作将会被阻塞
- 当阻塞队列是满的时候,往队列里添加元素的操作将会被阻塞
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒。
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
public class BlockingQueueDemo {
public static void main(String[] args) {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.add("1");
blockingQueue.add("1");
blockingQueue.add("1");
blockingQueue.add("1");
}
}
//设置的ArrayBlockingQueue的初始值大小为3,如果add()数量超过3时会抛出**Exception in thread "main" //java.lang.IllegalStateException: Queue full**异常
当阻塞队列满时,再往队列里面add插入元素会抛出IllegalStateException: Queue full异常
public class BlockingQueueDemo {
public static void main(String[] args) {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.add("a");
blockingQueue.add("b");
blockingQueue.add("c");
blockingQueue.element();//检查队列是否为空,若不为空,则返回队列首部元素
blockingQueue.remove();
blockingQueue.remove();
blockingQueue.remove();
blockingQueue.remove();
}
}
当阻塞队列空时,再往队列里remove移除元素会抛出NoSuchElementException异常
public class BlockingQueueDemo {
public static void main(String[] args) {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("A"));
System.out.println(blockingQueue.offer("A"));
System.out.println(blockingQueue.offer("A"));
System.out.println(blockingQueue.offer("A"));
}
}
结果
true
true
true
false
使用offer()添加元素,若超出队列长度则返回false。
public class BlockingQueueDemo {
public static void main(String[] args) {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("A"));
System.out.println(blockingQueue.offer("B"));
System.out.println(blockingQueue.offer("C"));
System.out.println(blockingQueue.offer("D"));
System.out.println(blockingQueue.peek());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
}
}
结果:
true
true
true
false
A
A
B
C
nul
插入成功返回true 插入失败返回false,peek()如果队列有值,返回队列首部元素,如果没有返回null,poll()取出队列,如果队列空时返回null.
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.put("A");
blockingQueue.put("A");
blockingQueue.put("A");
System.out.println("##################");
blockingQueue.put("A");
blockingQueue.take();
blockingQueue.take();
blockingQueue.take();
}
}
put,当队列满时,会发生阻塞,程序会一直等待,take当队列空时会发生阻塞,程序会一直等待。
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.offer("A",2, TimeUnit.SECONDS);
blockingQueue.offer("A",2, TimeUnit.SECONDS);
blockingQueue.offer("A",2, TimeUnit.SECONDS);
System.out.println("#############################");
blockingQueue.offer("A",2, TimeUnit.SECONDS);
}
}
当队列满时,会发生阻塞,在设置2秒之后,程序会超时退出
- LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常要高于ArrayBlockingQueue。
- SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue
public class SynchronousQueueDemo {
public static void main(String[] args) {
SynchronousQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t put 1");
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName() + "\t put 2");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName() + "\t put 3");
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "AAA").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "\t take "+blockingQueue.take());
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "\t take "+blockingQueue.take());
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "\t take "+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "BBB").start();
}
}
//第一个队列插入和第二个队列插入中间需要等待5秒钟
9、synchronized和Lock有什么区别?用新的lock有什么好处?举例说明
- 原始构成
- synchronized是关键字属于JVM层面
- monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象只有在同步块或者方法中才能调wait/notify等方法,monitorexit退出)
- Lock是具体类(java.util.concurrent.locks.Lock)是API层面的锁
- synchronized是关键字属于JVM层面
- 使用方法
- synchronized 不需要用户去手动释放锁,当synchronized代码执行完成后系统会自动让线程释放对锁的占用
- ReentrantLock 需要用户去手动释放锁,若没有主动释放锁,就有可能导致出现死锁现象,需要lock()和unlock()方法配合try/finally语句块来完成。
- 等待是否可中断
- synchronized 不可中断,除非抛出异常或者正常运行完成
- ReentrantLock 可以中断
- 设置超时方法tryLock(long timeout,TimeUit unit)
- lockInterruptibly()放代码块中,调用interrupt()方法可以中断
- 加锁是否公平
- synchronized 是非公平锁
- ReentrantLock 两者都可以,默认非公平锁,构造方法可以传入Boolean值,true为公平锁,false为非公平锁
- 锁绑定多个条件condition
- synchronized 没有
- ReentrantLock 用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized 要么随机唤醒一个线程要么唤醒全部线程。
使用Condition精准唤醒线程示例:
题目:多线程之间按顺序调用,实现A->B->C三个线程启动要求如下:
AA打印5次,BB打印10次,CC打印15次
紧接着
AA打印5次,BB打印10次,CC打印15次
打印10轮
class ShareResource {
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5() {
lock.lock();
try {
while (number != 1) {
c1.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
number = 2;
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (number != 2) {
c2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
number = 3;
c3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (number != 3) {
c3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
number = 1;
c1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class synchronizedAndReentrantLockDemo {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(()->{
for (int i = 1; i <=10 ; i++) {
shareResource.print5();
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <=10 ; i++) {
shareResource.print10();
}
},"BB").start();
new Thread(()->{
for (int i = 1; i <=10 ; i++) {
shareResource.print15();
}
},"CC").start();
}
}
10、线程通信之生产者消费者阻塞队列版
package com.yangluyao.test.blockingqueue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author YLY
* @ClassName ProdConsumer_BlockQueueDemo P44 线程通信之生产者消费者阻塞队列版
* @Date 2020/5/21
* @Version 1.0.2
*/
class MyResource {
private volatile boolean FLAG = true;
private AtomicInteger atomicInteger = new AtomicInteger();
BlockingQueue<String> blockingQueue = null;
public MyResource(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
System.out.println(blockingQueue.getClass().getName());
}
public void myprod() throws Exception {
String data = null;
boolean retValue;
while (FLAG) {
data = atomicInteger.incrementAndGet() + "";
retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
if (retValue) {
System.out.println(Thread.currentThread().getName() + "\t 插入队列" + data + "成功");
} else {
System.out.println(Thread.currentThread().getName() + "\t 插入队列" + data + "失败");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName() + "\t 大老板叫停了,表示flag=false,生产动作结束");
}
public void myConsumer() throws Exception {
String result = null;
boolean retValue;
while (FLAG) {
result = blockingQueue.poll(2L, TimeUnit.SECONDS);
if (result == null || result.equalsIgnoreCase("")) {
FLAG = false;
System.out.println(Thread.currentThread().getName() + "\t 超过两秒钟没有取到蛋糕");
System.out.println();
System.out.println();
System.out.println();
return;
}
System.out.println(Thread.currentThread().getName() + "\t 消费队列" + result + "成功");
}
}
public void stop() throws Exception {
this.FLAG = false;
}
}
/**
* 整合volatile/CAS/atomicInteger/BlockQueue/线程交互/原子引用
* @author 81509
*/
public class ProdConsumer_BlockQueueDemo {
public static void main(String[] args) throws Exception {
MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 生产线程启动");
System.out.println();
System.out.println();
try {
myResource.myprod();
System.out.println();
System.out.println();
} catch (Exception e) {
e.printStackTrace();
}
}, "Prod").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");
System.out.println();
System.out.println();
try {
myResource.myConsumer();
myResource.myConsumer();
myResource.myConsumer();
} catch (Exception e) {
e.printStackTrace();
}
}, "Consumer").start();
TimeUnit.SECONDS.sleep(5);
System.out.println();
System.out.println();
System.out.println();
System.out.println();
System.out.println("5秒钟时间到,大老板叫停,活动结束");
myResource.stop();
}
}
11、Callable接口
之前多线程实现 Runnable 接口是执行工作独立任务,没有返回值,而Callable接口可以在任务完成之后将期望值返回,实现Callable接口,重写call()方法。
/**
* @author YLY
* @ClassName CallableDemo
* @Date 2020/5/22
* @Version 1.0.2
*/
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "**************** come Callable ");
TimeUnit.SECONDS.sleep(3);
return 1024;
}
}
/**
* Callable线程是异步重启线程,如果要开启多个Callable线程,需要创建多个FutureTask
* @author 81509
*/
public class CallableDemo {
public static void main(String[] args) throws Exception {
//FutureTask(Callable<V> callable)
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
FutureTask<Integer> futureTask2 = new FutureTask<>(new MyThread());
new Thread(futureTask, "AA").start();
new Thread(futureTask2, "BB").start();
System.out.println(Thread.currentThread().getName() + "**************** come main ");
int result = 100;
//futureTask.isDone()表示该线程是否执行完毕
while (!futureTask.isDone()){
}
//futureTask.get() 获得Callable线程的计算结果,
// 如果没有计算完成就要强行获取Callable值,会导致堵塞,直到计算完成
System.out.println("************************ result: " + (result+futureTask.get()));
}
}
12、线程池使用过吗?ThreadPoolExecutor谈谈你的理解?
-
为什么使用线程池,优势是什么?
- 线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,则超出的线程需要线程等待,等其他线程执行完毕,再从队列中取出任务来执行
- 主要特点为线程复用:控制最大并发数,管理线程
- 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
-
线程池如何使用?
-
架构说明?
- Java中线程池是通过Executor框架实现的,该框架中使用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
-
常用方式
-
Executors.newFixedThreadPool(int) 一池固定数量线程 执行长期任务,性能好很多
-
主要特点:1、创建一个定长线程池,可控制线程最大并发数。超出的线程会在队列中等待
-
2、newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用LinkedBlockingQueue
-
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
-
-
Executors.newSingleThreadExecutor() 一池一线程 一个任务一个任务执行的场景
-
主要特点:1、创建一个单线程化的线程池,她只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
-
2、newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用LinkedBlockingQueue
-
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
-
-
Executors.newCachedThreadPool() 一池多线程 适用:执行很多短期异步的小程序或者负载较轻的服务器。
-
主要特点:1、创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
-
2、newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就新建线程运行,当线程空闲超过60秒,就销毁线程。
-
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
-
-
-
-
线程池7大参数介绍
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
- corePoolSize :线程池中常驻核心线程数
- 创建线程之后,当请求任务来了之后,就会安排池中的线程去执行请求任务,近似理解为今当值线程
- 当线程池中的线程数目达到corePoolSize 后,就会把到达的任务放入到缓存队列中
- maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
- keepAliveTime:多余的空闲线程的存活时间,当前线程池数量超过corePoolSize 时,当空闲时间达到
keepAliveTime值时,多余空闲线程会被销毁直到剩下corePoolSize 个线程为止。
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
- handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何拒绝
-
线程池底层工作原理
- 在创建了线程池后,等待提交过来的任务请求
- 当调用execute()方法添加一个请求任务时,线程池会做如下判断
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或者等于corePoolSize,那么将这个任务放入队列
- 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
- 如果队列满了且正在运行的线程数量大于或者等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
- 当一个线程完成任务时,他会从队列中取下一个任务来执行
- 当一个线程无事可做超过一定时间(keepAliveTime)时,线程池会判断:
- 如果当前运行的线程数大于corePoolSize ,那么这个线程就被停掉
- 线程池的所有任务完成后它最终会收缩到corePoolSize的大小
13、生产上如何设置合理的线程池参数
-
拒绝策略
- AbortPolicy(默认):直接抛出 RejectedExecutionException 异常阻止系统正常运行。最大接受线程数为:最大线程数+任务队列数
- CallerRunsPolicy :调用者运行一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用,从而降低新任务的流量
- DiscardOldestPolicy :抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy :直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种方案
-
工作中使用单一的/固定数的/可变的三种创建线程池的方法,用哪个比较多? 一个也不用
-
合理配置线程池
- cpu密集型:配置尽可能少的线程数量 公式为:CPU核心数+1个线程的线程池
- IO密集型:
- IO密集型任务线程并不是一直执行任务,尽可能配置多个线程 CPU核数*2
- 需要大量IO,即任务需要大量的IO,即大量的阻塞,CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间
14、死锁编码及定位分析
死锁是指两个或者两个以上的进程在执行过程中因争夺资源而造成的一种互相等待的现象,若无外力干涉那么它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就会很低,否则就会因争夺有限的资源而陷入死锁。
-
造成死锁的原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
class HoldLockThread implements Runnable{ private String lockA; private String lockB; public HoldLockThread(String lockA, String lockB) { this.lockA = lockA; this.lockB = lockB; } @Override public void run() { synchronized (lockA){ System.out.println(Thread.currentThread().getName()+"\t 自己持有:"+lockA+"\t 尝试获得"+lockB ); try { TimeUnit.SECONDS.sleep(2); }catch (Exception e){ e.printStackTrace(); } synchronized (lockB){ System.out.println(Thread.currentThread().getName()+"\t 自己持有:"+lockB+"\t 尝试获得"+lockA ); } } } } /** * @author YLY * @ClassName deadLockDemo 死锁demo * @Date 2020/5/23 * @Version 1.0.2 */ public class DeadLockDemo { public static void main(String[] args) { String lockA = "lockA"; String lockB = "lockB"; new Thread(new HoldLockThread(lockA,lockB),"ThreadAAA").start(); new Thread(new HoldLockThread(lockB,lockA),"ThreadBBB").start(); } }
-
定位分析