public static void main(String[] args) {
// 创建一个线程并给这个线程取一个名字
Thread t = new Thread("t1"){
@Override
public void run(){
log.info("thread running...");
}
};
// 启动线程
t.start();
log.info("main running...");
}
把【线程】和【任务】(要执行的代码)分开
public static void main(String[] args) {
Runnable r = new Runnable(){
@Override
public void run(){
log.info("running...");
}
};
// 参数1是任务对象;参数2是线程名字
Thread thread = new Thread(r,"t2");
thread.start();
}
使用lambda精简代码
public static void main(String[] args) {
Runnable r = () -> {log.info("running...");};
Thread thread = new Thread(r,"t3");
thread.start();
}
小结
FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> f = new FutureTask<>(() -> {
log.info("running...");
Thread.sleep(2000);
return 100;
});
new Thread(f,"t3").start();
// 会一直阻塞,直到线程执行完毕才返回
Integer integer = f.get();
log.info("线程执行完返回的值:{}",integer);
}
@Slf4j
public class TestMulitThread {
public static void main(String[] args) {
new Thread(() -> {
while (true){
log.info("running1...");
}
},"t1").start();
new Thread(() -> {
while (true){
log.info("running2...");
}
},"t2").start();
}
}
多个线程的输出顺序是不受控制的
栈与栈帧
我们都知道JVM中由堆、栈、方法区组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
总结:一个线程一个栈,一个方法一个栈帧
线程上下文切换(Thread Context Switch)
因为一下一些原因导致CPU不再执行当前的线程,转而执行另一个线程的代码:
当Context Switch发生时,需要操作系统保存当前线程的状态,并恢复另一个线程的状态,JVM中对应的概念就是程序计数器,他的作用是记住下一条JVM执行执行的地址,是线程私有的。
@Slf4j
public class Test4 {
public static void main(String[] args) {
new Thread(() -> {
log.info("t1 do...");
},"t1").run();
log.info("main do other thing...");
}
}
输出
21:16:19.011 [main] INFO com.hpu.heima.concurrent.test.Test4 - t1 do...
21:16:19.014 [main] INFO com.hpu.heima.concurrent.test.Test4 - main do other thing...
由此可见,调用线程的run方法并不会开启新的线程,而是还是由主线程调用run方法的执行体。
不可以连续两次调用线程的start()方法,否则会报线程状态非法的异常!
总结:调用这两个方法之后,线程进入的状态不同!sleep()进入阻塞状态,yield()进入就绪状态!
public class Test9 {
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
while (true){
System.out.println("-------->1>>>" + count++);
}
};
Runnable task2 = () -> {
int count = 0;
while (true){
// 此时该线程会让出时间片
// Thread.yield();
System.out.println(" -------->2>>>" + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
// 线程优先级高的执行次数就会多一些
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
在没有利用CPU来计算时,不要让while(true)空转浪费CPU,可以使用yield或者sleep来让出CPU的使用权给其他程序。否则CPU会
public class TestCpu {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true){
try {
// 不加这句话CPU会直接打满
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1");
}
}
下面代码执行完,r的值是多少?
@Slf4j
public class Test10 {
static int r = 0;
public static void main(String[] args) {
test1();
}
public static void test1(){
Thread t1 = new Thread(() -> {
log.info("t1线程开始");
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("t1结束");
r = 10;
},"t1");
t1.start();
log.info("主线程打印结果为:{}",r);
log.info("主线程结束");
}
}
21:02:32.660 [t1] INFO com.hpu.heima.concurrent.test.Test10 - t1线程开始
21:02:32.660 [main] INFO com.hpu.heima.concurrent.test.Test10 - 主线程打印结果为:0
21:02:32.664 [main] INFO com.hpu.heima.concurrent.test.Test10 - 主线程结束
21:02:32.667 [t1] INFO com.hpu.heima.concurrent.test.Test10 - t1结束
分析:
1、因为主线程和线程t1是并行执行的,t1线程需要睡眠一秒之后才能算出r=10
2、而主线程一开始就要打印r的结果,所以只能打印出r=0
解决方法:
1、用join(),加在t1.start()之后
总结:join()方法的作用:等待某个线程运行结束;join(long n)等待线程运行结束,最多等待n毫秒。谁调用就是等待谁,比如在main线程中调用t1.join(),意思就是等待t1运行结束。
以调用方角度来讲,如果:
下面的方法示例就是一个同步的例子,主线程等着其他线程运行结束才打印最终的结果
public static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
},"t1");
Thread t2 = new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 10;
},"t2");
final long l = System.currentTimeMillis();
t1.start();
t2.start();
log.info("t1 join start");
t1.join();
log.info("t1 join end");
log.info("t2 join start");
t2.join();
log.info("t2 join end");
log.info("主线程打印结果为:r1:{},r2:{}",r1,r2);
log.info("总耗时:{}",System.currentTimeMillis() - l);
}
21:21:22.458 [main] INFO com.hpu.heima.concurrent.test.Test10 - t1 join start
21:21:23.461 [main] INFO com.hpu.heima.concurrent.test.Test10 - t1 join end
21:21:23.461 [main] INFO com.hpu.heima.concurrent.test.Test10 - t2 join start
21:21:23.461 [main] INFO com.hpu.heima.concurrent.test.Test10 - t2 join end
21:21:23.461 [main] INFO com.hpu.heima.concurrent.test.Test10 - 主线程打印结果为:r1:10,r2:10
21:21:23.464 [main] INFO com.hpu.heima.concurrent.test.Test10 - 总耗时:1008
@Slf4j
public class Test11 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("sleep...");
try {
// 不仅是sleep,wait、join一样的效果
TimeUnit.MILLISECONDS.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
TimeUnit.MILLISECONDS.sleep(1000);
log.info("interrupt...");
t1.interrupt();
log.info("interrupt flag:{}",t1.isInterrupted());
}
}
21:42:19.108 [t1] INFO com.hpu.heima.concurrent.test.Test11 - sleep...
21:42:20.120 [main] INFO com.hpu.heima.concurrent.test.Test11 - interrupt...
21:42:20.120 [main] INFO com.hpu.heima.concurrent.test.Test11 - interrupt flag:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.hpu.heima.concurrent.test.Test11.lambda$main$0(Test11.java:19)
at java.lang.Thread.run(Thread.java:748)
打断正常运行的线程,打断状态不会被清空
@Slf4j
public class Test12 {
public static void main(String[] args) throws InterruptedException {
Thread t2 = new Thread(() -> {
while (true){
Thread thread = Thread.currentThread();
boolean interrupt = thread.isInterrupted();
if (interrupt){
log.info("打断状态:{},为true我要退出这个死循环",interrupt);
break;
}
}
},"t2");
t2.start();
TimeUnit.MILLISECONDS.sleep(1000);
t2.interrupt();
}
}
21:47:12.711 [t2] INFO com.hpu.heima.concurrent.test.Test12 - 打断状态:true,为true我要退出这个死循环
注意:被打断的正常线程依然会正常运行!此时可以通过判断打断状态来优雅地停止线程~,这种方式能够使线程在终止时有机会去清理资源,而不是武断地将线程终止(注意比较stop)
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
@Slf4j
public class Test15 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true){
log.info("always running");
if (Thread.currentThread().isInterrupted()){
break;
}
}
log.info("t1 thread end");
},"t1");
// 设置为守护线程,默认为false。只要当其他非守护线程运行结束,守护线程就会强制结束
thread.setDaemon(true);
thread.start();
TimeUnit.MILLISECONDS.sleep(3000);
log.info("main thread end");
}
}
没有守护线程的输出,thread线程会一直执行:
20:31:14.758 [t1] INFO com.hpu.heima.concurrent.test.Test15 - always running
20:31:14.758 [t1] INFO com.hpu.heima.concurrent.test.Test15 - always running
20:31:14.758 [t1] INFO com.hpu.heima.concurrent.test.Test15 - always running
注意:
问题详情可百度
@Slf4j
public class Test16 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("洗水壶");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("烧开水");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"老王");
t1.start();
Thread t2 = new Thread(() -> {
log.info("干三件事");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("等待老王结束,可以泡茶了");
},"小王");
t2.start();
}
}
一个程序运行多个线程本身是没有问题的,问题出现在多个线程访问共享资源,多个线程访问共享资源也没有问题,但是在多个线程对共享资源读写操作时发生指令交错,就会出现问题,一段代码块内如果存在对共享资源的多线程读写操作,则称这块代码为临界区,注意是代码块!
public class Temp {
static int count = 0;
public static void main(String[] args) {
}
// 临界区
static void add(){
count++;
}
// 临界区
static void sub(){
count--;
}
}
多个线程在临界区内执行,由于代码的执行顺序不同而导致结果无法预测,称之为发生了竞态条件
为了避免临界区的静态条件发生,有多重手段可以达到目的
下面使用阻塞式解决方案:synchronized来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程再想获取这个【对象锁】时就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区的代码,不用担心线程上下文的切换。
注意:虽然Java中互斥和同步都可以采用synchronized关键字来完成,但他们还是有区别的:
语法
synchronized (对象){
// 临界区
}
注意:多个线程必须保证对同一个对象使用synchronized!
解决
@Slf4j
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
room.add();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
room.sub();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.info("最终的结果:{}",room.getCount());
}
}
class Room{
private static int count = 0;
public void add(){
synchronized (this){
count++;
}
}
public void sub(){
synchronized (this){
count--;
}
}
public int getCount(){
synchronized (this){
return count;
}
}
}
总结:synchronized实际是用对象锁保证了临界区代码的原子性,临界区内的代码是不可分割的,不会被线程切换所打断。
修饰实例方法
synchronized只能锁对象,只不过加在实例方法上,锁的是this对象
class Demo{
public synchronized void test(){
}
}
等价于
class Demo{
public void test(){
synchronized (this){
}
}
}
修饰静态方法
锁的是类对象
class Demo{
public synchronized static void test(){
}
}
等价于
class Demo{
public static void test(){
synchronized (Demo.class){
}
}
}
局部变量时:
public static void test1(){
int i = 10;
i++;
}
每个线程调用test1方法时,局部变量i会在每个线程的栈帧内存中被创建多份,对变量i的操作都在栈帧内存中操作,操作完再刷新到主内存,因此不存在共享(因为多线程环境下一次只能有一个线程来操作)。
成员变量时:
public class TestThreadSafe {
ArrayList<String> list = new ArrayList<>();
public void test1(){
for (int i = 0; i < 100; i++) {
test2();
test3();
}
}
public void test2(){
list.add("1");
}
public void test3(){
list.remove(0);
}
}
此时的成员变量的存放位置是在堆中,而堆内存是线程共享的,当有多线程操作时,就会产生并发问题
稍作修改,将成员变量修改为成员变量就不会有线程安全问题了:
public class TestThreadSafe {
public void test1(){
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
test2(list);
test3(list);
}
}
public void test2(List<String> list){
list.add("1");
}
public void test3(List<String> list){
list.remove(0);
}
}
分析: 因为是list是局部变量,每个线程调用时会创建不同实例,没有共享
放在成员变量中就一定安全吗?答案是不可能的,比如下面这种情况:
public class TestThreadSafe {
public void test1(){
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
test2(list);
test3(list);
}
}
public void test2(List<String> list){
list.add("1");
}
public void test3(List<String> list){
list.remove(0);
}
}
class Children extends TestThreadSafe{
@Override
public void test3(List<String> list) {
new Thread(() -> {
list.remove(0);
});
}
}
分析:父类和子类中的list是同一个list,但是子类中的线程和父类中的线程却不是同一个,此时发生了资源共享,就会产生线程安全问题
这里说他们是线程安全的是指:多个线程调用他们同一个实例的某个方法时,是线程安全的,但是多个方法组合时,就不是线程安全的了。比如同时调用HashTable的get()和put()方法时,就不是线程安全的,HashTable只能保证get()和put()各自的安全!
Java对象在内存中一般由两部分组成Java对象头、对象中的成员变量
以32位虚拟机为例
Monitor被翻译为监视器或管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word就指向Monitor对象,由一个指针关联
给对象上锁的流程:当一个线程执行被synchrd(obj)修饰的代码块时,就会尝试将obj对象与操作系统提供的Monitor对象相关联,关联的方式是依靠一个指针指向Monitor对象,此时这个线程就成为了这个Monitor对象的所有者,也就是获取到了锁,当再有线程来执行synchrd(obj)代码块时,首先会判断obj对象有没有关联Monitor对象(monitorenter),再看这个Monitor对象有没有主人(Owner指向的线程),有的话就会和Monitor中的EntryList相关联从而进入BLOCKED状态。当Owner指向的线程执行完之后,就会按照一定的规则从EntryList中获取一个线程并与之关联起来,这个线程也就获得了锁。
注意:
1、synchronized必须是进入同一个对象的Monitor才有上述效果(重要提示:每一个对象都会关联一个Monitor)
2、不加synchronized的对象不会关联监视器,不遵从以上规则
示例代码:
public class Temp {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock){
counter++;
}
}
}
对应的字节码为:
synchronized字节码角度原理:拿到锁对象(lock)引用地址,锁对象会关联一个Monitor(见上面的解释),然后复制一份存储到slot1(据说是什么槽,后续再补充) 中,为了解锁时使用。【在此之前还没执行synchronized代码块,后面流程开始执行】。开始执行synchronized指令,对应的JVM指令为monitorenter,将lock对象的MarkWord置位Monitor指针(对应上面的图),代码块执行完退出代码块时,就会用到开始时复制的Monitor对象引用地址,然后根据MarkWord找到Monitor,然后调用monitorexit指令将锁对象的MarkWord重置,然后重新唤醒EntryList,等待下一个线程进入。
总结:synchronized让每个对象都关联一个Monitor,Monitor才是真正的锁,线程进入Monitor的指令为monitorenter,线程退出Monitor的指令为monitorexit。
轻量级锁的适用场景:如果一个对象虽然有多个线程访问,但多线程访问时错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍是synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void m1(){
synchronized (obj){
// 同步块A
m2();
}
}
public static void m2(){
synchronized (obj){
// 同步块B
}
}
Mark Word中锁状态标志:00:轻量级锁、01:无锁、10:重量级锁、11:GC
首先回忆一下对象的结构:对象头和对象体。对象头又由Mark Word和Klass Word组成,Mark Word中包含哈希码、分代年龄、锁状态等,这里重点讨论的就是锁状态的变化。【下面开始解释图】当一个线程执行到m1的synchronized块时,首先在线程的栈帧中产生一个Lock Record(锁记录)对象(上面左图),这个对象包含两个部分:加锁对象(比如上面代码中的lock)指针和加锁对象的Mark Word
让锁记录中的Object reference指向加锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录
这一步的重点是:要将线程的锁记录和加锁对象的Mark Word交换,交换成功就是表示加锁成功
如果CAS替换成功(成功的条件是所对象的锁标志位为01,如果已经为00了,会失败),对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁
此时对象头的Mark Word中就不再是哈希码那些信息了,而是变成了锁记录的地址以及锁类型,而锁记录里则变成了对象的哈希码等信息,解锁的时候再恢复回去
如果CAS失败,有两种情况
1、如果是其他线程已经持有了该Object的轻量级锁(锁标志位为00),这时表名有竞争,进入锁膨胀过程
2、如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数(下面会有偏向锁来优化这个)
当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入(自己重入,注意是自己),这时重置锁记录,表示重入计数减一
如果在尝试加轻量级锁的过程中,CAS操作无法返回成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
static final Object obj = new Object();
public static void m1(){
synchronized (obj){
// 同步块A
}
}
重量级锁竞争的时候,还可以使用自旋来进行优化(进入Monitor的EntryList之前),如果当前线程自选成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
自旋重试成功的情况:
自旋尝试失败的情况:
在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
自旋次数的默认值10次,用户可以使用参数 -XX:PreBlockSpin来更改
轻量级锁在没有竞争时(只有一个线程),每次重入仍然需要执行CAS操作。
Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word,之后发现这个线程ID是自己就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
例如:
static final Object obj = new Object();
public static void m1(){
synchronized (obj){
// 同步块A
m2();
}
}
public static void m2(){
synchronized (obj){
// 同步块B
m3();
}
}
public static void m3(){
synchronized (obj){
// 同步块C
}
}
调用了对象的hashcode之后,对象的偏向锁将被撤销。轻量级锁会在锁记录中记录hashcode,重量级锁会在Monitor中记录hashcode。
当多个线程使用同一个对象时(注意:此时没有竞争),会撤销偏向锁(也就是将偏向状态由可偏向变为不可偏向!)同时将偏向锁升级为轻量级锁。
重要总结:偏向锁时,锁对象的MarkWord中存储的是偏向的线程id;轻量级锁时,锁对象的MarkWord中记录的是锁记录指针
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的偏向锁仍然后机会重新偏向T2,重偏向会重置锁对象偏向的线程ID。
当撤销偏向锁的阈值超过20次后,JVM会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。
当撤销偏向锁阈值超过40次之后,JVM会这样觉得,自己确实是偏向错了,根据就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
他们都是线程之间进行协作的手段,都属于object对象的方法,必须获得此对象的锁,才能调用这个方法,注意:必须先获取锁!
下面演示一下唤醒所有线程
@Slf4j
public class Test18 {
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (lock){
log.info("t1 running");
try {
lock.wait();// 让线程进入monitor的waitset中等待
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("唤醒了开始执行其他代码");
}
},"t1").start();
new Thread(() -> {
synchronized (lock){
log.info("t2 running");
try {
lock.wait();// 让线程进入monitor的waitset中等待
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("唤醒了开始执行其他代码");
}
},"t2").start();
TimeUnit.SECONDS.sleep(2);
log.info("开始唤醒所有线程");
synchronized (lock){
// 唤醒所有线程
lock.notifyAll();
}
}
}
重点总结:重点理解一下上年的代码,一个object对象被两个线程共享,两个线程能够分别获得锁,说明这个共享的对象就是一把锁,可以应用到不同的线程上面,具体应用的方式是:哪个线程获取到锁这个object对象关联的Monitor中的Owner就指向哪个线程
开始之前先看sleep(long n)和wait(long n)的区别
使用方式总结:
public class Temp {
static final Object lock = new Object();
static boolean flag = false;
public static void main(String[] args) {
// 等待的线程
new Thread(() -> {
synchronized (lock) {
//
while (!flag) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
// 唤醒的线程
synchronized (lock) {
// 修改条件
flat = true;
lock.notifyAll();
}
}
}
这样使用的好处是避免了虚假唤醒,唤醒的线程满足条件可以立即执行下面的代码,不满足的则继续等待
@Slf4j
public class Test21 {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
try {
queue.put(new Message(id,"值-" + id));
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者-" + i).start();
}
new Thread(() -> {
try {
while (true){
TimeUnit.SECONDS.sleep(1);
Message take = queue.take();
log.info("消费了一个消息:" + take.getId());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"消费者").start();
}
}
@Slf4j
class MessageQueue{
// 存放消息的队列
private LinkedList<Message> list = new LinkedList<>();
// 默认容量
private int capcity;
public MessageQueue(int capcity) {
this.capcity = capcity;
}
public Message take() throws InterruptedException {
// 检查队列是否为空
synchronized (list){
while (list.isEmpty()){
log.info("队列为空,消费者线程阻塞");
list.wait();
}
// 从队列的头部返回一个消息,唤醒正在阻塞的队列
Message message = list.removeFirst();
log.info("消费一个消息");
list.notifyAll();
return message;
}
}
public void put(Message message) throws InterruptedException {
synchronized (list){
// 检查队里是否已满
while (list.size() == capcity){
log.info("队列已经满了,生产者线程阻塞");
list.wait();
}
list.addLast(message);
log.info("生产一个消息");
// 唤醒正在等待的线程
list.notifyAll();
}
}
}
class Message{
private Integer id;
private Object value;
public Message(Integer id, Object value) {
this.id = id;
this.value = value;
}
public Integer getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Message{" +
"id='" + id + '\'' +
", value=" + value +
'}';
}
}
他们是LockSupport类中的方法
// 暂停当前线程
LockSupport.park()
// 恢复某个线程的运行
LockSypport.unpark()
@Slf4j
public class Temp {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("start");
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("park");
LockSupport.park();
log.info("resume");
},"t1");
t1.start();
TimeUnit.MILLISECONDS.sleep(2000);
log.info("unpark");
LockSupport.unpark(t1);
}
}
unpark既可以在park之前调用,也可以在park之后调用,作用都是恢复某个线程的运行
与Object的wait¬ify相比
每个线程都有一个Parker对象,由三部分组成:_counter、_cond和_mutex
先调用park再调用unpark的情况:
1、调用Unsafe.unpark(Thread-0)方法,设置_counter为1
2、唤醒_cond条件变量中的Thread_0
3、Thread_0恢复运行
4、设置_counter为0
先调用unpark再调用park的情况:
1、调用Unsafe.unpark(Thread_0)方法,设置_counter为1
2、当前线程调用Unsafe.park()方法
3、设置_counter,本情况为1,这时线程无需阻塞,继续运行
4、设置_counter为0
情况1 NEW --> RUNNABLE
情况2 RUNNABLE --> WAITING
t线程用synchronized(obj)获取对象锁后
情况3 RUNNABLE --> WAITING
情况4 RUNNABLE --> WAITING
情况5 RUNNABLE --> WAITING
t线程用synchronized(obj)获取对象锁后
情况6 RUNNABLE --> TIMED_WAITING
情况7 RUNNABLE --> TIMED_WAITING
情况8 RUNNABLE --> TIMED_WAITING
情况9 RUNNABLE --> BLCOKED
情况10 RUNNABLE --> TERMINATED
当前线程所有代码执行完毕,进入TERMINATED
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1线程获得A对象锁,接下来想获取B对象锁
t2线程获得B对象锁,接下来想获取A对象锁
@Slf4j
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
public static void test1(){
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A){
log.info("A lock A");
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B){
log.info("A lock B");
log.info("do something");
}
}
},"t1");
Thread t2 = new Thread(() -> {
synchronized (B){
log.info("B lock B");
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A){
log.info("B lock A");
log.info("do something");
}
}
},"t2");
t1.start();
t2.start();
}
}
找到所有Java进程
D:\spider>jps
9940 Launcher
17768
17896 TestDeadLock
11612 RemoteMavenServer36
19996 Jps
查看制定进程的方法栈,可以看到t1和t2线程的线程状态都为BLOCKED (on object monitor)
D:\spider>jstack 17896
2021-11-02 20:54:08
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.201-b09 mixed mode):
"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x00000000027c6000 nid=0x4a0c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"t2" #13 prio=5 os_prio=0 tid=0x000000001a384800 nid=0x2ec waiting for monitor entry [0x000000001b18f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.hpu.heima.concurrent.test.TestDeadLock.lambda$test1$1(TestDeadLock.java:46)
- waiting to lock <0x00000000d65d51d0> (a java.lang.Object)
- locked <0x00000000d65d51e0> (a java.lang.Object)
at com.hpu.heima.concurrent.test.TestDeadLock$$Lambda$2/1509514333.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1" #12 prio=5 os_prio=0 tid=0x000000001a384000 nid=0x1f3c waiting for monitor entry [0x000000001b08f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.hpu.heima.concurrent.test.TestDeadLock.lambda$test1$0(TestDeadLock.java:31)
- waiting to lock <0x00000000d65d51e0> (a java.lang.Object)
- locked <0x00000000d65d51d0> (a java.lang.Object)
at com.hpu.heima.concurrent.test.TestDeadLock$$Lambda$1/1705929636.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
@Slf4j
public class TestDeadLockPhilosopher {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("z1",c1,c2).start();
new Philosopher("z2",c2,c3).start();
new Philosopher("z3",c3,c4).start();
new Philosopher("z4",c4,c5).start();
new Philosopher("z5",c5,c1).start();
}
}
@Slf4j
class Philosopher extends Thread{
private Chopstick left;
private Chopstick right;
public Philosopher(String name,Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@SneakyThrows
@Override
public void run(){
while (true){
synchronized (left){
synchronized (right){
eat();
}
}
}
}
public void eat() throws InterruptedException {
log.info("eat");
TimeUnit.MILLISECONDS.sleep(2000);
}
}
class Chopstick{
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}
两个线程互相改变对方的条件,导致谁也结束不了
@Slf4j
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
// 期望count减到0退出循环
new Thread(() -> {
while (count > 0){
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
log.info("count={}",count);
}
},"t1").start();
// 期望count大于30退出循环
new Thread(() -> {
while (count < 20){
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
log.info("count={}",count);
}
},"t2").start();
}
}
解决办法:将睡眠时间调整,使用随机睡眠时间
相比于synchronized它具备如下特点:
与synchronized一样,都支持可重入
基本语法
public class Test22 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
}
}
线程执行了lock()方法,就是获得了这把锁,再有其他线程来获取这把锁时,就会进入这把锁的等待队列,有点类似synchronized关键字
可重入是指同一线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获取锁时,自己也会被挡住
@Slf4j
public class Test22 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
// 临界区
log.info("enter main");
m1();
} finally {
lock.unlock();
}
}
public static void m1(){
lock.lock();
try {
// 临界区
log.info("enter m1");
m2();
} finally {
lock.unlock();
}
}
public static void m2(){
lock.lock();
try {
// 临界区
log.info("enter m2");
} finally {
lock.unlock();
}
}
}
线程在等待锁的过程中,其他线程可以终止该线程的等待,线程加锁使用的方法变为了lock.lockInterruptibly()
@Slf4j
public class Test22 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 如果没有竞争,此方法就会获取lock对象锁,
// 如果有竞争就进入阻塞队列,可以被其他线程用interrupt()方法打断
log.info("尝试获得锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.info("没有获得到锁,返回");
return;
}
try {
log.info("获取到锁");
} finally {
lock.unlock();
}
},"t1");
lock.lock();
t1.start();
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 主线程在这里打断t1,就是结束t1线程的阻塞状态,让他别等了
log.info("主线程打断t1线程");
t1.interrupt();
}
}
注意:synchronized虽然也有interrupt()方法,但是synchronized却不是可以被打断的,这个方法的租用只是改变synchronized的打断状态,以便后续根据打断状态进行停止线程或其他操作
提供一种主动的方式来避免死锁(打断是被动方式)。线程在获取锁的过程中,如果持有锁的线程一直没有释放锁,尝试获取锁的线程超过设置的时间之后就放弃等待,表示获取锁失败,可以避免线程无限等待。
@Slf4j
public class Test22 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("开始尝试获取锁");
try {
if (!lock.tryLock(4000, TimeUnit.MILLISECONDS)){
log.info("没有获取到锁");
return;
}
} catch (InterruptedException e){
e.printStackTrace();
return;
}
try {
log.info("获得到了锁");
} finally {
lock.unlock();
}
},"t1");
// 先让主线程获取到锁,t1就获取不到了,就会一直等待,直到超时
lock.lock();
t1.start();
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
log.info("主线程释放了锁");
}
}
@Slf4j
public class TestDeadLockPhilosopher {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("z1",c1,c2).start();
new Philosopher("z2",c2,c3).start();
new Philosopher("z3",c3,c4).start();
new Philosopher("z4",c4,c5).start();
new Philosopher("z5",c5,c1).start();
}
}
@Slf4j
class Philosopher extends Thread{
private Chopstick left;
private Chopstick right;
public Philosopher(String name,Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@SneakyThrows
@Override
public void run(){
while (true){
// 尝试获取左筷子
if (left.tryLock()){
// 尝试获取右筷子
try {
if (right.tryLock()){
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock();
}
}
}
}
public void eat() throws InterruptedException {
log.info("eat");
TimeUnit.MILLISECONDS.sleep(2000);
}
}
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}
ReentrantLock默认是不公平的,可以通过参数设置为公平锁
ReentrantLock reentrantLock = new ReentrantLock(true);
公平锁:当只有锁的线程释放锁时,阻塞度列中等待时间长的线程会先释放锁(先到先得)、
非公平锁:当只有锁的线程释放锁时,阻塞度列中等待锁的线程会一起竞争锁
synchronized中也有条件变量,就是我们讲原理时那个WaitSet等待队列,ReentrantLock的条件变量比synchronized强大之处在于:它是支持多个条件变量的,这就好比
使用流程:
@Slf4j
public class Test24 {
static ReentrantLock lock = new ReentrantLock();
static boolean flag = false;
// 创建一个新的条件变量,同一把锁可以有多个条件变量
static Condition condition1 = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
// 条件不满足进入条件变量1等待
try {
log.info("进入条件变量");
while (!flag){
condition1.await();
}
log.info("条件满足,做其他事情");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t1").start();
new Thread(() -> {
lock.lock();
try {
log.info("睡眠3秒,然后唤醒条件变量中的线程");
TimeUnit.SECONDS.sleep(3);
log.info("改变flag");
flag = true;
// 唤醒条件变量1中的线程
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t2").start();
}
}
比如:必须先打印2,再打印1
基于wait-notify
@Slf4j
public class Test25 {
static Object lock = new Object();
static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock){
while (!flag){
try {
// 线程进入wait状态,释放锁,此时去执行t2线程
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("1");
}
},"t1");
Thread t2 = new Thread(() -> {
synchronized (lock){
log.info("2");
flag = true;
lock.notify();
}
},"t2");
t1.start();
t2.start();
}
}
基于park-unpark
@Slf4j
public class Test26 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
log.info("1");
},"t1");
Thread t2 = new Thread(() -> {
log.info("2");
LockSupport.unpark(t1);
},"t2");
t1.start();
t2.start();
}
}
线程1输出a 5次,线程2输出b 5次,线程输出c 5次。现在要求abcabcabcabcabc怎么实现
基于wait-notify
public class Test27 {
public static void main(String[] args) {
WaitNotify waitNotify = new WaitNotify(1, 5);
new Thread(() -> {
waitNotify.print("a",1,2);
}).start();
new Thread(() -> {
waitNotify.print("b",2,3);
}).start();
new Thread(() -> {
waitNotify.print("c",3,1);
}).start();
}
}
/**
* 输出内容 等待标记 下一个标记
* a 1 2
* b 2 3
* c 3 1
*/
class WaitNotify{
public void print(String str,int waitFlag,int nextFlag){
for (int i = 0; i < loopNum; i++) {
synchronized (this){
while (flag != waitFlag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
// 等待标记
private int flag;
// 循环次数
private int loopNum;
public WaitNotify(int flag, int loopNum) {
this.flag = flag;
this.loopNum = loopNum;
}
}
基于await-signal
public class Test28 {
public static void main(String[] args) {
// 先让所有的线程都进入各自的条件变量,然后由主线程唤醒a,然后开始运行
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
new Thread(() -> {
awaitSignal.print("a", a, b);
}).start();
new Thread(() -> {
awaitSignal.print("b", b, c);
}).start();
new Thread(() -> {
awaitSignal.print("c", c, a);
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
awaitSignal.lock();
try {
System.out.println("主线程开始发起...");
a.signal();
} finally {
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock {
private int loopNum;
public AwaitSignal(int loopNum) {
this.loopNum = loopNum;
}
// 参数1:打印的内容 参数2:进入哪一个条件变量等待 参数3:下一个条件变量
public void print(String str, Condition current, Condition next) {
for (int i = 0; i < loopNum; i++) {
lock();
try {
try {
current.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(str);
next.signal();
} finally {
unlock();
}
}
}
}
基于park-unpark
public class Test29 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) throws InterruptedException {
ParkUnPark parkUnPark = new ParkUnPark(5);
t1 = new Thread(() -> {
parkUnPark.print("a",t2);
},"t1");
t2 = new Thread(() -> {
parkUnPark.print("b",t3);
},"t2");
t3 = new Thread(() -> {
parkUnPark.print("c",t1);
},"t3");
t1.start();
t2.start();
t3.start();
TimeUnit.SECONDS.sleep(1);
LockSupport.unpark(t1);
}
}
class ParkUnPark{
private int loopNum;
public ParkUnPark(int loopNum) {
this.loopNum = loopNum;
}
public void print(String str,Thread next){
for (int i = 0; i < loopNum; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(next);
}
}
}
本章需要重点掌握的是:
JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等
JMM体现在一下几个方面
先来看一个现象,main线程对run变量的修改对于线程t不可见,导致了线程t无法停止
@Slf4j
public class Test32 {
static boolean run= true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run){
}
},"t1");
t.start();
TimeUnit.SECONDS.sleep(2);
log.info("改变条件,期望暂停t线程运行");
run = false;
}
}
为什么呢?分析一下:
1、初始状态,t线程刚从主内存读取了run的值到工作内存
2、因为t线程要频繁从主内存中读取run的值,JIT编译期会将run的值缓存至自己的高速缓存中,减少对主内存中run的访问,提高效率
3、1秒之后,main线程修改了run值,并同步至主内存,而t是从自己工作内存的中的高速缓存中读取这个变量的值,结果永远是旧值
添加volatile关键字,volatile关键字修饰的变量每次读取不能从自己内存的高速缓存中读取,要从主内存中读取!
他可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主内存中获取他的值,线程操作volatile变量都是直接操作柱内存。
上面例子体现的实际就是可见性,他保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,但是不能保证原子性,仅能用在一个写线程,多个读线程的情况
比较一下之前讲线程安全时举的例子:两个线程一个i++,一个i–,只能保证看到最新值,不能解决指令交错的问题
注意:synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是synchronized是属于重量级操作,性能相对较低
JVM会在不影响正确性的前提下,对语句执行顺序进行调整,这种特性个称之为指令重排,多线程下指令重排会影响正确性。
为什么会有指令重排这项优化呢?下面从CPU指令层面理解:现代CPU支持多级指令流水线,例如支持同时执行取指令-指令y译码-执行指令-内存访问-数据协会的处理器,就可以成为五级指令流水线。这时CPU可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执时间最长的复杂指令),本质上流水线技术不能缩短单条指令的执行时间,但他变相提高了指令的吞吐率。
对变量添加volatile关键字,即可禁止指令重排
volatile的底层实现原理是内存屏障(Memory Barrier)
内存屏障出现背景:
为了协调CPU的运行效率和读写内存之间的效率问题,引入了缓存,但是引入缓存之后又出现了多处理器下的内存同步问题,所以为了解决内同步问题又引入了内存屏障的概念。
什么是内存屏障?
内存屏障是什么?内存屏障什么都不是,他只是一个抽象概念!如果这样还不理解,可以把他理解成一堵墙,这堵墙正面与反面的指令无法被CPU乱序执行及这堵墙正面与反面的读写操作需要有序执行。
总结:读前写后有内存屏障
延伸:JVM内存逃逸分析
public void actor2(){
num = 2;
// ready是volatile赋值带写屏障
read = true;
// 写屏障
}
public void actor2(Result r){
// 读屏障
// ready是volatile读取值带读屏障
if(ready){
r.r1 = num + num;
} else {
r.r1 = 1
}
}
public void actor2(){
num = 2;
// ready是volatile赋值带写屏障
read = true;
// 写屏障
}
public void actor2(Result r){
// 读屏障
// ready是volatile读取值带读屏障
if(ready){
r.r1 = num + num;
} else {
r.r1 = 1
}
}
还是那句话,不能解决指令交错(不能解决原子性)
本章重点讲解了JMM中的
原理方面
模式方面
前面看到AtomicInteger的解决办法,内部并没有用锁来保护共享边浪的线程安全,那么他是怎么实现的呢?
public void withDrow(Integer amount){
while (true){
int prev = balance.get();
int next = balance - amount;
// 比较并设置值(原子性 的)
if (balance.compareAndSet(prev,next)){
break;
}
}
}
其中的关键是
compareAndSet,他的简称是CAS,他是原子操作
获取变量时,为了保证该变量的可见性,需要volatile修饰
他可以用来修饰成员变量和静态变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主内存中获取他的值,线程操作volatile变量都是直接操作主存。即一个线程对volatile变量的修改,对另一个线程可见。
CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果
**总结:AtomicInteger中的变量是被volatile修饰的,保证了线程之间的可见性。 **
无锁情况下,即使重试失败,线程始终在高速运行没有停歇,而synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞(进入上述的EntryList)。
结合CAS和volatile可以实现无锁并发,适用于线程少、多核CPU的场景下(线程数不要多于核心数)。
JUC并发包提供了
以AtomicInteger为例
public class Test33 {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(5);
System.out.println(i.incrementAndGet());// i++
System.out.println(i.getAndIncrement());// ++i
System.out.println(i.getAndAdd(5));// 获取并加上5
System.out.println(i.addAndGet(5));// 加上5再获取
System.out.println(i.updateAndGet(e -> e * 10));// 乘以10再获取
System.out.println(i.getAndUpdate(e -> e * 10));// 先获取再乘以10
}
}
为什么需要原子引用类型?因为受保护的数据类型不一定是基本类型!
public class Test35 {
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
}
}
class DecimalAccountCas implements DecimalAccount{
private AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withDraw(BigDecimal bigDecimal) {
while (true){
BigDecimal pre = balance.get();
BigDecimal next = pre.subtract(bigDecimal);
if (balance.compareAndSet(pre,next)) {
break;
}
}
}
}
interface DecimalAccount{
// 获取金额
BigDecimal getBalance();
// 取款
void withDraw(BigDecimal bigDecimal);
static void demo(DecimalAccount account){
ArrayList<Thread> list = new ArrayList<Thread>();
for (int i = 0; i < 1000; i++) {
list.add(new Thread(() -> {
account.withDraw(BigDecimal.TEN);
}));
}
for (Thread thread : list) {
thread.start();
}
for (Thread thread : list) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(account.getBalance());
}
}
主线程仅仅能判断出共享变量的值与最初值A是否相同,不能感知到这种从A到B再到A的情况,如果主线程希望:
只要有其他线程【动过了】共享变量,那么自己的CAS就失败,这时仅仅比较是不过的,需要再加一个版本号
AtomicStapmedReferenct
@Slf4j
public class Test36 {
static AtomicStampedReference<String> reference = new AtomicStampedReference<String>("A",0);
public static void main(String[] args) {
log.info("start");
String s = reference.getReference();
// 版本号
int stamp = reference.getStamp();
log.info("{}",stamp);
other();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("change A->C {}",reference.compareAndSet(s,"C",stamp,stamp + 1));
}
public static void other(){
new Thread(() -> {
log.info("change A->B {}",reference.compareAndSet(reference.getReference(),"B",reference.getStamp(), reference.getStamp() + 1));
},"t1").start() ;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
log.info("change B->A {}",reference.compareAndSet(reference.getReference(),"A",reference.getStamp(), reference.getStamp() + 1));
},"t1").start();
}
}
AtomicMarkableReferenct
但是有时候,并不关心引用变量改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReferenct
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合volatile修饰的字段使用,否则会出现异常
public class Test37 {
public static void main(String[] args) {
Student student = new Student();
AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
updater.compareAndSet(student,null,"张三");
System.out.println(student);
}
}
class Student{
volatile String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
LongAdder
ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量
这些信息存储在一个原子变量ctl中,目的是将线程池状态与个数合二为一,这样就可以用一次CAS原子操作进行赋值
// c 为旧值,ctlOf返回结果为新值
ctl.compareAndSet(c,ctlOf(targetState,workerCountOf(c)))
// rs为高3位代表线程池状态,wc为低29位代表线程个数,ctl是合并他们
private static int ctlOf(int rs,int wc){
return rs | wc;
}
public ThreadPoolExecutor(int corePoolSize,
int maxPoolSize,
long keepAliveTime,
TimeUnit unit,
BolckingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectExecutionHandler handler
)
工作流程:
线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
当线程数达到corePoolSize并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue队列排队,直到有空闲的线程。
如果队列选择了有界队列,那么任务超过了队列大小时,会创建maximumPoolSize - corePoolSize数目的线程来救急。
如果线程到达maximumPoolSize仍然有新任务这时会执行拒绝策略。拒绝策略jdk提供了4种实现,其它著名框架也提供了实现
AbortPolicy 让调用者抛出RejectedExecutionException异常,这是默认策略
CallerRunsPolicy 让调用者运行任务
DiscardPolicy放弃本次任务
DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
Dubbo的实现,在抛出RejectedExecutionException异常之前会记录日志,并dump线程栈信息,方便定位问题
Netty的实现,是创建一 个新线程来执行任务
ActiveMQ的实现,带超时等待(60s) 尝试放入队列,类似我们之前自定义的拒绝策略
PinPoint的实现,它使用了一个拒绝策略链,会逐- 尝试策略链中每种拒绝策略
当高峰过去后,超过corePoolSize的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime和unit来控制。
根据这个构造方法,JDK Eexcutors工具类中提供了众多工厂方法来创建各种用途的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点:
评价:适用于任务量已知,相对耗时的任务
使用:创建一个固定大小的线程池,并且使用了线程工厂,给线程取了一个名字
@Slf4j
public class Temp {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2, new ThreadFactory() {
private AtomicInteger t = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r,"mypool_t" + t.getAndIncrement());
}
});
executorService.execute(() -> {
log.info("1");
});
executorService.execute(() -> {
log.info("2");
});
executorService.execute(() -> {
log.info("3");
});
executorService.shutdown();
}
}
21:43:34.599 [mypool_t2] INFO com.hpu.heima.concurrent.test.Temp - 2
21:43:34.599 [mypool_t1] INFO com.hpu.heima.concurrent.test.Temp - 1
21:43:34.601 [mypool_t2] INFO com.hpu.heima.concurrent.test.Temp - 3
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特点:
评价:整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲1分钟后释放线程。适合任务比较密集,但每个任务执行时间较短的线程。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用场景:希望多个任务排队执行。线程数固定为1,任务多于1时,会放入无界队列排队。任务执行完毕,这唯一的线程也不糊被释放。
区别:
@Slf4j
public class Temp {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService pool = Executors.newFixedThreadPool(2);
// 执行任务
// void execute(Runnable command);
pool.execute(() -> {
log.info("业务代码...");
});
// 提交任务task,用返回值Future获取任务返回结果
// Future submit(Callabletask);
Future<String> submit = pool.submit(() -> {
log.info("业务代码...");
TimeUnit.SECONDS.sleep(2);
return "ok";
});
String string = submit.get();
log.info("返回的结果:{}",string);
// 提交tasks中所有任务(接收一个任务的集合,返回的结果也是结果的集合)
// List> invokeAll(Collection extends Callable> tasks)
List<Future<String>> futures = pool.invokeAll(Arrays.asList(
() -> {
log.info("第1个任务");
TimeUnit.SECONDS.sleep(3);
return "1";
}
,
() -> {
log.info("第2个任务");
TimeUnit.SECONDS.sleep(1);
return "2";
},
() -> {
log.info("第3个任务");
TimeUnit.SECONDS.sleep(2);
return "3";
}
));
for (Future<String> future : futures) {
log.info("{}",future.get());
}
// 提交tasks中所有任务,带超时时间,时间范围内执行不完的任务会取消掉(接收一个任务的集合,返回的结果也是结果的集合)
// List> invokeAll(Collection extends Callable> tasks,long timeout,TimeUnit unit)
// 提交tasks中所有任务,哪个任务先执行完毕,返回次任务执行结果,其他任务取消
// T invokeAny(Collection extends Callable tasks>)
String invokeAny = pool.invokeAny(Arrays.asList(
() -> {
log.info("第1个任务");
TimeUnit.SECONDS.sleep(3);
return "1";
}
,
() -> {
log.info("第2个任务");
TimeUnit.SECONDS.sleep(1);
return "2";
},
() -> {
log.info("第3个任务");
TimeUnit.SECONDS.sleep(2);
return "3";
}
));
log.info("返回的结果:{}",invokeAny);
pool.shutdown();
}
}
shutdown
void shutdown();
shutdownNow
void shutdownNow();
线程池工作流程总结:
所以我们线程池的工作流程也比较好理解了:线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。当调用 execute() 方法添加一个任务时,线程池会做如下判断:如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会根据拒绝策略来对应处理。
当一个线程完成任务时,它会从队列中取下一个任务来执行。 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
CPU密集型运算
通常采用CPU核心数+1能够实现最优的CPU利用率,+1是保证当前线程由于页缺失故障或其他原因导致暂停时,额外的这个线程就能顶上去,保证CPU时钟周期不被浪费
I/O密集型
CPU不能总是处于繁忙状态,例如,当执行业务计算时,这时候会使用CPU资源,但你当执行IO操作时,这时CPU就闲下来了,可以利用多线程提高他的利用率。
经验公式如下:
线程数 = 核心数 乘以 期望CPU利用率 乘以 总时间(CPU计算时间+等待时间)/ CPU计算时间
在任务调度线程池功能加入之前,可以使用java.util.Timer来实现定时功能,Timer的有点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都会影响到之后的任务。
带延迟功能的线程池,前面的线程发生异常也不会影响后面的线程
public class Temp {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.schedule(() -> {
log.info("task1");
},1,TimeUnit.SECONDS);
pool.schedule(() -> {
log.info("task2");
},1,TimeUnit.SECONDS);
pool.shutdown();
}
}
带延迟和定时功能的线程池
public class Temp {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.info("start");
// 间隔指定时间执行
// 初始延迟时间、每个任务之间间隔之间、时间单位
pool.scheduleAtFixedRate(() -> {
log.info("running...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},1,1, TimeUnit.SECONDS);
// 等上一次任务执行结束才开始执行下一个
// 初始延迟时间、每个任务之间间隔之间、时间单位
pool.scheduleWithFixedDelay(() -> {
log.info("running...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},1,1, TimeUnit.SECONDS);
}
}
分别输出,重点看时间间隔
20:48:07.754 [main] INFO com.hpu.heima.concurrent.test.Temp - start
20:48:08.850 [pool-1-thread-1] INFO com.hpu.heima.concurrent.test.Temp - running...
20:48:10.859 [pool-1-thread-1] INFO com.hpu.heima.concurrent.test.Temp - running...
20:48:12.872 [pool-1-thread-1] INFO com.hpu.heima.concurrent.test.Temp - running...
20:48:14.887 [pool-1-thread-1] INFO com.hpu.heima.concurrent.test.Temp - running...
20:50:29.149 [main] INFO com.hpu.heima.concurrent.test.Temp - start
20:50:30.237 [pool-1-thread-1] INFO com.hpu.heima.concurrent.test.Temp - running...
20:50:33.252 [pool-1-thread-1] INFO com.hpu.heima.concurrent.test.Temp - running...
20:50:36.269 [pool-1-thread-1] INFO com.hpu.heima.concurrent.test.Temp - running...
20:50:39.282 [pool-1-thread-1] INFO com.hpu.heima.concurrent.test.Temp - running...
@Slf4j
public class Temp {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService pool = Executors.newFixedThreadPool(2);
// 调用sumbit方法有返回值,用Future对象接收
Future<Boolean> task1 = pool.submit(() -> {
log.info("task1");
int i = 1 / 0;
return true;
});
// 如果线程执行期间没有异常,会正确打印结果,有异常就打印异常
log.info("{}",task1.get());
}
}
Tomcat在哪里用到了线程池呢?先来看一下Tomcat的整体架构图
Tomcat线程池扩展了ThreadPoolExecutor,行为稍有不同
Fork/Join是JDK1.7加入的新的线程实现它体现的是一种分治思想,用于于能够进行任务拆分的CPU密集型运算
所谓的任务拆分,是将一个大型任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如:归并排序、斐波那契数列都可以使用分治思想进行求解
Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成。进一步提高了运算效率
Fork/Join默认会创建于CPU核心数相同的线程池
提交给Fork/Join线程池的任务需要继承RecursiveTask(有返回值)或RecursiveAction(没有返回值)。
public class TestForkJoin2 {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
Integer invoke = pool.invoke(new MyTask(5));
log.info("{}",invoke);
}
}
// 求1-n之间的和 求1-5的和可以理解为5 + MyTask(4),4 + MyTask(3) ...
class MyTask extends RecursiveTask<Integer>{
private int n;
public MyTask(int n) {
this.n = n;
}
// 这里就是要进行具体的拆分
@Override
protected Integer compute() {
// 终止拆分的条件
if (n == 1){
return 1;
}
MyTask t1 = new MyTask(n - 1);
// 让线程去执行此任务
t1.fork();
// 获取任务结果
int res = n + t1.join();
return res;
}
}
全称是AbtractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点
子类主要实现这样一些方法(抛出UnsupportedOperationException)
获取锁的姿势
// 如果获取锁失败
if(!tryAcquire(arg)){
// 入队,可以选择阻塞当前线程(基于park-unpark)
}
释放锁的姿势
// 如果释放锁成功
if(tryRelease(arg)){
// 让阻塞线程恢复运行
}
ReentrantLock的实现原理也是相同方式
@Slf4j
public class TestAqs {
public static void main(String[] args) {
MyLock myLock = new MyLock();
// 创建两个线程,第一个线程先获取了锁,睡眠3秒,导致第二个线程一直阻塞,直到第一个线程释放锁
new Thread(() -> {
myLock.lock();
log.info("locking...");
try {
log.info("doing...");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
log.info("unlocking...");
myLock.unlock();
}
},"t1").start();
new Thread(() -> {
myLock.lock();
log.info("locking...");
try {
log.info("doing...");
} finally {
log.info("unlocking...");
myLock.unlock();
}
},"t2").start();
}
}
// 自定义不可重入锁
class MyLock implements Lock{
// 独占锁,同步器类
class MySync extends AbstractQueuedSynchronizer{
// 加锁
@Override
protected boolean tryAcquire(int arg) {
// 确保加锁的原子性
if (compareAndSetState(0,1)){
// 加上了锁,设置owner为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 解锁
@Override
protected boolean tryRelease(int arg) {
// 这里不需要原子性,因为是锁的持有者释放锁
// 在setState(0)上面设置owner为null,防止指令重排带来的问题
setExclusiveOwnerThread(null);
// setState是volatile修饰的,对其他线程可见
setState(0);
return true;
}
// 是否是独占锁
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
public Condition newCondition(){
return new ConditionObject();
}
}
private MySync sync = new MySync();
// 加锁,不成功会进入等待队列
@Override
public void lock() {
sync.acquire(1);
}
// 加锁,可打断
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
// 尝试加锁一次
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
// 尝试加锁,带超时
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
// 解锁
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
P238-P46
当读操作远远高于写操作时,这时候可以使用读写锁让读-读支持高并发,提高性能
类似于数据库中的select * from table lock in share mode
提供一个数据容器类,内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法
@Slf4j
public class DateContainer {
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(() -> {
demo.write();
},"t1").start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
demo.write();
},"t1").start();
}
}
@Slf4j
class Demo {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read() {
log.info("get read lock");
r.lock();
try {
log.info("read...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return data;
} finally {
log.info("unlock read lock");
r.unlock();
}
}
public void write() {
log.info("get write lock");
w.lock();
try {
log.info("write...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
log.info("unlock write lock");
w.unlock();
}
}
}
总结:读读是并发的,读写、写写是互斥的
P253-P258
用来进行线程同步协作,等待所有线程完成倒计时
其中构造参数用来初始化等待计数值,await()用来等待计数器归零,countDown用来让计数减一
@Slf4j
public class TestCountdownLantch {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
log.info("t1 doing...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
},"t1").start();
new Thread(() -> {
log.info("t2 doing...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
},"t2").start();
new Thread(() -> {
log.info("t3 doing...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
},"t3").start();
// 主线程在这里等待上面三个线程运行结束,然后处理其他业务
log.info("waiting...");
latch.await();
log.info("end");
}
}
改进:CountdownLatch和join的功能类似,不过join相对更底层一些,下面演示结合线程池的使用
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
// 前三个线程干活,第四个线程做一下汇总
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> {
log.info("t1 doing...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
log.info("t1end {}",latch.getCount());
});
pool.submit(() -> {
log.info("t2 doing...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
log.info("t2end {}",latch.getCount());
});
pool.submit(() -> {
log.info("t3 doing...");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
log.info("t3end {}",latch.getCount());
});
pool.submit(() -> {
log.info("t4 waiting...");
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("all end, do others...");
});
}
循环栅栏,用来进行线程协作,等待线程满足某个计数器。构造时设置【计数个数】,每个线程执行到某个需要“同步”的时刻调用await()方法进行等待,当等待的线程数满足【计数个数】时,继续执行。
他和CountdownLatch最大的区别是计数可以恢复,可以设置成初始数
@Slf4j
public class TestCyclic {
public static void main(String[] args) {
// 第二个参数会等待task1和task2都运行结束之后,对结果进行汇总
CyclicBarrier cyclicBarrier = new CyclicBarrier(2,() -> {
log.info("task1 and task2 finish");
});
// 线程数和任务要数一致
ExecutorService pool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
pool.submit(() -> {
log.info("task1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
cyclicBarrier.await();
log.info("t1 end");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
pool.submit(() -> {
log.info("task2 start");
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
cyclicBarrier.await();
log.info("t2 end");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
pool.shutdown();
}
}
线程安全的集合类可以分为三大类