JavaのJUC并发编程(上)
1. 基础知识
多线程的优点:
-
提高程序性能,高并发系统
-
提高程序吞吐量,异步+回调登生产需求
1.1. start开启线程
openJDK下载地址:jdk8/jdk8/jdk: 687fd7c7986d / (java.net)
java线程通过start的方法启动,调用底层native的start0方法。主要的三个底层C++代码为thread.cpp, jvm.cpp和Thread.c
Thread.java对应的是Thread.c,start0其实就是JVM_startThread,jvm.cpp中有实现
1.2. 多线程相关概念
-
并发concurrent:一台处理器同时处理多个任务,同一时刻只有一个任务在执行
-
并行parallel:多台处理器同时处理多个任务,同一时刻处理器在执行相互独立的任务
-
线程
-
进程
-
管程monitor:就是锁,一种同步机制。保证同一时刻只能有一个线程访问临界资源。Monitor对象与java对象一同创建和销毁
-
用户线程:系统的工作线程
-
守护线程:为其他线程提供服务的,比如GC线程。可以使用**thread.isDaemon()**方法判断某一个线程是否是守护线程
2. CompletableFuture
2.1. FutureTask
Future接口(FutureTask实现类)定义**操作异步任务执行的一些方法**
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {}
Future接口是Java5中引入的,提供一种并行计算的能力。
如果主线程需要执行很耗时的任务,可通过Future接口将任务放入到异步线程中去执行。主线程处理完其他任务结束后,可通过Future获取计算结果。异步多线程任务执行的特定:多线程,有返回值,异步任务
Runnable Callable Future和FutureTask实现类
通过Callable接口和FutureTask
创建Callable接口的实现类 ,并实现Call方法。创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值**,使用FutureTask对象作为Thread对象的target创建并启动线程,调用FutureTask对象的get()来获取子线程执行结束的返回值**。通过Callable接口来创建线程的细节:
- 实现callable接口的线程可以防止线程阻塞,但是注意该线程中task的get方法需要在放在最后,防止主线程被阻塞
- 在创建线程时,如果传入的futureTask对象是同一个,只会执行一次线程中的方法
由于Thread的构造方法中没有以Callable接口为参数的,而Callable接口中存在一个子接口RunnableFuture,该子接口的实现类有一个FutureTask,并且在该类的构造方法中存在FutureTask(Callable callable)。因此可以使用该类去包装Callable接口才能作为Thread的target来创建线程:
Thread(Runnable target, String name) ->
Thread(RunnableFuture target, String name) ->
Thread(FutureTask(Callable) target, String name)
存在的弊端
- get方法返回结果,一旦任务没完成会进入到阻塞状态,程序无法继续执行
- 使用get方法设置超时时间,虽然会中断阻塞状态,但是会抛出
TimeException
- 使用isDone()方法进行while(true)轮询,会消费无谓的CPU资源,也不能及时得到结果
2.2. CompletableFuture
将多个异步任务的结果组合起来,后一个异步任务的结果依赖前一个异步任务的结果,这几个异步任务计算相互独立,同时后面又依赖前一个处理的结果
对于2.1.中的问题,希望可以通过传入回调函数,在Future结束时自动调用该回调函数,不再需要一直等待结果。CompletableFuture提供一种类似于观察者模式的机制,可以让任务执行完成后通知监听的一方
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {}
-
CompletionStage代表异步计算中的某一个阶段,一个阶段完成后可能会触发另一个阶段
-
一个阶段的计算执行可以是一个Function,Consumer或者是Runnable
-
一个阶段的执行可能是被单个节点的完成所触发的,也可能是由多个阶段一起触发(类似于Linux中的管道分隔符)
-
CompletableFuture提供函数式编程能力,可通过回调的方式处理计算结果,也提供了转换和组合的方法
a. 四大静态方法
runAsync 无返回值
以下没有指定Executor方法,则使用默认的ForkJoinPool.commonPool()
作为他的线程池执行异步代码
如果指定了Executor方法,则使用自定义或者特别指定的线程池执行异步代码
- 只有Runnable作为输入参数
public static CompletableFuture<Void> runAsync(Runnable runnable) {
return asyncRunStage(asyncPool, runnable);
}
- Runnable runnable,Executor executor
public static CompletableFuture<Void> runAsync(Runnable runnable,
Executor executor) {
return asyncRunStage(screenExecutor(executor), runnable);
}
supplyAsync 有返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
Executor executor) {
return asyncSupplyStage(screenExecutor(executor), supplier);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. runAsync(Runnable) 无返回值,使用默认的线程池ForkJoinPool.commonPool
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 2. runAsync(Runnable) 无返回值,使用自定义线程池
ExecutorService threadPool1 = Executors.newFixedThreadPool(3);
CompletableFuture<Void> completableFuture2 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, threadPool1);
threadPool1.shutdown();
// 3.supplyAsync 有返回值,使用默认线程池
CompletableFuture<String> completableFuture3 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello, completableFuture1...";
});
// 4. 3.supplyAsync 有返回值,使用自定义线程池
ExecutorService threadPool2 = Executors.newFixedThreadPool(3);
CompletableFuture<String> completableFuture4 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello, completableFuture2...";
}, threadPool2);
threadPool2.shutdown();
System.out.println(completableFuture1.get());
System.out.println(completableFuture2.get());
System.out.println(completableFuture3.get());
System.out.println(completableFuture4.get());
}
b. 通用演示以及优点分析
CompletableFuture是Future的增强版,减少阻塞和轮询可以传入回调函数,当异步任务完成或者发生异常时,自动调用回调对象的回调方法
public static void main(String[] args) throws ExecutionException, InterruptedException {
userFuture();
}
public static void userFuture() throws ExecutionException, InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " come in...");
int res = ThreadLocalRandom.current().nextInt(10);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2s之后出结果: " + res);
if(res >= 5){ // 演示异常时的回调
int i=1/0;
}
return res;
// 回调函数使用whenComplete(Consumer(当前任务的结果,异常))
}, threadPool).whenComplete( (v,e) ->{
if(e == null){
System.out.println("------------任务完成,返回结果: " + v);
}
// 发生异常时需要调用的回调函数
}).exceptionally(e ->{
e.printStackTrace();
System.out.println("出现异常: " + e.getMessage());
return null;
});
threadPool.shutdown();
System.out.println(Thread.currentThread().getName() + " 处理其他任务...");
// TimeUnit.SECONDS.sleep(5);
System.out.println("task is over...");
}
- 异步任务结束或者发生异常时,会自动回调某个对象的方法
- 主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以按顺序执行
c. CompletableFuture在电商项目中的使用
Lambda表达式+Stream流式调用+Chain链式调用+Java8函数式编程
需求:同一款产品,同时搜索出在各个平台的售价;同一款产品,同时搜索在同一个电商平台下,每个商家的售价
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 1.同一款产品,同时搜索出在各个平台的售价
* 2.同一款产品,同时搜索在同一个电商平台下,每个商家的售价
*/
public class CompletableFutureForMallProject {
// 电商平台集合
public static List<Mall> mallList = Arrays.asList(new Mall("jd"),
new Mall("Taobao"),
new Mall("Dangdang"),
new Mall("PDD"),
new Mall("Xiaohongshu"));
// 根据电商平台的集合以及商品名查询到在每个平台的售价 返回一个价格的集合
public static List<String> getPrices(List<Mall> list, String productName){
return list.
stream().
map((mall) -> {
return String.format(productName + " in %s price is %.2f",
mall.getMallName(),
mall.getProductPrice(productName));
}).collect(Collectors.toList());
}
// 使用CompletableFuture 并行处理每一个集合中的元素 处理完后收集每一个CompletableFuture任务的结果
// List<String> -> List<CompletableFuture<String>> -> List<String>
public static List<String> getPriceByCompletableFuture(List<Mall> list, String productName){
List<String> res = list.stream().map((mall) -> { return CompletableFuture.supplyAsync( () ->
String.format(productName + " in %s price is %.2f",
mall.getMallName(),
mall.getProductPrice(productName)));})
.collect(Collectors.toList())
.stream()
.map((cf) -> {return cf.join();})
.collect(Collectors.toList());
return res;
}
public static void main(String[] args) {
Long begin = System.currentTimeMillis();
List<String> prices = getPrices(mallList, "JVM虚拟机");
for(String price : prices){
System.out.println(price);
}
long end = System.currentTimeMillis();
System.out.println("----step by step process------cost time: " + (end - begin) + "ms");
System.out.println("==============================================");
Long begin2 = System.currentTimeMillis();
List<String> prices2 = getPriceByCompletableFuture(mallList, "JVM虚拟机");
for(String price : prices2){
System.out.println(price);
}
long end2 = System.currentTimeMillis();
System.out.println("----use CompletableFuture process------cost time: " + (end2 - begin2) + "ms");
}
}
@Data
@AllArgsConstructor
class Mall{
private String mallName;
// 查询具体某一个电商平台中productName的售价
public double getProductPrice(String productName){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ThreadLocalRandom.current().nextDouble()*2 + productName.charAt(0);
}
}
d. CompletableFuture的常用方法
- 获得结果和触发计算
// 获得结果的方法
public T get(long timeout, TimeUnit unit)
public T join()
// 如果当前任务未完成则输出getNow中指定的值
public T getNow(T valueIfAbsent)
//当调用get方法发生阻塞时,complete方法结束当前阻塞,返回true以及complete中设置的值; 否则只返回false
public boolean complete(T value)
- 对计算结果进行处理
// thenApply: 出现异常时立即停止
// handle:与thenApply的区别在于发生异常时,可以根据带的参数进一步处理
// 计算结果存在依赖关系 -> 使得线程串行化
public <U> CompletableFuture<U> thenApply(
Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> handle(
BiFunction<? super T, Throwable, ? extends U> fn)
- 对计算结果进行消费
// 接收任务的处理结果并消费处理,无返回结果
public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
对比任务之间的执行顺序
- thenApply:任务A执行完成后执行任务B,任务B需要任务A的结果,任务B有返回值
- thenAccept:任务A执行完成后执行任务B,任务B需要任务A的结果,任务B无返回值
- thenRun:任务A执行完成后执行任务B,并且B不需要A的结果
import java.util.concurrent.CompletableFuture;
/**
* thenAccept任务A执行完成后执行任务B,任务B需要任务A的结果,任务B无返回值
* 并且是消费型接口 -> join得到的是null
* thenRun:没有返回值 -> join得到的是null
* thenApply: 有返回值
*/
public class CompletableFutureAPI_3 {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() ->{
return 1;
}).thenApply((val)->{
return val+1;
}).thenApply((val) ->{
return val*10;
}).thenAccept((val) ->{
val *= 100;
System.out.println(val);
}).whenComplete((val, ex)->{
if(ex == null){
System.out.println(val);
}
});
System.out.println("=========================");
System.out.println(CompletableFuture.supplyAsync(() -> {
return "res1";
}).thenRun(() -> {
System.out.println("thenRun...step");
}).join());
System.out.println("=========================");
System.out.println(CompletableFuture.supplyAsync(() -> {
return "res1";
}).thenApply((val) -> {
System.out.println("thenApply...step");
return val + " res2";
}).join());
}
}
- 对计算速度进行选用
public <U> CompletableFuture<U> applyToEither(
CompletionStage<? extends T> other, Function<? super T, U> fn) {}
- 对计算结果进行合并
两个CompletableFuture任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理,先完成的等待着分配其他任务
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class CompletableFutureSpeed {
public static void main(String[] args) {
CompletableFuture<String> play1 = CompletableFuture.supplyAsync(() -> {
System.out.println("player1 come in....");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "player1...";
});
CompletableFuture<String> play2 = CompletableFuture.supplyAsync(() -> {
System.out.println("player2 come in....");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "player2...";
});
// public <U> CompletableFuture<U> applyToEither(
// CompletionStage<? extends T> other, Function<? super T, U> fn)
// 对计算速度进行选用 -> 对对处理速度较快的CompletableFuture进行处理
CompletableFuture<String> res = play1.applyToEither(play2, (who) -> {
return who + " is winner!";
});
System.out.println(Thread.currentThread().getName() + " process\t==>" + res.join());
System.out.println("===========================");
thenCombine();
System.out.println("===========================");
thenCombine2();
}
public static void thenCombine(){
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t启动");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t启动");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 100;
});
// public <U,V> CompletableFuture<V> thenCombine(
// CompletionStage<? extends U> other,
// BiFunction<? super T,? super U,? extends V> fn)
CompletableFuture<Integer> res = future1.thenCombine(future2, (x, y) -> {
System.out.println(Thread.currentThread().getName() + "\t合并结果...");
return x+y;
});
System.out.println(res.join());
}
public static void thenCombine2(){
CompletableFuture<Integer> res = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t启动");
return 10;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t启动");
return 20;
}), (x, y) -> {
System.out.println(Thread.currentThread().getName() + "\t启动");
return x + y;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "\t启动");
return 30;
}), (a, b) -> {
System.out.println(Thread.currentThread().getName() + "\t启动");
return a + b;
});
System.out.println(res.join());
}
}
3. Java中的锁
3.1. 悲观锁和乐观锁
悲观锁:适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源
乐观锁:认为自己在使用数据时不会有其他线程修改数据,不会添加锁
如果临界资源被没有被更新,当前线程将自己修改的数据成功写入。如果临界资源发生更新,则根据不同的实现方式执行不同的操作,比如放弃修改,重新去抢占锁等
判断规则:版本号机制version;CAS中的自旋锁(原子类的递增操作)
3.2. 线程8锁
import java.util.concurrent.TimeUnit;
/**
* 多线程8锁
* 1.标准访问,先打印邮件再执行发微信
* 2.邮件先暂停4秒,先打印邮件再打印微信
* 一个对象里面如果有多个synchronized普通方法,某一个时刻内,只能有一个线程去调用其中的一个synchronized方法,
* 其他的线程只能等待;锁的是当前对象this(调用者),被锁定后,其他线程都不能进入到当前对象的其他synchronized方法
* ==================================================
* 3.新增一个普通方法,邮件先暂停4秒,先打印看电影再打印发邮件
* 由于普通方法并没有加上synchronized上锁 并不需要和发邮件方法去抢占资源,所以可以直接调用watchMovie方法
* ====================================================
* 4.两个资源类,两个同步普通方法,邮件暂停4秒,先执行发微信再执行发邮件
* sendEmail方法锁的是当前对象phone1,而phone2是另一个对象,不是同一把锁,各用各的,所以phone2的方法先执行
* ======================================================
* 5.两个静态同步方法,同一个资源,邮件暂停4秒,先打印邮件再打印微信
* 6.两个静态同步方法,两个资源,邮件暂停4秒,先打印邮件再打印微信
* 静态同步方法锁的是所在的整个类模板(类锁),加锁的对象从实例对象变成类,无论有多少资源,同一个时刻只能有一个线程去调用方法
* ===============================================================
* 7.1个普通同步方法1个静态同步方法,1个资源,邮件暂停4秒,先执行发微信再执行发邮件
* 8.1个普通同步方法1个静态同步方法,2个资源,邮件暂停4秒,先执行发微信再执行发邮件
* 普通同步方法锁的是当前new出来的实例对象(this),而静态同步方法锁的是这个类模板(.class),
* 一个是对象锁一个是类锁,互不影响,所以微信方法执行;
* 对于两个资源,一个实例对象也是一个新的对象锁,与类锁也不会竞争
*/
class Phone{
public static synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(4); // 暂停4秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送邮件...");
}
public static synchronized void sendWechat(){
System.out.println("发微信...");
}
public void watchMovie(){
System.out.println("看电影...");
}
}
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
// Phone phone2 = new Phone();
new Thread(() -> {
try{
phone1.sendEmail();
}catch (Exception e){
e.printStackTrace();
}
}, "t1").start();
Thread.sleep(100);
new Thread(() -> {
try{
phone1.sendWechat();
// phone1.watchMovie();
// phone2.sendWechat();
}catch (Exception e){
e.printStackTrace();
}
}, "t2").start();
}
}
能加对象锁,就不要用类锁。尽可能使加锁的代码块工作量小,避免在锁代码块中调用RPC方法
3.3. synchronized
具体应用在三个地方
- 作用于实例方法,当前实例对象进行加锁,进入到同步代码之前获取当前实例的锁
- 作用于同步代码块,对括号中的对象进行加锁
- 作用于静态方法,当前类加锁,进入到同步代码块之前需要获取当前类锁
synchronized锁字节码分析
javac -p ..class
class文件进行反编译:发现synchronized同步代码块实现是靠monitorenter和monitorexit两个指令(多一个monitorexit指令,防止出现异常不能正常释放锁)
javac -v ..class
: 普通通方法会检查该方法的flags中的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会先持有monitor锁,然后再执行方法内容,最后在方法执行结束时释放monitor锁
ACC_STATIC, ACC_SYNCHRONIZED访问标志区分该方法是否是静态同步方法
synchronized底层原语分析
为什么任何一个对象都可以成为一个锁?
根据OpenJDK的源码objectMonitor.java,objectMonitor.cpp,objectMonitor.hpp
由于每个对象天生就带着一个对象监视器objectMonitor,每一个被锁住的对象都会和Monitor所关联
其中有ObjectMonitor中有几个关键属性
_owner | 指向持有ObjectMonitor对象的线程 |
---|---|
_WaitSet | 存放处与wait状态的线程队列 |
_EntryList | 存放处于等待所block状态的线程队列 |
_recursions | 锁的重入次数 |
_count | 记录当前线程获取锁的次数 |
3.3. 公平锁和非公平锁
synchronized和Lock默认都是非公平锁,为什么默认是非公平锁?
- 恢复挂起的线程到真正锁的获取还是有时间差的,非公平锁可以更充分的利用CPU时间片,尽量减少CPU的空闲状态时间
- 当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程重新获取同步状态的概率就会变大,也就减少了线程切换的开销
总的来说非公平锁可以获得更高的吞吐量
3.4. 可重入锁(递归锁)
ReentrantLock和Synchronized均是可重入锁
同一线程外层函数获得锁之后,内层递归函数依旧能获取该锁的代码;同一个线程在外层方法获取锁时,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个他已经拥有的锁所同步着的代码块(自己可以获取自己的内部锁)
如果一个有synchronized修饰的递归方法或者代码块,在内部调用本类的其他synchronized方法,一定可以获取到锁。作用是防止死锁
synchronized原理分析
- 每个锁对象拥有一个锁计数器count和一个指向持有该锁的线程的指针owner
- 当执行monitorenter时,如果目标锁对象的计数器为0,则该锁没有被其他线程所持有,JVM会将该锁对象的持有线程owner参数设置为当前线程,并将计数器+1
- 在count不为0的情况下,如果锁对象的持有线程是当前线程,那么JVM可以将其计数器+1,否则需要等待,直至持有线程释放该锁
- 当执行monitorexit时,JVM将锁对象的计数器-1,计数器为0代表锁已被释放
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
class Lock{
public synchronized void m1(){
System.out.println(Thread.currentThread().getName() + "===============step1");
m2();
System.out.println(Thread.currentThread().getName() + "===============over...");
}
public synchronized void m2(){
System.out.println(Thread.currentThread().getName() + "===============step2");
m3();
}
public synchronized void m3(){
System.out.println(Thread.currentThread().getName() + "===============step3");
}
}
public class reentrantLockDemo {
public static java.util.concurrent.locks.Lock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
final Object obj = new Object();
new Thread(()->{
synchronized (obj){
System.out.println("==============1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
System.out.println("===================2");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
System.out.println("=======================3");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
Lock lock = new Lock();
new Thread(() ->{
lock.m1();
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("================Lock显示锁演示====================");
new Thread(() ->{
reentrantLock.lock();
try{
System.out.println(Thread.currentThread().getName() + "========step1");
reentrantLock.lock();
try{
System.out.println(Thread.currentThread().getName() + "========step2");
}finally {
reentrantLock.unlock();
}
}finally {
// reentrantLock的加锁和解锁必须成对出现 并且unlock方法必须在lock方法之后使用
reentrantLock.unlock();
}
}, "t3").start();
}
}
3.5. 死锁
死锁:两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉他们将无法推进下去
代码演示:
import java.util.concurrent.TimeUnit;
public class DeadLockDemo {
public static final Object objA = new Object();
public static final Object objB = new Object();
public static void main(String[] args) {
new Thread(() ->{
synchronized (objA){
System.out.println(Thread.currentThread().getName() + "\t自己持有"+objA+ "锁,希望获得"+ objB+"锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objB){
System.out.println(Thread.currentThread().getName() + "\t成功获得"+ objB+"锁");
}
}
}, "t-1").start();
new Thread(() ->{
synchronized (objB){
System.out.println(Thread.currentThread().getName() + "\t自己持有"+objB+ "锁,希望获得"+ objA+"锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objA){
System.out.println(Thread.currentThread().getName() + "\t成功获得"+ objA+"锁");
}
}
}, "t-2").start();
}
}
解决:
windows下查看java运行程序,类似ps的查看线程的命令,在IDEA的terminal中使用
jps -l
定位哪一个线程
jstack 线程id
找到该线程的java工作栈查看
在cmd窗口输入jconsole
通过图形化界面查看当前线程的死锁状态
总结
synchronized(this):指针指向monitor对象(管程或监视器对象)的起始地址,每个对象都存在一个monitor与之对应,当一个monitor被某个线程持有后,便处于锁定锁状态。在JVM中,monitor对象是由ObjectMonitor对象实习的,在JVM-hotspot源码objectMonitor.hpp文件中
锁的性质:Java中的任何对象都可以当成锁来使用,相当于给对象做个标记,对象在内存中的存储布局,包括:
- markword:占用8字节,包括GC信息,hashcode,锁的信息
- 类型指针class pointer:占用4字节
- 实例数据instance data:成员变量的字节数总和,引用类型为4个字节
- 对齐padding(保证每一个java对象都是8字节的整数倍)
添加jol-core依赖查看具体对象在内存中的存储布局
public class InstanceLock {
private static class T{
int m;
long p;
boolean b;
String str = "str";
}
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
T t = new T();
// System.out.println("创建对象时的布局:" + ClassLayout.parseInstance(obj).toPrintable());
System.out.println("创建对象时的布局:" + ClassLayout.parseInstance(t).toPrintable());
synchronized (t){
System.out.println("加锁之后对象的布局: " + ClassLayout.parseInstance(t).toPrintable());
}
TimeUnit.SECONDS.sleep(1);
System.out.println("解锁之后对象的布局:" + ClassLayout.parseInstance(t).toPrintable());
}
}
打印信息发现给某个对象加锁,只会改变该对象的mark部分,相当于对mark部分做一些标记
markword部分的最后三位的状态如果为
001则处于无锁状态
101则为偏向锁状态
00则为轻量级锁,自旋锁或者无锁
10为重量级锁
11为GC标记信息(CMS过程用到的标记信息)
轻量级锁和重量级锁的区别?
JVM层面能解决的锁称之为轻量级锁,使用自旋来完成,当并发量比较高或者临界区比较复杂时,该种方式的开销较大
需要交给底层OS来完成的称为重量级锁,线程进入等待队列
偏向锁
由于JVM虚拟机有一些默认启动的线程,存在许多sync代码,这些sync代码启动时就知道肯定会存在竞争,如果使用偏向锁,就会造成偏向锁不断进行锁撤销和锁升级的操作,效率降低。默认为4s,当4s之后,再次锁该对象,则此时锁的状态是偏向锁
使用参数 -XX:BiasedLockingStartupDelay=?
可设置具体时间