黑马-并发编程讲解

3、线程

3.1创建和运行线程

方法一:直接使用Thread

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...");
    }

方法二:使用Runnable配合Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread代表线程
  • Runnable可运行的任务(线程要执行的代码)
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();
    }

小结

  • 方法一是把线程和任务合并在了一起,方法二是把线程和任务分开了
  • 用Runnable更容易与线程池等高级API配合
  • 用Runnable让人物类脱离了Thread继承体系,更灵活

方法三:Future配合Thread

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);
    }

3.2观察多个线程运行的状况

@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();


    }
}

多个线程的输出顺序是不受控制的

3.3查看进程和线程的方法

3.4原理之线程运行

栈与栈帧

我们都知道JVM中由堆、栈、方法区组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

总结:一个线程一个栈,一个方法一个栈帧

线程上下文切换(Thread Context Switch)

因为一下一些原因导致CPU不再执行当前的线程,转而执行另一个线程的代码:

  • 线程的CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了sleep、yield、join等方法

当Context Switch发生时,需要操作系统保存当前线程的状态,并恢复另一个线程的状态,JVM中对应的概念就是程序计数器,他的作用是记住下一条JVM执行执行的地址,是线程私有的。

3.5常见方法

黑马-并发编程讲解_第1张图片

3.6start与run

@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()方法,否则会报线程状态非法的异常!

3.7sleep与yield

sleep

  • 调用sleep会让当前线程从Running进入Timed Waiting状态
  • 其它线程可以**使用interrupt 方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行
  • 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性(TimeUnit.MICROSECONDS.sleep(1000))

yield(让出)

  • 调用yield会让当前线程从Runming进入Rummable状态,然后调度执行其它同优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果
  • 具体的实现依赖于操作系统的任务调度器

总结:调用这两个方法之后,线程进入的状态不同!sleep()进入阻塞状态,yield()进入就绪状态!

线程优先级

  • 线程优先级会提示调度器优先调度该线程,但是他仅仅是一个提示,调度器可以忽略他
  • 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没什么作用
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");
    }
}

3.8join方法讲解

下面代码执行完,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运行结束。

应用案例之同步(案例1)

以调用方角度来讲,如果:

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果范湖,就能继续运行就是异步

下面的方法示例就是一个同步的例子,主线程等着其他线程运行结束才打印最终的结果

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

3.9打断

打断处于阻塞状态的线程(sleep、join、wait)

@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)

两阶段终止(待补充)

3.10不推荐的方法

3.11主线程与守护线程

默认情况下,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

注意:

  • 垃圾回收线程也是一种守护线程
  • Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等他们处理完当前请求会立马结束。

3.12五种状态(CPU层面)

黑马-并发编程讲解_第2张图片

3.13六种状态(Java API层面)

根据Thread.State枚举,分为六种状态
黑马-并发编程讲解_第3张图片

  • NEW:线程刚被创建,还没有调用start()方法
  • RUNNABLE:当调用了start()方法之后,注意:JavaAPI层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】、【阻塞状态】
  • TERMINATED:线程时间片用完,死亡

3.14应用案例之统筹(烧水泡茶)

问题详情可百度

@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();

    }
}

4、共享模型之管程

4.1共享带来的问题

临界区

一个程序运行多个线程本身是没有问题的,问题出现在多个线程访问共享资源,多个线程访问共享资源也没有问题,但是在多个线程对共享资源读写操作时发生指令交错,就会出现问题,一段代码块内如果存在对共享资源的多线程读写操作,则称这块代码为临界区,注意是代码块!

public class Temp {
    static int count = 0;

    public static void main(String[] args) {
        
    }
    
    // 临界区
    static void add(){
        count++;
    }
    
    // 临界区
    static void sub(){
        count--;
    }
}

竞态条件

多个线程在临界区内执行,由于代码的执行顺序不同而导致结果无法预测,称之为发生了竞态条件

4.2synchronized解决方案

应用之互斥

为了避免临界区的静态条件发生,有多重手段可以达到目的

  • 阻塞式解决方案:synchronized、lock
  • 非阻塞式解决方案:原子变量

下面使用阻塞式解决方案:synchronized来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程再想获取这个【对象锁】时就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区的代码,不用担心线程上下文的切换。

注意:虽然Java中互斥和同步都可以采用synchronized关键字来完成,但他们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程的执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点

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实际是用对象锁保证了临界区代码的原子性,临界区内的代码是不可分割的,不会被线程切换所打断。

4.3方法上的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){

        }
    }
}

4.4变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果他们没有共享,则线程安全
  • 如果他们被共享了,根据他们的状态是否能够改变,又分两种情况
    • 如果之后读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但是局部变引用的对象则未必
    • 如果该对象没有逃离方法的作用和访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量和成员变量线程安全分析

局部变量时

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,但是子类中的线程和父类中的线程却不是同一个,此时发生了资源共享,就会产生线程安全问题

线程安全的类

  • String(String的replace、substring等方法虽然可以改变值,其原理是复制一份老的String值,然后在新的String值上做修改,所以Stirng一直都是不可变得)
  • Integer
  • StringBuffer
  • Random
  • Vector
  • HashTable
  • JUC

这里说他们是线程安全的是指:多个线程调用他们同一个实例的某个方法时,是线程安全的,但是多个方法组合时,就不是线程安全的了。比如同时调用HashTable的get()和put()方法时,就不是线程安全的,HashTable只能保证get()和put()各自的安全!

4.5习题

卖票

转账

4.6Monitor概念

Java对象在内存中一般由两部分组成Java对象头、对象中的成员变量

Java对象头

以32位虚拟机为例

普通对象黑马-并发编程讲解_第4张图片
其中Mark Word的结构为黑马-并发编程讲解_第5张图片

Monitor(可以认为是锁)

Monitor被翻译为监视器管程

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word就指向Monitor对象,由一个指针关联

Monitor结构如下(这个图和解释很重要!):
黑马-并发编程讲解_第6张图片

给对象上锁的流程:当一个线程执行被synchrd(obj)修饰的代码块时,就会尝试将obj对象与操作系统提供的Monitor对象相关联,关联的方式是依靠一个指针指向Monitor对象,此时这个线程就成为了这个Monitor对象的所有者,也就是获取到了锁,当再有线程来执行synchrd(obj)代码块时,首先会判断obj对象有没有关联Monitor对象(monitorenter),再看这个Monitor对象有没有主人(Owner指向的线程),有的话就会和Monitor中的EntryList相关联从而进入BLOCKED状态。当Owner指向的线程执行完之后,就会按照一定的规则从EntryList中获取一个线程并与之关联起来,这个线程也就获得了锁。

  • 刚开始Monitor中的Owner为null
  • 当Thread-2执行synchronized(obj)代码块时就会将Monitor的所有者Owner置位Thread-2,Monitor中只能有一个Owner
  • 在Thread-2上锁的过程中,如果Thread-3、Thread-4也来执行synchronized(obj),就会进入Monitor的EntryList进入BLOCKED状态
  • 当Thread-2执行完同步代码块的内容之后,会唤醒Monitor中EntryList中等待的线程来竞争锁,竞争是非公平的
  • 图中WaitSet中的Thread-0、thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面讲wait-notify时会分析。

注意:
1、synchronized必须是进入同一个对象的Monitor才有上述效果(重要提示:每一个对象都会关联一个Monitor
2、不加synchronized的对象不会关联监视器,不遵从以上规则

原理之synchronized

示例代码:

public class Temp {
    static final Object lock = new Object();
    static int counter = 0;

    public static void main(String[] args) {
        synchronized (lock){
            counter++;
        }
    }
}

对应的字节码为:
黑马-并发编程讲解_第7张图片
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进阶

1、轻量级锁

轻量级锁的适用场景:如果一个对象虽然有多个线程访问,但多线程访问时错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍是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

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁对象的Mark Word

黑马-并发编程讲解_第8张图片
首先回忆一下对象的结构:对象头和对象体。对象头又由Mark Word和Klass Word组成,Mark Word中包含哈希码、分代年龄、锁状态等,这里重点讨论的就是锁状态的变化。【下面开始解释图】当一个线程执行到m1的synchronized块时,首先在线程的栈帧中产生一个Lock Record(锁记录)对象(上面左图),这个对象包含两个部分:加锁对象(比如上面代码中的lock)指针和加锁对象的Mark Word

  • 让锁记录中的Object reference指向加锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录
    黑马-并发编程讲解_第9张图片
    这一步的重点是:要将线程的锁记录和加锁对象的Mark Word交换,交换成功就是表示加锁成功

  • 如果CAS替换成功(成功的条件是所对象的锁标志位为01,如果已经为00了,会失败),对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁
    黑马-并发编程讲解_第10张图片
    此时对象头的Mark Word中就不再是哈希码那些信息了,而是变成了锁记录的地址以及锁类型,而锁记录里则变成了对象的哈希码等信息,解锁的时候再恢复回去

  • 如果CAS失败,有两种情况
    1、如果是其他线程已经持有了该Object的轻量级锁(锁标志位为00),这时表名有竞争,进入锁膨胀过程
    2、如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数(下面会有偏向锁来优化这个)
    黑马-并发编程讲解_第11张图片

  • 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入(自己重入,注意是自己),这时重置锁记录,表示重入计数减一

黑马-并发编程讲解_第12张图片

  • 当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用CAS将Mark Word的值恢复给对象头。
    1、成功:表示解锁成
    2、失败:说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁的解锁流程

2、锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法返回成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

static final Object obj = new Object();
    public static void m1(){
        synchronized (obj){
            // 同步块A
        }
    }
  • 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁

黑马-并发编程讲解_第13张图片

  • 这时Thread-1加轻量级锁失败,进入锁膨胀过程
    1、即为Object对象申请Monitor锁(说明轻量级锁还没涉及到Monitor),让Object的Mark Word指向重量级锁地址
    2、然后自己进入Monitor的EntryList,处于BLOCKED状态
    黑马-并发编程讲解_第14张图片
  • 当Thread-0退出同步代码块时,使用CAS将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程

3、自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化(进入Monitor的EntryList之前),如果当前线程自选成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞

自旋重试成功的情况:
黑马-并发编程讲解_第15张图片
自旋尝试失败的情况:
黑马-并发编程讲解_第16张图片
在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

自旋次数的默认值10次,用户可以使用参数 -XX:PreBlockSpin来更改

4、偏向锁

轻量级锁在没有竞争时(只有一个线程),每次重入仍然需要执行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
        }
    }

轻量级锁和偏向锁的加锁对比:
黑马-并发编程讲解_第17张图片

黑马-并发编程讲解_第18张图片

偏向状态

回忆一下对象头格式
黑马-并发编程讲解_第19张图片一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord值最后3位为101,这时他的thread,epoch,age都是0
  • 偏向锁时默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以使用JVM参数-xx:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,MarkWord值最后3位为001,这时他的hashcode、age都为0,第一次用到hashcode时才会赋值。
撤销-调用对象hashcode

调用了对象的hashcode之后,对象的偏向锁将被撤销。轻量级锁会在锁记录中记录hashcode,重量级锁会在Monitor中记录hashcode。

撤销-其他线程使用对象

当多个线程使用同一个对象时(注意:此时没有竞争),会撤销偏向锁(也就是将偏向状态由可偏向变为不可偏向!)同时将偏向锁升级为轻量级锁。

重要总结:偏向锁时,锁对象的MarkWord中存储的是偏向的线程id;轻量级锁时,锁对象的MarkWord中记录的是锁记录指针

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的偏向锁仍然后机会重新偏向T2,重偏向会重置锁对象偏向的线程ID。

当撤销偏向锁的阈值超过20次后,JVM会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。

批量撤销

当撤销偏向锁阈值超过40次之后,JVM会这样觉得,自己确实是偏向错了,根据就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

4.7wait-notify

原理之wait-notify

黑马-并发编程讲解_第20张图片

  • Owner线程(已经获取到Monitor锁的线程)发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
  • BLOCKED线程会在Owner线程释放锁时唤醒
  • WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,扔需要进入EntryList重新竞争

API介绍

  • obj.wait()让进入object监视器的线程(再强调一遍:即是已经获得锁的线程)到WaitSet中等待 -
  • obj.wait(long timeout)让进入object监视器的线程(再强调一遍:即是已经获得锁的线程)到WaitSet中等待指定的时间,时间结束后线程会继续执行下面的代码
  • obj.notify()在object上正在WaitSet中等待的线程中挑一个唤醒,不能指定某个,随机唤醒!
  • obj.notifyAll()让在object上正在WaitSet中等待的线程全部唤醒

他们都是线程之间进行协作的手段,都属于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就指向哪个线程

4.8wait-notify的正确姿势

开始之前先看sleep(long n)和wait(long n)的区别

  • sleep是Thread的方法,而wait是Object的方法
  • sleep不需要强制配合synchronized使用,而wait需要和synchronized一起使用
  • sleep在睡眠的同时,不会释放对象锁,但wait在等待的是会释放对象锁

使用方式总结:

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();
        }
    }
}

这样使用的好处是避免了虚假唤醒,唤醒的线程满足条件可以立即执行下面的代码,不满足的则继续等待

异步模式之生产者消费者

黑马-并发编程讲解_第21张图片

@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 +
                '}';
    }
}

4.9Park&Unpark

基本使用

他们是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相比

  • wait、notify和notifyAll必须配合Object Monitor一起使用,而unpark不必
  • park&unpark是以线程为单位来【阻塞】和【唤醒】线程的,而notify只能随机唤醒一个等待的线程,notifyAll是唤醒所有线程,就不那么精确
  • park&unpark可以先unpark,而wait¬ify不能先notify

原理之park&unpark

每个线程都有一个Parker对象,由三部分组成:_counter、_cond和_mutex

先调用park再调用unpark的情况:
黑马-并发编程讲解_第22张图片
1、调用Unsafe.unpark(Thread-0)方法,设置_counter为1
2、唤醒_cond条件变量中的Thread_0
3、Thread_0恢复运行
4、设置_counter为0

先调用unpark再调用park的情况:

黑马-并发编程讲解_第23张图片

1、调用Unsafe.unpark(Thread_0)方法,设置_counter为1
2、当前线程调用Unsafe.park()方法
3、设置_counter,本情况为1,这时线程无需阻塞,继续运行
4、设置_counter为0

4.10重新理解线程状态转换

黑马-并发编程讲解_第24张图片
假设有线程Thread t

情况1 NEW --> RUNNABLE

  • 当调用t.start()方法时,由NEW --> RUNNABLE

情况2 RUNNABLE --> WAITING

t线程用synchronized(obj)获取对象锁后

  • 调用obj.wait(),t线程从RUNNABLE --> WAITING
  • 调用obj.notify(),t.interrupt()时
    竞争锁成功,t线程从WAITING --> RUNNABLE
    竞争锁失败,t线程从WAITING --> BLOCKED

情况3 RUNNABLE --> WAITING

  • 当前线程调用t.join()方法时,当前线程从RUNNABLE --> WAITING
    注意是当前线程在t线程对象的监视器上等待
  • t线程运行结束,或调用了当前线程的interrupt()时,当前线程从WAITING --> RUNNABLE

情况4 RUNNABLE --> WAITING

  • 当前线程调用LockSupport.park()方法会让当前线程从RUNNABLE --> WAITING
  • 调用LockSupport.unpark(目标线程)或调用了线程的interrupt(),会让目标线程从WAITING --> RUNNABLE

情况5 RUNNABLE --> WAITING
t线程用synchronized(obj)获取对象锁后

  • 调用obj.wait(long n)方法时,t线程从RUNNABLE --> TIMED_WAITING
  • t线程等待时间超过了n毫秒,或调用obj.notify()、t.interrupt()时
    竞争锁成功,t线程从TIMED_WAITING --> RUNNABLE
    竞争锁失败,t线程从TIMED_WAITING --> BLCOKED

情况6 RUNNABLE --> TIMED_WAITING

  • 当前线程调用t.join(long n)方法时,当前线程从RUNNABLE --> TIMED_WAITING
    注意是当前线程在t线程对象的监视器上等待
  • 当前线程等待时间超过了n毫秒,或t线程运行结束,或调用了当前线程的interrupt()时,当前线程从TIMED_WAITING --> RUNNABLE

情况7 RUNNABLE --> TIMED_WAITING

  • 当前线程调用Thread.sleep(long n),当前线程从RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了n毫秒,当前线程从TIMED_WAITING --> RUNNABLE

情况8 RUNNABLE --> TIMED_WAITING

  • 当前线程调用LockSupport.parkNanos(long nanos)或LockSupport.parkUnitl(long millis)时,当前线程从RUNNABLE --> TIMED_WAITING
  • 调用LockSupport.unpark(目标线程)或调用了线程的interrupt(),或是等待超时,会让目标线程从TIMED_WAITING --> RUNNABLE

情况9 RUNNABLE --> BLCOKED

  • t线程用synchronized(obj)获取对象锁时如果竞争失败,从RUNNABLE --> BLCOKED
  • 持有obj锁的线程的同步代码块执行完毕,会环形该对象上所有BLOCKED的线程重新竞争,如果其中t线程竞争成功,从BLCOKED --> RUNNABLE ,其他线程仍然BLCOKED

情况10 RUNNABLE --> TERMINATED
当前线程所有代码执行完毕,进入TERMINATED

4.11多把锁

4.12活跃性

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

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();
    }
}

解决办法:将睡眠时间调整,使用随机睡眠时间

饥饿

4.13ReentrantLock

相比于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的打断状态,以便后续根据打断状态进行停止线程或其他操作

  • lock是可中断锁,而synchronized 不是可中断锁
  • 线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
  • 如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
  • 如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情

锁超时

提供一种主动的方式来避免死锁(打断是被动方式)。线程在获取锁的过程中,如果持有锁的线程一直没有释放锁,尝试获取锁的线程超过设置的时间之后就放弃等待,表示获取锁失败,可以避免线程无限等待。

@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强大之处在于:它是支持多个条件变量的,这就好比

  • synchronized是那些不满足条件的线程都在一间休息室等待
  • 而ReentrantLock支持多间休息室

使用流程:

  • await前需要获得锁
  • await执行后,会释放锁,进入conditionObject等待
  • await的线程被唤醒(打断、超时)去重新竞争lock锁
  • 竞争lock锁成功后,从await后继续执行
@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);
        }
    }
}

本章小结

本章需要重点掌握的是:

  • 分析多线程访问共享资源时,那些代码片段属于临界区
  • 掌握synchronized互斥解决临界区的线程安全问题
    • 掌握synchronized锁对象语法
    • 掌握synchronized加载成员方法和静态方法语法
    • 掌握wait-notify同步方法
  • 使用lock互斥解决临界区的线程问题
    -掌握lock的使用细节:可打断、锁超时、公平锁、条件变量
  • 学会分析变量的线程安全性,掌握常见线程安全类的使用
  • 了解线程活跃性的问题:死锁、活锁、饥饿
  • 应用方面
    • 互斥:使用synchronized或Lock达到共享资源互斥效果
    • 同步:使用wait-notify或Lock的条件变量来达到线程Jan通信效果
  • 原理方面
    • monitor、synchronized、wait-notify原理
    • synchronized进阶原理
    • park-unpark原理
  • 模式方面
    • 同步模式之保护性暂停
    • 异步模式之生产者消费者
    • 同步按模式之顺序控制

5、共享模型之内存

5.1Java内存模型

JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等

JMM体现在一下几个方面

  • 原子性:保证指令不会受到线程上下文切换的影响
  • 可见性:保证指令不会受到CPU缓存的影响
  • 有序性:保证指令不会受到CPU指令并行优化的影响

5.2可见性

退不出的循环

先来看一个现象,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的值到工作内存
黑马-并发编程讲解_第25张图片
2、因为t线程要频繁从主内存中读取run的值,JIT编译期会将run的值缓存至自己的高速缓存中,减少对主内存中run的访问,提高效率
黑马-并发编程讲解_第26张图片
3、1秒之后,main线程修改了run值,并同步至主内存,而t是从自己工作内存的中的高速缓存中读取这个变量的值,结果永远是旧值
黑马-并发编程讲解_第27张图片

解决办法

添加volatile关键字,volatile关键字修饰的变量每次读取不能从自己内存的高速缓存中读取,要从主内存中读取!

他可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主内存中获取他的值,线程操作volatile变量都是直接操作柱内存。

可见性VS原子性

上面例子体现的实际就是可见性,他保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,但是不能保证原子性,仅能用在一个写线程,多个读线程的情况

比较一下之前讲线程安全时举的例子:两个线程一个i++,一个i–,只能保证看到最新值,不能解决指令交错的问题

注意:synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是synchronized是属于重量级操作,性能相对较低

5.3有序性

JVM会在不影响正确性的前提下,对语句执行顺序进行调整,这种特性个称之为指令重排,多线程下指令重排会影响正确性。

为什么会有指令重排这项优化呢?下面从CPU指令层面理解:现代CPU支持多级指令流水线,例如支持同时执行取指令-指令y译码-执行指令-内存访问-数据协会的处理器,就可以成为五级指令流水线。这时CPU可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执时间最长的复杂指令),本质上流水线技术不能缩短单条指令的执行时间,但他变相提高了指令的吞吐率。

对变量添加volatile关键字,即可禁止指令重排

5.4volatile原理

volatile的底层实现原理是内存屏障(Memory Barrier)

  • 对volatile变量的写指令后会加入写屏障
  • 对volatile变量的读指令前会加入读屏障

内存屏障出现背景:

为了协调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
	}
}

黑马-并发编程讲解_第28张图片

如何保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
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
	}
}

还是那句话,不能解决指令交错(不能解决原子性)

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到他前面去
  • 而有序性的保证也是保证了本线程内先关代码不被重排序

黑马-并发编程讲解_第29张图片

本章小结

本章重点讲解了JMM中的

  • 可见性
  • 有序性
  • happens-before规则

原理方面

  • CPU指令并行
  • volatile关键字

模式方面

  • 两阶段终止模式的volatile改进
  • 同步模式之balking

6、共享模式之无锁

6.1问题提出

6.2CAS与volatile

前面看到AtomicInteger的解决办法,内部并没有用锁来保护共享边浪的线程安全,那么他是怎么实现的呢?

public void withDrow(Integer amount){
        while (true){
            int prev = balance.get();
            int next = balance - amount;
            // 比较并设置值(原子性 的)
            if (balance.compareAndSet(prev,next)){
                break;
            }
        }
    }

其中的关键是
compareAndSet,他的简称是CAS,他是原子操作
黑马-并发编程讲解_第30张图片

volatile

获取变量时,为了保证该变量的可见性,需要volatile修饰
他可以用来修饰成员变量和静态变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主内存中获取他的值,线程操作volatile变量都是直接操作主存。即一个线程对volatile变量的修改,对另一个线程可见。

CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果

**总结:AtomicInteger中的变量是被volatile修饰的,保证了线程之间的可见性。 **

为什么无锁效率高

无锁情况下,即使重试失败,线程始终在高速运行没有停歇,而synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞(进入上述的EntryList)。

CAS的特点

结合CAS和volatile可以实现无锁并发,适用于线程少、多核CPU的场景下(线程数不要多于核心数)。

  • CAS是基于乐观锁的思想:最乐观地估计,不怕别的线程来修改共享变量,就算修改了也没关系,大不了再重试
  • synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们谁都别想修改,我改完了解锁,你们才有机会
  • CAS体现的是无锁并发,无阻塞并发,仔细体会这两句话的意思
    • 因为没有使用synchronized,所以线程不会陷入阻塞,这时效率提升的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受到影响

6.3原子整数

JUC并发包提供了

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

以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
    }
}

6.4原子引用

为什么需要原子引用类型?因为受保护的数据类型不一定是基本类型!

  • AotmicReference
  • AtomicMarkavleReferencr
  • AtomicStapmedReference
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());
    }
}

ABA问题

主线程仅仅能判断出共享变量的值与最初值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

6.5原子数组

6.6字段更新器

  • AtomicReferenceFieldUpdater 域/字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(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 + '\'' +
                '}';
    }
}

6.7原子累加器

LongAdder

6.8Unsafe

本章小结

  • CAS与volatile
  • API
    • 原子整数
    • 原子引用
    • 原子数组
    • 字段更新器
    • 原子累加器
  • Unsafe

7、不可变对象

8、共享模型之工具

8.1线程池

1、自定义线程池

2、ThreadPoolExecutor

线程池状态

ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量

黑马-并发编程讲解_第31张图片
这些信息存储在一个原子变量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:核心线程数目(最多保留线程数)
  • maxPoolSize:最大线程数目(核心线程数+救急线程数
  • keepAliveTime:生存时间-针对救急线程
  • unit:时间单位-针对救急线程
  • workQueue:阻塞队列(核心线程用完了,新的任务就会进入阻塞队列中等待
  • threadFactory:线程工厂-可以为线程创建时起个好名字
  • 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工具类中提供了众多工厂方法来创建各种用途的线程池

newFixedThreadPool

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

newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

特点:

  • 核心线程数是0,最大线程数是Integer.MAX_VALUE,救急线程的空闲时间是60s,意味着
    • 全部都是救急线程(60s后可以回收)
    • 救急线程可以无限创建
  • 队列采用了SynchronousQueue,实现特点是他没有容量,没有线程来取是放不进去的(一手交钱,一手交货)

评价:整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲1分钟后释放线程。适合任务比较密集,但每个任务执行时间较短的线程。

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

使用场景:希望多个任务排队执行。线程数固定为1,任务多于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> 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> tasks,long timeout,TimeUnit unit)

        // 提交tasks中所有任务,哪个任务先执行完毕,返回次任务执行结果,其他任务取消
        //  T invokeAny(Collection 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

  • 线程池状态变为SHUTDOWN
  • 不会接收新任务
  • 已提交的任务会执行完
  • 此方法不会阻塞调用线程的执行
void shutdown();

shutdownNow

  • 线程池状态变为STOP
  • 不会接收新任务
  • 会将队列中的任务返回
  • 并用interrupt的方式中断正在执行的任务
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的有点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都会影响到之后的任务。

newScheduledThreadPool

带延迟功能的线程池,前面的线程发生异常也不会影响后面的线程

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的整体架构图
黑马-并发编程讲解_第32张图片

  • LimitLatch用来限流,可以控制最大连接个数,类似JUC中的Semaphore
  • Acceptor只负责【接收新的socket连接】
  • Poller只负责监听socket channel是否有【可读的IO事件】
  • 一旦可读,封装一个任务对象(socketProcessor),提交给Executor线程池处理
  • Executor线程池中的工作线程最终负责【处理请求】

Tomcat线程池扩展了ThreadPoolExecutor,行为稍有不同

  • 如果总线程数达到maximumPoolSize
    • 这时不会立刻抛出RejectedExecutionException
    • 而是再次尝试将任务放入队列,如果还失败,才抛出RejectedExecutionException异常

3、Fork/Join

概念

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;
    }
}

8.2JUC工具类

1、AQS原理

概述

全称是AbtractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架

特点

  • 用state属性表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取释放锁
    • getState-获取state状态
    • setState-设置state状态
    • compareAndSetState-乐观锁机制设置state状态
    • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
  • 提供了基于FIFO的等待队列,类似于Monitor的EntryList
  • 条件变量来实现等待-唤醒机制,支持多个条件变量,类似于Monitor的WaitSet

子类主要实现这样一些方法(抛出UnsupportedOperationException)

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively(是否持有独占锁)

获取锁的姿势

// 如果获取锁失败
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();
    }
}

2、ReentrantLock原理

P238-P46

3、读写锁

ReentrantReadWriteLock(支持重入的读写锁)

当读操作远远高于写操作时,这时候可以使用读写锁让读-读支持高并发,提高性能

类似于数据库中的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

4、semaphore

5、CountdownLatch(倒计时锁)

用来进行线程同步协作,等待所有线程完成倒计时
其中构造参数用来初始化等待计数值,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...");
        });
        
    }

6、CyclicBarrier

循环栅栏,用来进行线程协作,等待线程满足某个计数器。构造时设置【计数个数】,每个线程执行到某个需要“同步”的时刻调用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();
    }
}

7、线程安全的集合类

概述

线程安全的集合类可以分为三大类

  • 早期的线程安全集合如HashTable、Vector
  • 使用Collections装饰的线程安全集合如Collections.synchronizedList、Collections.synchronizedMap
  • java.util.concurrent.*下的线程安全集合类,里面包含三类关键词:Blocking、CopyOnWrite、Concuttent
    • Blocking大部分实现基于锁,并提供用来阻塞的方法
    • CopyOnWrite之类的集合修改开销相对较重
    • Concurrent类型的容器:内部很多使用cas优化

你可能感兴趣的:(面试题集锦,java)