黑马并发编程JUC总结

并发编程总结1

并发编程

(图片来源黑马JUC教程)

2.进程和线程

2.1定义

进程:资源分配最小的单位,主要就是线程的容器。运行的程序,负责管理IO和内存,以及指令的执行

线程:其实就是指令流,进程的子集,进程可以分多个线程运行。

区别:

  • 进程是最小资源分配,线程是最小调度(指的是cpu切换指令流执行的调度)
  • 线程是进程的子集
  • 同样的计算机里面进程通讯是IPC方式,不同计算机就要通过协议比如HTTP
  • 线程切换的成本更低,比如CPU需要并发执行进程或者是线程那么线程比较轻量切换消耗CPU时间也更小。

2.2并发和并行

定义

并发:cpu同时执行多个线程,交替执行。好比打扫一会就去洗洗厕所

并行:多个cpu执行多个线程。多个饭煲一起煮饭的意思

image-20211011194907606

2.3应用

异步调用

其实就是在一个程序执行的时候,一段指令流需要阻塞或者是IO操作,那么完全可以开一个线程来异步执行这段指令流。而不是一个进程完成所有的指令处理。

  • 常用在视频格式转换

并发应用

并发不一定能够提升效率,反而在来回切换的时候浪费了资源和时间。有的任务也不适合切分,主要还是看任务的执行目标

3.java线程

3.1线程创建

创建线程方法1

Thread重写run方法

@Slf4j(topic = "c.MyTest1")
public class MyTest1 {
    
    
    public static void main(String[] args) {
    
    
        Thread t=new Thread(){
    
    
            @Override
            public void run() {
    
    
                log.info("好人");
            }
        };
        t.setName("线程1");
        t.start();

        log.info("main测试");
    }
}

创建方法2

直接写一个Runable传给线程Thread

@Slf4j(topic = "c.Test2")
public class Test2 {
    
    
    public static void main(String[] args) {
    
    
        Runnable r = () -> {
    
    log.debug("running");};

        Thread t = new Thread(r, "t2");

        t.start();
    }
}

Thread和Runable的区别

Thread实现了Runable方法,也就是说Thread可以覆盖Runable的任务方法执行run。但是也可以通过传入任务来执行对应的Runable方法。

 @Override
    public void run() {
    
    
        if (target != null) {
    
    
            target.run();
        }
    }

创建方法3

FutureTask实现了Runnable和Future,Future主要是用来返回值的。其实相当于也是一个任务类但是需要实现Callable实现类对象传入到task上。并且执行之后才能够使用task的get来获取数据,如果没有执行线程那么get就会阻塞

@Slf4j(topic = "c.MyTest2")
public class MyTest2 {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        FutureTask<Integer> task=new FutureTask<Integer>(new Callable<Integer>() {
    
    
            @Override
            public Integer call() throws Exception {
    
    
                log.debug("你好");
                return 4;
            }
        });

        Thread t=new Thread(task,"t1");
        t.start();
        log.debug("{}",task.get());

    }
}

总结:更推荐方法2,原因是Runable就是任务,把任务和线程分开才能更好的分配任务。

3.2线程运行

命令

jps:查看java进程

tasklist:window查看所有进程

taskkill /F /PID XXX:杀死某个进程

3.3线程运行

线程的栈

每次开启一个线程都会产生一个线程的栈给线程使用,实际上就是一开始分配的虚拟机栈。

  • 每个栈都有多个栈帧
  • 线程只能有一个活动栈帧

线程执行的过程

先分配栈给线程,线程调用main方法,在栈分配一个栈帧给main方法,栈帧保存锁记录、局部变量表、操作数栈、返回地址(返回到原来的栈帧方法的下一条指令)

image-20211011215014942

线程上下文切换

导致上下文切换的条件

  • 时间片用完
  • 优先级
  • 垃圾回收
  • 自己调用sleep,wait等

线程的状态包括

  • 程序计数器,记录执行到什么位置
  • 虚拟机栈,包括所有的栈帧信息

常见方法

start

线程进入就绪状态,等待调度器调用。这里相当于是开启了一个新的线程

run

执行Runnable里面的方法。这里并没有开启线程,只是通过本线程执行代码

如果开启了两次start就会出现线程状态异常的问题IllegalThreadStateException

线程的两个状态

  • NEW
  • start之后就是Runnable等待被调度

sleep

线程睡眠,并且把状态改为Timed Waiting。被打断的时候会抛出异常InterruptedException。可以通过TimeUnit.SECONDS.sleep来规定睡眠时间的单位。睡眠可以使用在while循环自转的地方,如果长时间自转就会消耗CPU的使用时间,其它线程无法使用

interrupt

唤醒线程,如果线程处于睡眠状态那么就会抛出异常

public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread("t1") {
    
    
            @Override
            public void run() {
    
    
                log.debug("enter sleep...");
                try {
    
    
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
    
    
                    log.debug("wake up...");
                    e.printStackTrace();
                }
            }
        };
        t1.start();

        Thread.sleep(1000);
        log.debug("interrupt...");
        t1.interrupt();
    }
@Slf4j(topic = "c.Test8")
public class Test8 {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        log.debug("enter");
        TimeUnit.SECONDS.sleep(1);
        log.debug("end");
//        Thread.sleep(1000);
    }
}

yield

其实就是把线程状态从Running转变到Runnable暂时让出cpu,重新去竞争

setPriority

设置优先级是给调度器进行提示,先执行这个线程,但是仍然没有办法控制线程。

join

join实际上就是卡点,就是一定要等待调用join的线程完成之后才能够执行下面的代码

@Slf4j(topic = "c.Test10")
public class Test10 {
    
    
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
    
    
        test1();
    }
    private static void test1() throws InterruptedException {
    
    
        log.debug("开始");
        Thread t1 = new Thread(() -> {
    
    
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        },"t1");
        t1.start();
        t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}

同步应用案例

多线程的join只需要等待最长的那个线程就可以了,因为他们都是并行执行的。如果是t1先的话,那么就是t1在处理1秒的同时,t2也在处理一秒,然后t2执行join,那么还只剩下1s处理,那么t1和t2也只需要等待多1s就能够继续执行主线程的。

如果是t2先那么在处理2s的同时,t1已经先处理了1s并且进入了等待状态,等待t2处理完那么就可以执行主线程了。在t2的join end的同时,t1处理完,并且也会执行join end。

简单来说就是两个线程开启,如果下面两个都是join,那么实际上两个线程都会各自执行,互不感染,直到最后一个线程执行完,那么才会执行join下面的代码。

test3案例,实际上就是限时等待,如果超过时间那么就不等了。如果没有超过时间,那么结束join还是以处理完线程的任务时间为主,而不是最大的等待时间

image-20211012113346065

image-20211012113839537

@Slf4j(topic = "c.TestJoin")
public class TestJoin {
    
    
    static int r = 0;
    static int r1 = 0;
    static int r2 = 0;

    public static void main(String[] args) throws InterruptedException {
    
    
        test2();
    }

    public static void test3() throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            sleep(2);
            r1 = 10;
        });

        long start = System.currentTimeMillis();
        t1.start();

        // 线程执行结束会导致 join 结束
        log.debug("join begin");
        t1.join(3000);
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }

    private static void test2() throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            sleep(1);
            r1 = 10;
        });
        Thread t2 = new Thread(() -> {
    
    
            sleep(2);
            r2 = 20;
        });
        t1.start();
        t2.start();
        long start = System.currentTimeMillis();
        log.debug("join begin");

        t1.join();
        log.debug("t1 join end");
        t2.join();
        log.debug("t2 join end");
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }

    private static void test1() throws InterruptedException {
    
    
        log.debug("开始");
        Thread t1 = new Thread(() -> {
    
    
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        });
        t1.start();
        t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}

interrupt

打断阻塞

打断sleep和wait。打断后会抛出异常但是不会给它打上打断标记。而且这里需要给主线程睡眠一会,不然t1线程还没睡眠,主线程就已经调用打断,那么这个时候的打断是打断t1,并且加上打断标记,但是打断睡眠并不会有打断标记

@Slf4j(topic = "c.Test11")
public class Test11 {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            log.debug("sleep...");
            try {
    
    
                Thread.sleep(5000); // wait, join
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        },"t1");

        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}", t1.isInterrupted());
    }
}

打断正常

当我们打断的是正常的线程,那么就会给这个线程加上打断标记。但是要不要打断取决于被打断线程的意愿,其它线程只能给一个通知。

@Slf4j(topic = "c.Test12")
public class Test12 {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            while(true) {
    
    

                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted){
    
    
                    break;
                }
            }
        }, "t1");
        t1.start();

        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
    }
}
两阶段终止模式

其实就是t1终止t2,给t2料理后事的时间,处理锁的问题等

错误的终结方式

①stop()线程可能导致锁住的某些共享资源,锁没有释放。

②System.exit(0)让整个程序停止而不是一个线程

流程

先看看有没有被打断,有那么就料理后事结束循环,没有那么就睡眠2s,没有异常那么就记录,如果是在睡眠过程中被打断那么就设置好打断的标记,重新去到while循环。如果没有重新标记,那么线程只是打断之后抛出了异常,但是并没有终结线程的执行,因为别的线程只能够通知线程打断的提示,这个提示就是打断阻塞之后抛出异常,但是没有对被打断线程做打断标记,所以这个时候是很有必要给线程重新打断做上标记,那么下一次循环才能够料理后事结束循环。

拓展

这个地方如果刚好执行到log.debug(“执行监控记录”);也就是不是处于睡眠状态被打断,最后就会标记上,并且直接退出程序。

还有就是interrupted是会消除标记的。

@Slf4j(topic = "c.TestTwo")
public class TestTwoPhase {
    
    
    public static void main(String[] args) {
    
    
        TwoPhase twoPhase = new TwoPhase();
        log.debug("开始执行");
        twoPhase.start();
        try {
    
    
            Thread.sleep(3500);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        twoPhase.stop();
    }
}
@Slf4j(topic = "c.TestPhrase")
class TwoPhase{
    
    
    private Thread monitor;

    public void start(){
    
    
        monitor=new Thread(()->{
    
    
            while(true){
    
    
                Thread thread = Thread.currentThread();
                if(thread.isInterrupted()){
    
    
                    log.debug("料理后事");
                    break;
                }

                try {
    
    
                    Thread.sleep(1000);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                    log.debug("重新打上标记");
//                    thread.interrupt();
                }
            }

        },"t1");
        monitor.start();
    }

    public void stop(){
    
    
        monitor.interrupt();
    }
}

image-20211012120911061

LockSupport

其实就是一个锁的支持类,它的方法park可以模拟sleep把线程进行阻塞,但是需要标记是false的时候。如果打断标记是true那么就无法使用。但是这个地方可以使用Thread.interrupted来获取打断标记状态和消除标记。

 public static void test5() {
    
    
        Thread t1 = new Thread(()->{
    
    
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.interrupted());
            LockSupport.park();
            log.debug("unpark...");

        },"t1");
        t1.start();

        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        t1.interrupt();


    }

3.4主线程和守护线程

守护线程其实就是Daemon也就是在其他非守护线程运行完之后,无论守护线程是否还有任务需要执行都会强制停止。

  • 垃圾回收器是守护线程
  • tomcat的Acceptor和Poller
@Slf4j(topic = "c.Test15")
public class Test15 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            while (true) {
    
    
                if (Thread.currentThread().isInterrupted()) {
    
    
                    break;
                }
            }
            log.debug("结束");
        }, "t1");
        t1.setDaemon(true);
        t1.start();

        Thread.sleep(1000);
        log.debug("结束");
    }
}

3.5线程5种状态

①初始:new线程的时候

②可运行:执行了start

③运行:线程可以使用cpu的时候

④阻塞:不能被cpu调度器使用的时候

⑤终止:线程结束的时候

image-20211012130538241

3.6线程的六种状态

NEW:初始化

RUNNABLE:包括了运行、可运行和阻塞,通常表示正在运行

WAITING:没有时间限制

TIMED_WAITING:有时限的等待

BLOCKED:阻塞

TERMINATED:终止

3.7烧水泡茶案例

join思路

其实就是老王洗烧壶和烧水,小王洗茶壶、茶杯、茶叶,等待烧水完成之后泡茶。可以使用join来等待t1处理。

问题

  • 老王想泡茶怎么办
  • 小王洗好茶叶需要交给老王怎么办
Slf4j(topic = "c.Test16")
public class Test16 {
    
    



    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            log.debug("洗茶壶");
            Sleeper.sleep(1);
            log.debug("烧水");
            Sleeper.sleep(5);
        }, "老王");


        Thread t2=new Thread(()->{
    
    
            log.debug("洗茶壶");
            Sleeper.sleep(1);
            log.debug("洗茶叶");
            Sleeper.sleep(2);
            log.debug("洗茶杯");
            Sleeper.sleep(1);

            try {
    
    
                t1.join();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            log.debug("泡茶");
        },"小王");

        t1.start();
        t2.start();


    }
}

总结

  1. 线程状态、线程创建、线程的api。
  2. 应用方面
    • 统筹规划
    • 异步
    • 效率
    • 同步等待join
  3. 原理
    • 线程的栈
    • 上下文切换
    • Thread创建的源码
  4. 模式
    • 两阶段终止

4.并发之共享模型

4.1多线程带来的共享问题

  • 共享资源在被两个线程使用的时候,可能,线程1要对变量i=0进行+1操作,但是在进行+1的途中切换交给了线程2,线程2也对变量i进行-1操作。然后把结果-1返回到共享变量。但是切换回线程1中的变量已经+1也就是局部变量已经是1,并且重新赋值给共享变量导致的并发问题
  • 第二个是共享资源如果一直被一个线程使用,线程可能会由于需要sleep,wait,io等操作浪费cpu的使用时间,那么可以把这段时间让给别人。

问题分析

这个地方实际上字节码分析是

getstatic i 获取静态变量i,也就是主存的i。

iconst_1 准备常量1

iadd 进行相加

pustatic i

比起我们看到的i++的背后它并不是一条原子指令,既然不是原子指令,那么线程自然可以在切换过程中,导致这些指令交错执行。最后导致的共享资源是个脏数据的问题。

image-20211012152027370

临界区

实际上就是多个线程访问的代码里面有共享资源,那么这段代码就是临界区

竞态条件

如果在临界区中多线程执行发生执行指令序列不同导致结果无法预测的状况就是竞态条件

4.2解决方案

①synchronize

线程1上锁之后,线程2无法获取锁不能够执行临时区,线程2阻塞等待线程1完成释放锁之后才能够使用。可以把synchronize类比成一个房间,每次有锁的人才能够进入房间做事情,就算cpu时间片用完,只要没有这个锁那么其他线程是无法进入房间的。当用完之后就会释放锁,并且唤醒那些阻塞的线程。

static int count=0;
    static Object lock=new Object();
    public static void main(String[] args) throws InterruptedException {
    
    
        Room room = new Room();
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 5000; i++) {
    
    
//                room.increment();
                synchronized (lock){
    
    
                    count++;
                }

            }
        }, "t1");

        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 5000; i++) {
    
    
//                room.decrement();
                synchronized (lock){
    
    
                    count--;
                }

            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
//        log.debug("{}", room.getCounter());
        log.debug("{}",count);
    }

image-20211012153626171

思考

①如果synchronize放到for外面?实际上就是要执行完整个for才会把锁放出去–原子性

②如果t1和t2使用的是不同的obj会怎么样?相当于进入了不同的房间,那么锁是没有效果的,两个线程仍然会各自执行临界区的代码块导致执行序列不同。

③如果t1加锁,但是t2没有?相当于就是t2可以随时执行临时区–所以锁对象一定要相同。

面向对象改进

实际上就是把那些需要加锁的临时区和共享资源全部封装到一个类上。在方法上面加synchronize相当于就是锁住了this。如果是静态方法那么相当于就是锁住了类对象.class

class Room {
    
    
    private int counter = 0;

    public synchronized void increment() {
    
    
        counter++;
    }

    public synchronized void decrement() {
    
    
        counter--;
    }

    public synchronized int getCounter() {
    
    
        return counter;
    }
}

4.3方法中的synchronize

线程八锁

情况1:没有sleep的时候n1执行a和b方法都是锁住自己的对象。所以是互斥的。

情况2:有sleep其实还是一样,执行a和b方法的两个线程看谁先获取到锁。那么谁就先执行。不管里面是不是有sleep,没获取到锁的线程就是要等待

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    
    
    public static void main(String[] args) {
    
    
        Number n1 = new Number();
//        Number n2 = new Number();
        new Thread(() -> {
    
    
            log.debug("begin");
            n1.a();
        }).start();
        new Thread(() -> {
    
    
            log.debug("begin");
            n1.b();
        }).start();
    }
}
@Slf4j(topic = "c.Number")
class Number{
    
    
    public synchronized void a() {
    
    
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
    
    
        log.debug("2");
    }
}

情况3:这种情况c是没有加锁,也就是随意都可以执行。可能出现的结果是3 12,也可能是32 1,也可能是23 1。

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    
    
    public static void main(String[] args) {
    
    
        Number n1 = new Number();
//        Number n2 = new Number();
        new Thread(() -> {
    
    
            log.debug("begin");
            n1.a();
        }).start();
        new Thread(() -> {
    
    
            log.debug("begin");
            n1.b();
        }).start();

        new Thread(() -> {
    
    
            log.debug("begin");
            n1.c();
        }).start();

    }
}
@Slf4j(topic = "c.Number")
class Number{
    
    
    public synchronized void a() {
    
    
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
    
    
        log.debug("2");
    }

    public void c(){
    
    
        log.debug("3");
    }
}

情况4:这种他们绑定的锁都是不同的,相当于就是无锁。最后的结果肯定是21。因为1线程sleep了

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -> {
            log.debug("begin");
            n1.a();
        }).start();
        new Thread(() -> {
            log.debug("begin");
            n2.b();
        }).start();

//        new Thread(() -> {
//            log.debug("begin");
//            n1.c();
//        }).start();

    }
}
@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
//
//    public void c(){
//        log.debug("3");
//    }
}

情况5-8都是加上了static,那么就根据他们锁的对象来判断是不是同一把锁就可以了

4.4变量的线程安全分析

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

如果只是读就没有,如果是读写那就要关注临界区

局部变量?

如果是引用类型的话那么就有。

局部变量的值存储在线程的栈帧里面,也就是私有的。而不是像static变量那样先从方法区中取出这个变量然后再进行对应的修改。

image-20211012161844837

情况1:ThreadUnsafe的list是在类里面创建的,那么就会造成,线程处理的是堆里面同一个list导致的线程安全问题

情况2:把list放到method1里面,那么就是一个局部变量的引用,而且每个线程调用方法后都有自己的一个list。那么就不会造成线程安全问题

情况3:有子类重写了这个方法那么也能获取这个list,导致多个线程能够操作list。解决办法就是给方法加上final防止子类重写。

public class TestThreadSafe {
    
    

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
    
    
//        ThreadSafeSubClass test = new ThreadSafeSubClass();
        ThreadUnsafe test=new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
    
    
            new Thread(() -> {
    
    
                test.method1(LOOP_NUMBER);
            }, "Thread" + (i+1)).start();
        }
    }
}
class ThreadUnsafe {
    
    
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
    
    
        for (int i = 0; i < loopNumber; i++) {
    
    
            method2();
            method3();
        }
    }

    private void method2() {
    
    
        list.add("1");
    }

    private void method3() {
    
    
        list.remove(0);
    }
}

class ThreadSafe {
    
    
    public final void method1(int loopNumber) {
    
    
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
    
    
            method2(list);
            method3(list);
        }
    }

    public void method2(ArrayList<String> list) {
    
    
        list.add("1");
    }

    private void method3(ArrayList<String> list) {
    
    
        System.out.println(1);
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
    
    
//    @Override
    public void method3(ArrayList<String> list) {
    
    
        System.out.println(2);
        new Thread(() -> {
    
    
            list.remove(0);
        }).start();
    }
}

image-20211012162312240

线程安全类

  • Integer
  • HashTable
  • String
  • Random
  • Vector
  • JUC下的类

它们单个方法是线程安全的,但是如果多个方法执行的时候就不一样了。下面的代码出现的问题就是线程1判断成功之后切换,刚好释放了锁,然后就是线程2获取锁进行判断,再次切换线程1获取锁处理put,切换线程2也可以获取锁处理put。因为单个方法执行完就会释放锁。所以这样还是需要整体上加锁才能够继续处理。

image-20211012170925749

不可变类线程安全

String

String和Integer都是不可变的,String本质上就是一个char[]数组。如果是substring方法实际上就是复制一个新的数组出来,然后再给String的char数组进行赋值。replace也实际上只是创建数组,然后对比原数组的旧值,如果是旧值那么直接给新的数组的那个位置赋值新值

 public String replace(char oldChar, char newChar) {
    
    
        if (oldChar != newChar) {
    
    
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
    
    
                if (val[i] == oldChar) {
    
    
                    break;
                }
            }
            if (i < len) {
    
    
                //创建新数组
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
    
    
                    buf[j] = val[j];
                }
                while (i < len) {
    
    
                    char c = val[i];
                    //根据原来的数组是旧值的位置改变成新值
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }
 public String substring(int beginIndex) {
    
    
        if (beginIndex < 0) {
    
    
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
    
    
            throw new StringIndexOutOfBoundsException(subLen);
        }
     //实际上就是创建了一个新的String,而不是修改了值
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }

  public String(char value[], int offset, int count) {
    
    
        if (offset < 0) {
    
    
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
    
    
            if (count < 0) {
    
    
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
    
    
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
    
    
            throw new StringIndexOutOfBoundsException(offset + count);
        }
      //实际上就是创建新数组并且进行复制
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

实例分析

这种线程不安全,原因就是这个MyServlet是共享的,而且UserService是在堆里面的。可以被多个线程调用它的方法修改count。可能会引发并发问题。

image-20211012180710474

这种横切也是有并发问题的,单例共享,可以被多个多线程调用,覆盖start这种变量。

image-20211012180827420

这种不会发生线程安全问题,原因是没有任何变量可以被修改。

image-20211012181239244

这里是会发生安全问题,原因是Connection暴露出去,可以被多个线程处理。如果线程1在处理getCon的时候切换到线程2刚好close那么线程1的Connection就没办法继续执行,因为已经被修改了。

image-20211012181402838

这个地方没有线程安全问题,原因是每次都是新创建的一个UserDao相当于是一个局部变量。并不是一个共享资源

image-20211012181512188

这个地方是因为通过子类方法把局部变量暴露出去,可能会被子类对象通过线程把这个局部变量进行修改

image-20211012181624999

总结:线程是否安全取决于是否有能够被修改的共享资源。

4.5习题

卖票问题

出现线程安全问题的是在卖票的时候可能多个线程在卖票,同时取出了共享变量count,最后的count数量就是最后那个线程处理的值。而不是共同处理的值,因为他们的执行序列发生了错误,导致count没有等待处理完就被另外一个线程先读取进来了。

解决办法就是给临界区加锁,实际上就是window的sell。那么为什么不给amountList和window加锁?原因是他们操作的并不是同一个共享资源处理不同自然就不需要加锁。但是之前的HashTable两个操作get和put都是针对同一个共享资源,导致最后的value会被最后的那个线程覆盖。因为判空成功的时候线程1还没有完成put。而且也没有锁住两个操作,而是做一个放一个

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        // 模拟多人买票
        TicketWindow window = new TicketWindow(1000);

        // 所有线程的集合
        List<Thread> threadList = new ArrayList<>();
        // 卖出的票数统计
        List<Integer> amountList = new Vector<>();
        for (int i = 0; i < 2000; i++) {
    
    
            Thread thread = new Thread(() -> {
    
    
                // 买票

                try {
    
    
                    Thread.sleep(random(10));
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                int amount = window.sell(random(5));
                // 统计买票数
                amountList.add(amount);
            });
            threadList.add(thread);
            thread.start();
        }

        for (Thread thread : threadList) {
    
    
            thread.join();
        }

        // 统计卖出的票数和剩余票数
        log.debug("余票:{}",window.getCount());
        log.debug("卖出的票数:{}", amountList.stream().mapToInt(i-> i).sum());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int random(int amount) {
    
    
        return random.nextInt(amount) + 1;
    }
}

// 售票窗口
class TicketWindow {
    
    
    private int count;

    public TicketWindow(int count) {
    
    
        this.count = count;
    }

    // 获取余票数量
    public int getCount() {
    
    
        return count;
    }

    // 售票 synchronized
    public  int sell(int amount) {
    
    
        if (this.count >= amount) {
    
    
            this.count -= amount;
            return amount;
        } else {
    
    
            return 0;
        }
    }
}

Vector自己的方法就已经带锁

public synchronized boolean add(E e) {
    
    
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

转账问题

这里其实会发生线程安全问题。主要就是在a转账的同时b也进去转账,那么b获取到的肯定就是没有转账的a,相对a也是。那么直接给方法加上synchronize行不行?如果绑定的是本类对象,很明显是不行,因为是两个账户,锁是不同的,进入了不同房间。那么解决方案就是可以使用唯一的Account.class的本类,那么就能够锁住了

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 1000; i++) {
    
    
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 1000; i++) {
    
    
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    public static int randomAmount() {
    
    
        return random.nextInt(100) + 1;
    }
}

// 账户
class Account {
    
    
    private int money;

    public Account(int money) {
    
    
        this.money = money;
    }

    public int getMoney() {
    
    
        return money;
    }

    public void setMoney(int money) {
    
    
        this.money = money;
    }

    // 转账
    public void transfer(Account target, int amount) {
    
    
        synchronized(Account.class) {
    
    
            if (this.money >= amount) {
    
    
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

4.6Monitor

JAVA对象头

包括了markword主要就是存储hashcode,age(gc生命值),biase_lock是不是偏向锁,01加锁的情况

还有就是klassword只要就是指向类对象(类的信息)。

如果是数组那么就还包括了数组的长度。

image-20211012190117132

Monitor锁

monitor是os提供的,成本很高

工作原理

实际上就是把obj的markword前面30bit记录monitor的地址,指向monitor。然后如果有线程要执行临时区的时候那么就把monitor的owner指向对应的线程。如果又有线程进来,那么会看看obj是否关联锁,然后再看看锁是否有owner,如果有那么就进入到EntryList阻塞等待。等待线程释放锁之后,唤醒entryList然后重新开始竞争。

image-20211012190808458

字节码角度

字节码的角度其实就是先把lock的引用复制放到slot1,然后就是monitorentry,把lock的markword指向monitor,并且把hashcode等存入monitor。接着执行业务代码,最后就是取出引用slot1,然后就是monitorexit,解锁。而且对业务代码也就是同步块进行了异常监视,如果出现异常,那么还是会进行解锁操作的。

image-20211012191838416

4.7synchronize优化

小故事

如果两个线程之间没有竞争那么就可以使用轻量级锁,相当于就是挂书包,如果发现是对方书包那么就在外面等待。后来另一个线程不用了,那么另一个线程就可以刻名字在门外,相当于就是偏向锁,如果这个时候有人来竞争,那么就会升级为书包也就是轻量级锁。后来另一线程回来了,发现那个线程把很多个门都刻上名字,就去找os把那些名字批量刻成自己的。也就是修改偏向锁。最后名字实在刻太多取消了偏向。

偏向锁是单个线程专属的,如果单个线程处理某个代码没有竞争,那么就可以使用偏向锁,如果有竞争那么就可以升级为轻量级锁。

1.轻量级锁

本质就是线程的调用临时区方法的栈帧的锁记录保存对象的引用和对象markword的信息。接着就是把对应锁记录的锁信息与obj进行交换,比如说把01改成了00告诉obj这是一个轻量级锁,而且告诉了obj锁记录的地址,相当于就是给obj贴上是谁的锁的标签。如果是可重入锁,那么锁记录markword部分就是null表示的是这是可重入的,用的是同一个锁。

image-20211012194226364

2.锁膨胀

  • 其实就是竞争轻量级锁的时候,没有地方给竞争的线程放着,那么这个时候就需要把轻量锁转换成重量级锁monitor,其实就是把obj的markword指向monitor。然后就是monitor的owner指向当前线程的锁记录。把阻塞线程放到等待队列里面。
  • 恢复的时候,CAS尝试把线程的锁记录给恢复过去,但是发现失败。这个时候恢复方式改成了重量级锁的恢复方式,唤醒list,然后owner设置为null,线程重新竞争monitor。如果没有就把monitor保存的hashcode信息恢复。

3.自旋优化

实际上相当于就是等红绿灯如果很快到绿灯那么就等一会,如果还有很久那么就拉手刹。自旋就是旋多一会等等别人释放重量级锁,如果成功一次,那么下次就会确定成功几率加大,自旋多几次。如果没有等到那么就阻塞。

自旋的原因:阻塞会导致线程的上下文切换需要消耗cpu时间和资源。速度相对比较慢。

image-20211012195850961

4.偏向锁

之所以要用偏向锁是因为轻量级锁的锁重入每次都调用CAS进行对比,CAS是一个OS指令操作,速度很慢。所以偏向锁是把ThreadId直接赋值给markword,那么下次能直接在java上对比这个markword。

  • 偏向锁带有延迟性,通常对象创建过一会才会生成
  • 先生成偏向锁->轻量级锁->重量级锁
  • 如果给临时区使用偏向锁,那么对应执行线程的id赋值给markword
  • 如果使用了锁的hashcode,那么偏向锁就会被禁止,因为hashcode占用的bit太多。
  • 轻量级在锁记录上记录hashcode,重量级在monitor上记录
  • 如果两个线程用同一个偏向级锁,那么锁会变成不可偏向,升级为轻量级锁。

image-20211012202241386

批量重偏向

其实就是多个没有竞争的线程,使用同一个锁,如果jvm发现撤销的偏向次数超过20次,那么就会自动偏向另外一个线程。比如t1线程使用一堆锁,锁偏向t1。但是如果t2使用这些锁,并且需要撤销锁的偏向超过20次,那么这些锁会全部偏向t2

批量撤销

如果撤销超过40次那么jvm就会撤销所有对象的偏向

5.锁消除

其实就是JIT发现锁的临时区里面根本就没有共享资源,那么就取消了这个锁。

image-20211012210126414

4.8wait和notify

小故事

小南需要烟才能工作,但它又要占这锁让别人无法进来。那么这个时候开一个waitSet相当于就是休息室让小南进去。并且释放锁。如果烟到了,那么notify小南就能够继续工作了。

Blocked和Waiting区别

  • 其实就是waiting是释放了锁,blocked是没有锁
  • waiting被notify之后仍然需要进入到entrylist进行等待。

image-20211012211045161

wait和notify的规则

线程调用对象wait和notify的时候一定是要使用这个锁成为monitor的主人的时候。这样才能够wait释放锁,和被其他人获取这个锁的人通知。

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    
    
    final static Object obj = new Object();

    public static void main(String[] args) {
    
    

        new Thread(() -> {
    
    
            synchronized (obj) {
    
    
                log.debug("执行....");
                try {
    
    
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
    
    
            synchronized (obj) {
    
    
                log.debug("执行....");
                try {
    
    
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
    
    
//            obj.notify(); // 唤醒obj上一个线程
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

wait()方法可以限制等待的时候,wait(参数)

4.9sleep和wait

区别

sleep:Thread调用,静态方法,而且不会释放锁

wait:所有obj,但是要配合synchronize使用,可以释放锁

拓展

通常锁会加上final防止被修改

正确使用

小南需要烟才能工作,如果是使用sleep不释放锁,那么其他需要等待干活的人就会干等着,等烟来。但是wait可以让小南释放锁,让其他线程工作,并且唤醒小南

存在问题

会不会有其他线程在等待着锁?如果是那么会不会唤醒错了线程?

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep2 {
    
    
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
    
    
        new Thread(() -> {
    
    
            synchronized (room) {
    
    
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
    
    
                    log.debug("没烟,先歇会!");
                    try {
    
    
                        room.wait(2000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
    
    
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
    
    
            new Thread(() -> {
    
    
                synchronized (room) {
    
    
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        sleep(1);
        new Thread(() -> {
    
    
            synchronized (room) {
    
    
                hasCigarette = true;
                log.debug("烟到了噢!");
                room.notify();
            }
        }, "送烟的").start();
    }

}

解决办法

可以通过while多次判断条件是否成立,直接使用notifyAll来唤醒所有的线程。然后线程被唤醒之后先再次判断条件是否成立,成立那么往下面执行,如果不成立那么继续执行wait。

image-20211013213307741

同步保护性暂停

定义

  • t1需要t2的结果,那么就可以通过一个中间对象guardedObject来充当这个中间商,t2执行完就发送消息到obj,然后obj交给t1
  • 如果是不断发送结果那么可以使用消息队列
  • 要等待所以是同步
  • join和future就是用的这个原理

image-20211013214103157

public class MyTest20 {
    
    
    public static void main(String[] args) {
    
    
        GuaObj guaObj = new GuaObj();
        Thread thread = new Thread(() -> {
    
    
            System.out.println("锁住,等待结果");
            guaObj.get(2000);
            System.out.println("解锁");
        }, "t1");
        thread.start();


        Thread thread1 = new Thread(() -> {
    
    
            System.out.println("先睡两秒");
            try {
    
    
                Thread.sleep(2000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("解锁,设置对象");
            guaObj.set(new Object());
        }, "t2");
        thread1.start();
    }
}

class GuaObj{
    
    
    public Object obj;

    public void get(long timeout){
    
    
        synchronized (this){
    
    
            long cur = System.currentTimeMillis();
            long paseTime=0;
            while(obj==null){
    
    
                try {
    
    
                    long waitTime=timeout-paseTime;
                    //超时就不等了
                    if(waitTime<=0) break;
                    this.wait(waitTime);
                    paseTime=System.currentTimeMillis()-cur;
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            System.out.println("等待结束");
        }
    }

    public void set(Object obj){
    
    
        synchronized (this){
    
    
            this.obj=obj;
            this.notifyAll();
        }
    }
}
  • 需要记录超时的时间,并且重新设置waittime,原因是可能会有虚假唤醒,那么这个时候超时时间不是timeout而是timeout-passedTime,也就是线程执行的时间。
  • 如果超时的话,那么就会自动结束

join的源码

 public final synchronized void join(long millis)
    throws InterruptedException {
    
    
      //一开始的时间
        long base = System.currentTimeMillis();
     //线程执行的时间
        long now = 0;

     //如果是<0那么就抛出异常
        if (millis < 0) {
    
    
            throw new IllegalArgumentException("timeout value is negative");
        }

     //如果是0那么就一直等待线程执行完,isAlive是否生存
        if (millis == 0) {
    
    
            while (isAlive()) {
    
    
                wait(0);
            }
        } else {
    
    
            //timeout超时那么就结束
            while (isAlive()) {
    
    
                long delay = millis - now;
                if (delay <= 0) {
    
    
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

Future

相当于就是一个信箱,里面装了很多GuardObject对象,线程可以通过对应的地址访问对象获取结果

image-20211013222036057

测试

在执行邮箱线程之前一定要先睡上几秒,不然会出现的问题就是,邮箱线程先执行导致最后都没有生成对象就开始想送信了,那么肯定是不行的。这里的Mail模拟信箱,放入,取出之后立刻删除。

  • post和people一一对应
  • people执行之后过一段实现才能到post执行不然就会发生没有生成对象的问题
@Slf4j(topic = "c.haoren")
public class MyTest20 {
    
    
    public static void main(String[] args) {
    
    
        for(int i=0;i<1;i++){
    
    
            new People1("t1").start();
        }

        Sleeper.sleep(1);

        Set<Integer> idSet = Mailed.getIdSet();
        for(Integer id:Mailed.getIdSet()){
    
    
            new PostMan1(id,"内容"+id,"t2").start();
        }
    }
    
}

@Slf4j(topic ="c.people")
class People1 extends Thread{
    
    
    public People1(String name) {
    
    
        super(name);
    }

    @Override
    public void run(){
    
    
        log.debug("等待收信");
        GuaObj guaObj = Mailed.generateObj();
        Object o = guaObj.get(2000);
        log.debug("对象是:{}",o);
    }
}

@Slf4j(topic = "c.post")
class PostMan1 extends Thread{
    
    
    private int id;
    private String mail;

    public PostMan1(int id,String mail,String name){
    
    
        super(name);
        this.id=id;
        this.mail=mail;

    }
    @Override
    public void run() {
    
    
        log.debug("送信");
        GuaObj obj = Mailed.getObj(id);
        obj.set(mail);
        log.debug("送信成功");

    }
}


class Mailed{
    
    
    public static Integer id=0;
    public static Map<Integer,GuaObj> map=new HashMap<>();

    public synchronized static int generateId(){
    
    
        return id++;
    }

    public static GuaObj generateObj(){
    
    
        int id = generateId();
        GuaObj guaObj = new GuaObj(id);
        map.put(guaObj.getId(),guaObj);
        return guaObj;

    }

    public static Set<Integer> getIdSet(){
    
    
        return map.keySet();
    }

    public static GuaObj getObj(int id){
    
    
        GuaObj remove = map.remove(id);
        return remove;

    }
}

@Slf4j(topic = "c.le")
class GuaObj{
    
    
    public Object obj;
    public int id;

    public int getId() {
    
    
        return id;
    }

    public void setId(int id) {
    
    
        this.id = id;
    }

    public GuaObj(int id) {
    
    
        this.id = id;
    }
    public GuaObj() {
    
    

    }

    public Object get(long timeout){
    
    
        synchronized (this){
    
    
            long cur = System.currentTimeMillis();
            long paseTime=0;
            while(obj==null){
    
    
                try {
    
    
                    long waitTime=timeout-paseTime;
                    //超时就不等了
                    if(waitTime<=0) break;
                    this.wait(waitTime);
                    paseTime=System.currentTimeMillis()-cur;
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            log.debug("等待结束");
            if(obj==null){
    
    
                log.debug("结果是null");
            }
        }
        return obj;
    }

    public void set(Object obj){
    
    
        synchronized (this){
    
    
            this.obj=obj;
            this.notifyAll();
        }
    }
}

异步生产者/消费者模型

定义

相当于就是生产者给队列生产结果,消费者负责处理结果

  • 不需要一一对应
  • 平衡资源
  • 消息队列有容量控制
  • 阻塞队列控制结果出队列

image-20211013230203424

消费队列,这里面put需要判断队列是否为满,如果是那么线程就需要被阻塞,并且为了防止虚假打断,这个时候需要while持续判断。get就是判空。接着就能直接执行业务通过队列来传输消息和结果.

@Slf4j(topic = "c.test")
public class MyTest21 {
    
    
    public static void main(String[] args) {
    
    
        MessageQueue1 messageQueue1 = new MessageQueue1(2);
        for(int i=0;i<3;i++){
    
    
            int id=i;
            new Thread(()->{
    
    
                Message1 message1=new Message1(id,"内容"+id);
                messageQueue1.put(message1);
            },"生产者"+i).start();
        }

        new Thread(()->{
    
    
            
            while(true){
    
    
                Sleeper.sleep(1);
                Message1 message1 = messageQueue1.get();
            }
        },"消费者").start();
    }
}

@Slf4j(topic = "c.queue")
class MessageQueue1{
    
    

    private LinkedList<Message1> list=new LinkedList();
    private int capcity;

    public MessageQueue1(int capcity) {
    
    
        this.capcity = capcity;
    }

    public Message1 get()  {
    
    
        synchronized (list){
    
    
            //1.是否为空
            while(list.isEmpty()){
    
    
                //线程等待并且让出锁
                try {
    
    
                    log.debug("队列已经是空的,进入阻塞队列");
                    list.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            Message1 message1 = list.removeLast();
            log.debug("消费:"+message1.getMessage());
            list.notifyAll();//唤醒那些由于队列满了,阻塞的线程
            return message1;
        }
    }

    public void put(Message1 message1){
    
    
        synchronized (list){
    
    
            //查满
            while(list.size()>=capcity){
    
    
                try {
    
    
                    log.debug("队列已经满了,进入阻塞队列");
                    list.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            list.addFirst(message1);
            log.debug("生产消息:"+message1.getMessage());
            list.notifyAll();
        }
    }
}


final class Message1{
    
    
    private int id;
    private String message;

    public int getId() {
    
    
        return id;
    }

    public void setId(int id) {
    
    
        this.id = id;
    }

    public String getMessage() {
    
    
        return message;
    }

    public void setMessage(String message) {
    
    
        this.message = message;
    }

    public Message1(int id, String message) {
    
    
        this.id = id;
        this.message = message;
    }

    @Override
    public String toString() {
    
    
        return "Message1{" +
                "id=" + id +
                ", message='" + message + '\'' +
                '}';
    }
}

4.10park和unpark

与wait和notify的区别

  • 不需要与monitor一起使用
  • 可以精准唤醒和阻塞线程
  • 可以先unpark,但是不能先notify。但是unpark之后park不起作用。

工作原理

①park,先去到counter里面判断是不是0,如果是那么就让线程进入队列。接着就是把counter设置为0

②unpark,那么唤醒线程,恢复运行,并且把counter设置为0

③先unpark后park,那么就unpark补充counter为1,那么park判断counter是1,认为还有体力可以继续执行。

image-20211013234827849

4.11重新理解线程状态

情况1:new->runnable

线程start

情况2:runnable->waiting

①notify和wait。wait进入阻塞,notify让他们重新竞争锁进入runnable,其它还是进入blocked。

情况3

②park和unpark

情况4

③t.join调用它的线程会进入等待,等待t完成任务或者是被interrupt

情况5-8

其实就是wait,join,sleep加上时间而已。都是从runnable->bolcked

情况9

synchronize获取锁失败那么就会进入blocked

情况10

所有代码执行完那么就是terminated。

image-20211013235726840

4.12多把锁

一个房间睡觉和学习。但是只有一把锁睡觉的时候不能学习并发度非常低。那么这个时候可以通过细化锁的粒度,分成两把一把是学习房间,一把是卧室的锁,那么就能够让两个功能并发执行。

问题

如果锁太多,一个线程需要多把锁会导致死锁的发生。

public class TestMultiLock {
    
    
    public static void main(String[] args) {
    
    
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
    
    
            bigRoom.study();
        },"小南").start();
        new Thread(() -> {
    
    
            bigRoom.sleep();
        },"小女").start();
    }
}

@Slf4j(topic = "c.BigRoom")
class BigRoom {
    
    

    private final Object studyRoom = new Object();

    private final Object bedRoom = new Object();

    public void sleep() {
    
    
        synchronized (this) {
    
    
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }

    public void study() {
    
    
        synchronized (this) {
    
    
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }

}

死锁的案例

t1有A但是想要B,t2有B但是想要A

@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
    
    
    public static void main(String[] args) {
    
    
        test1();
    }

    private static void test1() {
    
    
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
    
    
            synchronized (A) {
    
    
                log.debug("lock A");
                sleep(1);
                synchronized (B) {
    
    
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
    
    
            synchronized (B) {
    
    
                log.debug("lock B");
                sleep(0.5);
                synchronized (A) {
    
    
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

死锁的检查方式

①jps定位进程id,jstack id来查看程序信息

②jconsole直接查看进程消息。

活锁

其实就是两个线程都在改变对方的解锁条件导致没有释放锁。但没有阻塞。死锁就是含有对方的锁不放开导致线程阻塞。

解决方案

可以通过改变线程执行的时间,让他们交错执行,快速执行解锁条件。

image-20211014010221794

饥饿问题

其实就是线程一直获取不到锁导致没有执行。(?)

4.13ReentrantLock

相比synchronize

  • 可以被中断
  • 可以设置获取超时,超时之后就自动放弃获取锁
  • 公平锁,防止饥饿问题
  • 条件变量多

可重入

只要是同一个线程获取同一把锁,那么就能够被使用第二次。(在没有被解锁的时候可被使用第二次)

@Slf4j(topic = "c.test22")
public class MyTest22 {
    
    
    public static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) {
    
    
        lock.lock();
        try{
    
    
            log.debug("开始进入m1");
            m1();
        }finally {
    
    
            lock.unlock();
        }
    }

    public static void m1(){
    
    
        lock.lock();
        try {
    
    
            log.debug("m1进入");
            m2();
        }finally {
    
    
            lock.unlock();
        }
    }

    public static void m2(){
    
    
        lock.lock();
        try{
    
    
            log.debug("m2进入");
        }finally {
    
    
            lock.unlock();
        }
    }
}

可中断

lockInterrupt,这个方法才能够被其它线程中断等待锁。如果是lock那么就算中断也没有任何效果.这种可中断可以减少死锁的发生。

@Slf4j(topic = "c.test23")
public class MyTest23 {
    
    
    public static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            log.debug("上锁");
            lock.lock();
//            try {
    
    
//                lock.lockInterruptibly();
//            } catch (InterruptedException e) {
    
    
//                e.printStackTrace();
//                log.debug("无法获取锁");
//            }
            try {
    
    
                log.debug("获取到锁");
            } finally {
    
    
                lock.unlock();
            }
        }, "t1");
        t1.start();

        lock.lock();
        Sleeper.sleep(1);
        log.debug("帮助t1解锁");
        t1.interrupt();


    }
}

超时

这个地方可以使用tryLock来设定获取锁的超时时间,如果超时那么就自动放弃获取锁。而不是一直锁住

@Slf4j(topic = "c.test24")
public class MyTest24 {
    
    
    public static ReentrantLock lock=new ReentrantLock();

    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            log.debug("尝试获得锁");

            try {
    
    
                if(!lock.tryLock(2, TimeUnit.SECONDS)){
    
    
                    log.debug("获取锁失败");
                    return ;
                }
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
                log.debug("获取锁失败1");
                return;
            }
            try{
    
    
                log.debug("获取锁成功");
            }finally {
    
    
                lock.unlock();
            }
        }, "t1");

        log.debug("main获取锁");
        lock.lock();
        t1.start();
        Sleeper.sleep(1);
        log.debug("main解锁");
        lock.unlock();
    }
}

解决哲学家的问题

思路

可以使用ReentrantLock的tryLock,如果尝试失败那么就会往下面执行而不是一直等待。尝试获取锁,获取不到是不会阻塞线程的。

 @Override
    public void run() {
    
    
        while (true) {
    
    
            if(left.tryLock()){
    
    

                try{
    
    
                    if(right.tryLock()){
    
    
                        try{
    
    
                          eat();
                        }finally {
    
    
                            right.unlock();
                        }
                    }
                }finally {
    
    
                    left.unlock();
                }
            }
        }
    }

条件变量

定义

synchronize可以有一把锁,并且通过wait和notify来释放锁进入到waitSet。对于ReentrantLock就相当于有多个休息室waitSet,创建锁之后可以创建多个条件变量(多个房间),可以认为ReentrantLock里面有多个休息室,进入不同休息室可以通过不同的小锁处理。但实际上释放的锁还是ReentrantLock,然后交给别人使用。只不过条件变量可以控制不同的房间,让同房间的线程去竞争锁。

这里就是使用了条件变量,但他们进入的线程房间不同,操作的方式是相同的。唤醒的房间的线程不相同。

@Slf4j(topic = "c.Test24")
public class Test224 {
    
    
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    static ReentrantLock ROOM = new ReentrantLock();
    // 等待烟的休息室
    static Condition waitCigaretteSet = ROOM.newCondition();
    // 等外卖的休息室
    static Condition waitTakeoutSet = ROOM.newCondition();

    public static void main(String[] args) {
    
    


        new Thread(() -> {
    
    
            ROOM.lock();
            try {
    
    
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
    
    
                    log.debug("没烟,先歇会!");
                    try {
    
    
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
    
    
                ROOM.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
    
    
            ROOM.lock();
            try {
    
    
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
    
    
                    log.debug("没外卖,先歇会!");
                    try {
    
    
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
    
    
                ROOM.unlock();
            }
        }, "小女").start();

        sleep(1);
        new Thread(() -> {
    
    
            ROOM.lock();
            try {
    
    
                hasTakeout = true;
                waitTakeoutSet.signal();
            } finally {
    
    
                ROOM.unlock();
            }
        }, "送外卖的").start();

        sleep(1);

        new Thread(() -> {
    
    
            ROOM.lock();
            try {
    
    
                hasCigarette = true;
                waitCigaretteSet.signal();
            } finally {
    
    
                ROOM.unlock();
            }
        }, "送烟的").start();
    }

}

用锁来固定顺序执行

思路

其实就是两个线程都需要用到这个锁,但是t1线程一定要t2线程运行之后才能运行,那么判断条件就是一个boolean,如果t2运行那么就修改,并且唤醒线程1。线程1如果发现t2没有运行那么wait进入等待,如果被虚假唤醒可以通过while来循环进入重新等待。

@Slf4j(topic = "c.25")
public class MyTest25 {
    
    
    static Object lock=new Object();
    static boolean t2Run=false;
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            synchronized (lock){
    
    


                try {
    
    
                    while(!t2Run){
    
    
                        lock.wait();
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                log.debug("1");
            }
        }, "t1");


        Thread t2 = new Thread(() -> {
    
    
            synchronized (lock){
    
    
                log.debug("2");
                //唤醒线程1
                t2Run=true;
                lock.notify();
            }
        }, "t2");

        t1.start();
        t2.start();

    }
}

第二种方法

其实就是使用LockSupport的park方法处理。这种t1如果先执行那么就会park,进入阻塞,然后t2执行之后unpark唤醒t1。如果是t2先执行也没关系,这里线程的unpark之后会把counter变成1,t1如果park先检查counter发现是1那么就可以继续执行。

@Slf4j(topic = "c.26")
public class MyTest26 {
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            LockSupport.park();
            log.debug("1");
        }, "t1");

        Thread t2 = new Thread(() -> {
    
    
            log.debug("2");
            LockSupport.unpark(t1);
        }, "t2");

        t1.start();
        t2.start();


    }
}

轮流打印abc的思路(交替执行)

synchronized方式

其实还是对是否到这个线程的flag进行判断,如果是1那么就t1执行,如果是2那么就t2执行。执行之后还需要唤醒其它线程来查看是不是自己的条件,如果不是那么就继续进入等待条件,符合条件的就能获取锁继续执行。

public class MyTest27 {
    
    
    public static void main(String[] args) {
    
    
        WaitNotify1 waitNotify1 = new WaitNotify1(1, 5);
        Thread t1 = new Thread(() -> {
    
    
            waitNotify1.print("a",1,2);
        }, "t1");
        Thread t2 = new Thread(() -> {
    
    
            waitNotify1.print("b",2,3);
        }, "t2");
        Thread t3 = new Thread(() -> {
    
    
            waitNotify1.print("c",3,1);
        }, "t3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class  WaitNotify1{
    
    

    public void print(String s,int waitFlag,int nextFlag){
    
    

        synchronized (this){
    
    
            try {
    
    
                for(int i=0;i<loopNum;i++){
    
    
                    //如果不是1那么就等待
                    while(this.flag!=waitFlag){
    
    
                        this.wait();
                    }
                    System.out.print(s);
                    this.flag=nextFlag;
                    this.notifyAll();
                }

            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }

    }


    private int flag;
    private int loopNum;

    public WaitNotify1(int flag,int loopNum) {
    
    
        this.flag = flag;
        this.loopNum = loopNum;
    }
}

ReentrantLock处理顺序执行问题思路

这个地方执行的思路其实就是通过lock开多几个条件变量,条件变量控制的是各个休息室,使用条件变量来把线程阻塞,之后再释放。a,b,c分别开了3个,然后就是执行一个之后调用另一个condition来解锁另外一个继续执行。一开始需要把a,b,c三个线程都上锁,接着就是手动解锁一个让循环开始执行。

public class MyTest28 {
    
    
    public static void main(String[] args) {
    
    
        WaitLock waitLock = new WaitLock(5);
        Condition a = waitLock.newCondition();
        Condition b = waitLock.newCondition();
        Condition c = waitLock.newCondition();

        new Thread(()->{
    
    
             waitLock.print("a",a,b);
         },"t1").start();
        new Thread(()->{
    
    
            waitLock.print("b",b,c);
        },"t2").start();
        new Thread(()->{
    
    
            waitLock.print("c",c,a);
        },"t3").start();

        Sleeper.sleep(1);
        waitLock.lock();
        try{
    
    
            //唤醒a
            a.signal();
        }finally {
    
    
            waitLock.unlock();
        }
    }
}

@Slf4j(topic = "c.lock")
class WaitLock extends ReentrantLock{
    
    

    private int loopNum;

    public WaitLock(int loopNum) {
    
    
        this.loopNum = loopNum;
    }

    public void print(String str,Condition cur,Condition next){
    
    
        for(int i=0;i<loopNum;i++){
    
    
            lock();
            try{
    
    
                cur.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                unlock();
            }

        }
    }
}

LockSupport执行思路

其实这个地方直接就可以使用park先让三个线程阻塞,然后再手动开启一个线程。如果需要让阻塞的线程开启就需要unpart(t)就是需要传入对应的线程参数,唤醒对应的线程。t1执行完之后,唤醒t2,t2执行完之后唤醒t3,那么这个时候调用的方法就要传入对应的线程参数。而且线程参数是可以共享的,放到方法区,如果是放到main线程上是无法看到的。

@Slf4j(topic = "c.test29")
public class MyTest29 {
    
    
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) {
    
    
        WaitPark waitPark = new WaitPark(5);
        t1=new Thread(()->{
    
    
            waitPark.print("a",t2);
        },"t1");
        t2=new Thread(()->{
    
    
            waitPark.print("b",t3);
        },"t2");
        t3=new Thread(()->{
    
    
            waitPark.print("c",t1);
        },"t3");

        t1.start();
        t2.start();
        t3.start();
        Sleeper.sleep(1);
        LockSupport.unpark(t1);
    }
}
@Slf4j(topic = "c.lock")
class WaitPark{
    
    
    private int loopNum;

    public WaitPark(int loopNum) {
    
    
        this.loopNum = loopNum;
    }



    public void print(String str,Thread t){
    
    
        for(int i=0;i<loopNum;i++){
    
    
            LockSupport.park();
            log.debug(str);
            LockSupport.unpark(t);
        }
    }
}

第四章思维导图

image-20211014155437973

image-20211014160114622

image-20211014155515544

image-20211014160427507

image-20211014195905368

5.共享模型之内存

(上一章是monitor怎么保证共享区的原子性)

5.1JMM内存抽象模型

主要就是把cpu下面的缓存,内存,磁盘等抽象成主存和工作内存

体现在

  • 可见性
  • 原子性
  • 有序性

5.2可见性

出现的问题

t线程如果频繁读取一个静态变量,那么JIT编译器就会把它存入到线程的缓存,那么就算主线程修改了主存中的静态变量也没有任何作用,因为t线程读取的是缓存里面的。所以程序判断仍然是错误无法停止。

@Slf4j(topic = "c.Test32")
public class Test32 {
    
    
    // 易变
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t = new Thread(()->{
    
    
            while(true){
    
    
                    if(!run) {
    
    
                        break;
                    }
            }
        });
        t.start();

        sleep(1);
            run = false; // 线程t不会如预想的停下来
    }
}

image-20211014161432054

解决方案

volatile和synchronized可以让线程不能访问缓存,一定要访问主内存里面的run。

@Slf4j(topic = "c.Test32")
public class Test32 {
    
    
    // 易变
     static boolean run = true;
     static Object lock=new Object();

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t = new Thread(()->{
    
    
            while(true){
    
    
                synchronized (lock){
    
    
                    if(!run) {
    
    
                        break;
                    }
                }

            }
        });
        t.start();

        sleep(1);
        synchronized (lock){
    
    
            run = false; // 线程t不会如预想的停下来
        }

    }
}

加上sout也是可以解决可视化问题。原因是这个println是一个synchronize的方法,也就是要输出那么就会在同步块,同步块可以完成可视化,那么自然run就可以被读取。

public void println(boolean x) {
    
    
        synchronized (this) {
    
    
            print(x);
            newLine();
        }
    }

两阶段终止思路

原本那个终止是依靠isInterrupt和两段interrupt,包括中断sleep的interrupt和重新标记的interrupt。这种方式容易漏掉处理最后的重新标记的interrupt。那么现在有一种更好的处理方式就是使用volatile让变量可视化,那么两个线程可以通过一个变量和interrupt来进行沟通和交流。这个地方通过stop,

tpt线程开启之后等待stop的赋值为true,否则继续睡眠,如果main线程修改了stop之后可以立刻interrupt通知。如果不通过interrupt也可以但是会执行更长时间的睡眠。

@Slf4j(topic = "c.TwoPhaseTermination")
public class Test13 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();

        Sleeper.sleep(3);
        tpt.stop();


        /*Thread.sleep(3500);
        log.debug("停止监控");
        tpt.stop();*/
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
    
    
    // 监控线程
    private Thread monitorThread;
    // 停止标记
//    private volatile boolean stop = false;
    // 判断是否执行过 start 方法
    private volatile  boolean stop=false;
    private boolean starting = false;

    // 启动监控线程
    public void start() {
    
    
        synchronized (this) {
    
    
            if (starting) {
    
     // false
                return;
            }
            starting = true;
        }
        monitorThread = new Thread(() -> {
    
    
            while (true) {
    
    
                Thread current = Thread.currentThread();
                // 是否被打断
              if(stop){
    
    
                  log.debug("料理后事");
                  break;
              }
                try {
    
    
                    Thread.sleep(1000);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
    
    
                }
            }
        }, "monitor");
        monitorThread.start();
    }

    // 停止监控线程
    public void stop() {
    
    
        stop=true;
        monitorThread.interrupt();
    }
}

Balking模式

其实就是start方法可以开启多个线程是没有必要的,这个时候可以增加一个start来进行处理。如果有线程处理过,那么就直接return。但是这样还是会出现问题,如果多线程模式同时通过了判断,那么导致的问题就是能够产生多个线程处理start。原因是没有保证判断和创建是原子操作,这里可以使用synchronize来进行处理.

 // 启动监控线程
    public void start() {
    
    
        synchronized (this) {
    
    
            if (starting) {
    
     // false
                return;
            }
//            Sleeper.sleep(2);
            starting = true;
        }
        monitorThread = new Thread(() -> {
    
    
            while (true) {
    
    
                Thread current = Thread.currentThread();
                // 是否被打断
              if(stop){
    
    
                  log.debug("料理后事");
                  break;
              }
                try {
    
    
                    Thread.sleep(1000);
                    log.debug("执行监控记录");
                } catch (InterruptedException e) {
    
    
                }
            }
        }, "monitor");
        monitorThread.start();
    }

应用

这个地方采用了Balking模式来限制线程的重复生成。而且这里的start之所以是volatile是为了线程在stop的时候用的不是缓存中的而是使用主内存中的starting。

private volatile boolean stop;
    private volatile boolean starting;
    private Thread monitorThread;
public void start() {
    
    
        // 缩小同步范围,提升性能
        synchronized (this) {
    
    
            log.info("该监控线程已启动?({})", starting);
            if (starting) {
    
    
                return;
            }
            starting = true;
        }

        // 由于之前的 balking 模式,以下代码只可能被一个线程执行,因此无需互斥
        monitorThread = new Thread(() -> {
    
    
            while (!stop) {
    
    
                report();
                sleep(2);
            }
            // 这里的监控线程只可能启动一个,因此只需要用 volatile 保证 starting 的可见性
            log.info("监控线程已停止...");
            starting = false;
        });

        stop = false;
        log.info("监控线程已启动...");
        monitorThread.start();
    }

5.3指令重排优化

为什么要指令重排?

因为各个语句都是由多个指令组成,相当于是多个分工,这些分工有的可以同时完成,那么就把他们先组合到一起。其它需要前一步的结果的指令就在后面排序等待。

诡异的结果

这里其实就是指令重排会导致这个结果是0。实际上就是线程2指令重排先执行了ready=true,然后被切换到线程1,刚好通过if先做出了计算,最后才是切换到线程2执行num=2;

image-20211014180946423

解决

时候用volatile可以防止变量前面的代码重排序。

image-20211014181631609

volatile原理

volatile的原理其实就是内存屏障。写屏障就是把修改的变量之前的所有变量同步到主存中每次都是在主存中修改,而且保证前面的代码指令重 排到屏障之后。如果是读屏障那么就是带有volatile变量以下的所有变量都同步到主存中,防止屏障以下的代码重排到屏障之前,也就保护了volatile属性。

  • 保证了写屏障的变量是最新的。
  • 但是无法解决指令交错问题,也就是只能在本地线程保证指令有序,但是无法保证多线程的指令交错问题

image-20211014182242159

image-20211014182030286

双重检查

单例模式为了防止多次加锁,可以先判空之后,再加锁,再判空。这样的好处就是创建对象之后,只需要判空,而不需要再次加锁。只有在第一次需要加锁创建对象,防止多个线程同时创建对象。

问题

第一个if代码会被指令重排,为什么会重排?

image-20211014183534027

双重检查的问题根源分析(dcl)

关键就是if(INSTANCE==null)是一个在monitor之外的代码,那么产生的问题就是在执行INSTANCE=

new Singleton()的时候,他并不是一个原子操作,包括了invokespecial执行构造方法指令和putstatic给引用赋值(找到对象的堆内存地址)。

补充:那么这里synchronize为什么还是会出现指令重排?原因是它本来就会产生指令重排,只不过在synchronize中不会产生原子,可视化和有序性的问题,但这里是两个线程而且synchronize没有完全控制变量INSTANCE的原因。

image-20211014184233904

image-20211014184521801

解决方案

可以通过volatile的读写屏障防止代码指令重排到屏障之外,这样就能够避免invokespecial走到putstatic中。

image-20211014191639106

image-20211014191827602

happen-before(可见性)

  • synchronize
  • volatile
  • 等待线程执行完之后在读取变量
  • 静态变量写好之后,线程才调用
  • 线程打断之前的修改
  • 变量默认值

image-20211014192505730

image-20211014192718464

习题

balking习题

指令重排序问题,解决方案可以使用synchronize来把这些变量框住,防止其他线程切换的时候都通过了第一个if,导致的重复执行问题。

image-20211014192914381

单例

1.why加上final?

原因就是防止类被继承,之后重写的方法带上单例对象被改变

2.怎么防止反序列化破坏单例

需要增加一个返回Obj的方法,直接返回单例对象,而不是通过字节码重新创建。

3.构造私有?

防止被创建很多次

4.初始化能保证线程安全?

静态变量在类加载的时候完成了初始化。

5.不把Instance变成public的原因

防止被直接修改,提供封装性,隐藏细节。

image-20211014193208199

  1. 字节码里面全部都是public final static的类对象,所以可以限制实例对象
  2. 不会有并发问题,在类加载的时候静态变量已经加载完了
  3. 不会被反射破坏单例,enum的设计
  4. 也不会被反序列化破坏,它实现了序列化和返回单例的方法
  5. 它是一个饿汉式

image-20211014194043129

image-20211014194921770

静态内部类被调用的时候才会初始化对象。

image-20211014195413555

总结

  • 可见性(jvm优化速度,把变量放进线程的缓存)
  • 有序(指令重排,优化执行速度)
  • happen-before写入是否对线程可见
  • volatile原理
  • 两阶段终止volatile模式,让变量可见之后通过变量判断和干扰
  • 同步模式balking

image-20211014200138249

6.共享模型之无锁

6.1提出问题

关于对共享变量修改的多线程问题其实就是指令交错问题导致取值的时机相同,最后修改之后以最后一个修改的线程为标准赋值给最新的变量。

6.2CAS和volatile

CAS

定义其实就是一个操作系统的指令。它是一个原子方法。能够保证比较和赋值同时完成

CAS的锁机制

其实就是无锁,通过不断的旧值和新值的比较如果成功那么就赋值和交换。所谓的旧值其实就刚传入进来的时候的共享变量(赋值给局部变量定下来),然后在执行compareAndSet的时候对比局部变量和最新的共享变量。(其实就是这里共享变量可能会被其他线程先进行修改)如果对比不行那么就再次循环重试。

public class TestAccount {
    
    
    public static void main(String[] args) {
    
    
        Account account = new AccountCas(10000);
        Account.demo(account);
//        Account account=new AccountUnsafe(10000);
//        Account.demo(account);
    }
}

class AccountCas implements Account {
    
    
    private AtomicInteger balance;
//    private Integer balance;

    public AccountCas(int balance) {
    
    
//        this.balance=balance;
        this.balance = new AtomicInteger(balance);
    }

    @Override
    public Integer getBalance() {
    
    
        return balance.get();
//        synchronized (this){
    
    
//            return balance;
//        }

    }

    @Override
    public void withdraw(Integer amount) {
    
    
//        synchronized (this){
    
    
//            this.balance-=amount;
//        }
        while(true) {
    
    
            // 获取余额的最新值
            int prev = balance.get();
            // 要修改的余额
            int next = prev - amount;
            // 真正修改
            if(balance.compareAndSet(prev, next)) {
    
    
                break;
            }
        }
//        balance.getAndAdd(-1 * amount);
    }
}

class AccountUnsafe implements Account {
    
    

    private Integer balance;

    public AccountUnsafe(Integer balance) {
    
    
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
    
    
        synchronized (this) {
    
    
            return this.balance;
        }
    }

    @Override
    public void withdraw(Integer amount) {
    
    
        synchronized (this) {
    
    
            this.balance -= amount;
        }
    }
}

interface Account {
    
    
    // 获取余额
    Integer getBalance();

    // 取款
    void withdraw(Integer amount);

    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(Account account) {
    
    
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
    
    
            ts.add(new Thread(() -> {
    
    
                account.withdraw(10);
            }));
        }
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach(t -> {
    
    
            try {
    
    
                t.join();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()
                + " cost: " + (end-start)/1000_000 + " ms");
    }
}

CAS-volatile

CAS依赖volatile原因就是要通过volatile保证取到的值是最新的值。防止对比出现问题。

为什么无锁的效率更高

  • 无锁不需要线程上下文切换,但是synchronize需要上下文切换消耗资源
  • 无锁的情况需要额外cpu运行,cpu就像是跑道,如果没有跑道,线程这样的赛车是无法继续运行需要上下文切换,本质就是在循环等待对比的旧值和新值,一旦成功那么就立刻修改。

CAS的特点

  • 无锁并发,无阻塞
  • 无阻塞就是CAS效率比synchronize更高的原因。
  • 线程数少的时候使用,防止对比频率太多导致慢
  • 多核cpu,为了while循环继续运行

6.3原子整数

AtomicInteger

api

public class MyTest30 {
    
    
    public static void main(String[] args) {
    
    
        AtomicInteger i=new AtomicInteger(0);
        System.out.println(i.getAndAdd(1));//i++
        System.out.println(i.addAndGet(1));//++i
        System.out.println(i.get());

        System.out.println(i.getAndAdd(5));
        System.out.println(i.addAndGet(5));

    }
}

模仿updateAndGet

本质其实就是compareAndSet,也就是乐观锁来保证并发安全,然后加上接口来实现乘法。

public class MyTest30 {
    
    
    public static void main(String[] args) {
    
    
        //乘法和编程式函数
        AtomicInteger i=new AtomicInteger(12);
        System.out.println(i.updateAndGet(x -> x * 10));
        //模仿updateAndGet
        int i1 = updateAndGet(i, x -> x / 10);
        System.out.println(i1);
    }

    public static int updateAndGet(AtomicInteger x, IntUnaryOperator operator){
    
    
        int pre = x.get();//以前的值,用于对比新值
        int next=operator.applyAsInt(pre);//接口实现,乘法得到结果
        //把x设置为next

        while(true){
    
    
            if(x.compareAndSet(pre,next)){
    
    
                break;
            }
        }
        return x.get();
    }
}

updateAndGet源码

public final int updateAndGet(IntUnaryOperator updateFunction) {
    
    
        int prev, next;
        do {
    
    
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }

6.4原子引用

AtomicReference

用于保护其它类型的对象。比如decimal,或者其他类等。

class DecimalAccountCas implements DecimalAccount {
    
    
    private AtomicReference<BigDecimal> balance;

    public DecimalAccountCas(BigDecimal balance) {
    
    
//        this.balance = balance;
        this.balance = new AtomicReference<>(balance);
    }

    @Override
    public BigDecimal getBalance() {
    
    
        return balance.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
    
    
//
        while(true){
    
    
            BigDecimal pre=balance.get();
            BigDecimal next=pre.subtract(amount);
            if(balance.compareAndSet(pre,next)){
    
    
                break;
            }
        }
    }
}

ABA问题

其实线程2先启动,到对比变量的时候切换线程,如果线程1修改变量A->B之后又把它修改为B->A,轮到线程2修改的时候发现线程2是无法感知到这个变量被改变了。

@Slf4j(topic = "c.Test36")
public class Test36 {
    
    

    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
    
    
        log.debug("main start...");
        // 获取值 A
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);
        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();
        sleep(1);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }

    private static void other() {
    
    
        new Thread(() -> {
    
    
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t1").start();
        sleep(0.5);
        new Thread(() -> {
    
    
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t2").start();
    }
}

解决方案

AtomicStampedReference

可以通过AtomicStampedReference来进行处理,实际上就是加上了判断版本号,也就是每次修改不仅仅要对比旧值和新值,还需要对比修改的版本号。每次引用被修改版本号都会被改变.

@Slf4j(topic = "c.test36")
public class MyTest36 {
    
    
     static AtomicStampedReference<String> ref=new AtomicStampedReference<>("A",0);
     public static void main(String[] args) {
    
    
         String prev = ref.getReference();
         int stamp = ref.getStamp();
         log.debug("版本号{}",stamp);
         other();
         Sleeper.sleep(1);
         log.debug("A->B{}",ref.compareAndSet(prev,"B",stamp,stamp+1));
     }


    public static void other(){
    
    
         new Thread(()->{
    
    
             log.debug("stamp:{}",ref.getStamp());
             log.debug("A->B {}",ref.compareAndSet(ref.getReference(),"B",ref.getStamp(),ref.getStamp()+1));
         },"t1").start();

         Sleeper.sleep(0.5);
        new Thread(()->{
    
    
            log.debug("stamp:{}",ref.getStamp());
            log.debug("B->A {}",ref.compareAndSet(ref.getReference(),"A",ref.getStamp(),ref.getStamp()+1));
        },"t2").start();

    }
}
AtomicMarkableReference

这个相当于就是把版本号修改成了boolean,如果发生了修改那么boolean也会发生修改,因为你只需要知道到底有没有修改。这里的mark标记垃圾袋满了就是true,如果发生修改为空那么就是true。;两个线程,如果保洁阿姨已经把垃圾袋改为空,那么主线程就不需要把垃圾袋的状态进行修改。主要就是标记垃圾袋的状态。而且修改内容的时候也能够感知到。

Slf4j(topic = "c.Test38")
public class Test38 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        GarbageBag bag = new GarbageBag("装满了垃圾");
        // 参数2 mark 可以看作一个标记,表示垃圾袋满了
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);

        log.debug("start...");
        GarbageBag prev = ref.getReference();
        log.debug(prev.toString());

        new Thread(() -> {
    
    
            log.debug("start...");
            bag.setDesc("空垃圾袋");
            ref.compareAndSet(bag, bag, true, false);
            log.debug(bag.toString());
        },"保洁阿姨").start();

        sleep(1);
        log.debug("想换一只新垃圾袋?");
        boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
        log.debug("换了么?" + success);
        log.debug(ref.getReference().toString());
    }
}

class GarbageBag {
    
    
    String desc;

    public GarbageBag(String desc) {
    
    
        this.desc = desc;
    }

    public void setDesc(String desc) {
    
    
        this.desc = desc;
    }

    @Override
    public String toString() {
    
    
        return super.toString() + " " + desc;
    }
}

6.5原子数组

AtomicIntegerArray

保证线程的安全性。能够保证每次自增的时候都是只有一个线程在处理。相当于就是给数组的每个位置都加上cas操作,每次操作的时候都需要进行CAS。

public class Test39 {
    
    

    public static void main(String[] args) {
    
    
        demo(
                ()->new int[10],
                (array)->array.length,
                (array,index)->array[index]++,
                array-> System.out.println(Arrays.toString(array))
        );
        demo(
                ()->new AtomicIntegerArray(10),
                (array)->array.length(),
                (array,index)->array.getAndIncrement(index),
                array-> System.out.println(array)

        );
    }

    /**
     参数1,提供数组、可以是线程不安全数组或线程安全数组
     参数2,获取数组长度的方法
     参数3,自增方法,回传 array, index
     参数4,打印数组的方法
     */
    // supplier 提供者 无中生有  ()->结果
    // function 函数   一个参数一个结果   (参数)->结果  ,  BiFunction (参数1,参数2)->结果
    // consumer 消费者 一个参数没结果  (参数)->void,      BiConsumer (参数1,参数2)->
    private static <T> void demo(
            Supplier<T> arraySupplier,
            Function<T, Integer> lengthFun,
            BiConsumer<T, Integer> putConsumer,
            Consumer<T> printConsumer ) {
    
    
        List<Thread> ts = new ArrayList<>();
        T array = arraySupplier.get();
        int length = lengthFun.apply(array);
        for (int i = 0; i < length; i++) {
    
    
            // 每个线程对数组作 10000 次操作
            ts.add(new Thread(() -> {
    
    
                for (int j = 0; j < 10000; j++) {
    
    
                    putConsumer.accept(array, j%length);
                }
            }));
        }

        ts.forEach(t -> t.start()); // 启动所有线程
        ts.forEach(t -> {
    
    
            try {
    
    
                t.join();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        });     // 等所有线程结束
        printConsumer.accept(array);
    }
}

6.6原子更新器

AtomicReferenceUpdater

主要就是处理对象里面的变量的原子性,本质还是CAS进行的处理

@Slf4j(topic = "c.Test40")
public class Test40 {
    
    

    public static void main(String[] args) {
    
    
        Student student = new Student();
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        System.out.println(updater.compareAndSet(student, null, "张三"));
        System.out.println(student);

    }
}

class Student {
    
    
    volatile String name;



    @Override
    public String toString() {
    
    
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}

6.7原子累加器

LongAdder

原子累加器处理速度更快的原因就是使用了多个cell,相当于就是i要进行多线程的控制自增,然后分开两部分来相加CAS,然后最后汇总起来。

public class Test41 {
    
    
    public static void main(String[] args) {
    
    
        for (int i = 0; i < 5; i++) {
    
    
            demo(
                    () -> new AtomicLong(0),
                    (adder) -> adder.getAndIncrement()
            );
        }
        System.out.println();

        for (int i = 0; i < 5; i++) {
    
    
            demo(
                    () -> new LongAdder(),
                    adder -> adder.increment()
            );
        }
    }

    /*
    () -> 结果    提供累加器对象
    (参数) ->     执行累加操作
     */
    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
    
    
        T adder = adderSupplier.get();
        List<Thread> ts = new ArrayList<>();
        // 4 个线程,每人累加 50 万
        for (int i = 0; i < 4; i++) {
    
    
            ts.add(new Thread(() -> {
    
    
                for (int j = 0; j < 500000; j++) {
    
    
                    action.accept(adder);
                }
            }));
        }
        long start = System.nanoTime();
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
    
    
            try {
    
    
                t.join();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        });

        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start) / 1000_000);
    }
}

CAS实现锁的原理

实际上就是lock的时候改变state变量为1,那么其他线程进来的时候发现state不是0那么就进入到while。直到state被解锁为0,那么其他线程就能够再次进入。模仿加锁和解锁,只不过阻塞是改变成while处理。

public class Test41 {
    
    
    public static void main(String[] args) {
    
    
        for (int i = 0; i < 5; i++) {
    
    
            demo(
                    () -> new AtomicLong(0),
                    (adder) -> adder.getAndIncrement()
            );
        }
        System.out.println();

        for (int i = 0; i < 5; i++) {
    
    
            demo(
                    () -> new LongAdder(),
                    adder -> adder.increment()
            );
        }
    }

    /*
    () -> 结果    提供累加器对象
    (参数) ->     执行累加操作
     */
    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
    
    
        T adder = adderSupplier.get();
        List<Thread> ts = new ArrayList<>();
        // 4 个线程,每人累加 50 万
        for (int i = 0; i < 4; i++) {
    
    
            ts.add(new Thread(() -> {
    
    
                for (int j = 0; j < 500000; j++) {
    
    
                    action.accept(adder);
                }
            }));
        }
        long start = System.nanoTime();
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
    
    
            try {
    
    
                t.join();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        });

        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start) / 1000_000);
    }
}

image-20211015141435711

缓存伪共享

其实就是CPU的缓存都是以缓存行进行的存储,cpu1和cpu2读取了内存块1和2进入自己的缓存行,导致的问题就是一方的修改导致对方的缓存失效,那么就要去修改内存再通知其他缓存块。这里就会造成缓存失效的问题。解决办法就是通过Contended注解,把内存块分成两行相当于就是增加paddind空块,然后让Cell数据存放到内存块的下一行,让cpu读取的时候存入不同的缓存行,那么就不会出现在修改的时候还需要去修改另外一个cpu的缓存。

@sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

image-20211015142052846

image-20211015142241036

LongAdder源码

add部分

其实就是在base的CAS累加失败的时候(其实就是因为有线程在竞争),那么会创建cells通过longAccumulate,然后就是重新进行判断。如果cells不为空,那么就要看看当前线程的cell是否创建,如果没有创建那么就longAccumulate创建,如果创建那么就通过cell来完成累加的机制。

public void add(long x) {
    
    
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
    
    //判断是否创建cells和判断是否能够通过base无竞争直接完成累加
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||//判断cells是不是空的
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))//如果不是空的那么就执行cell的CAS操作
                longAccumulate(x, null, uncontended);//如果是空的那么创建sell
        }
    }

image-20211015143317341

longAccumulate

这个地方有三个判断,第一个判断是当这个cells不为空的时候。第二个判断是casBusy是0的时候,而且cells没有被修改,把casBusy改为1,相当于就是上锁。最后一个判断就是执行给base进行cas的累加操作,如果失败那么就返回循环。

这里主要讲第二个判断之后的逻辑,cells不存在,cell也不存在

  • 创建Cells数组,并且创建累加x的cell
  • 然后给cells赋值为rs也就是刚才创建的Cells数组
  • 并且给casBusy进行赋值为0相当于就是解锁,可以让其他线程进来。
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
    
    //如果未加锁、而且cells没有被其他线程创建或者是修改、那么就给casBusy赋值1加锁
                boolean init = false;
                try {
    
                               // Initialize table
                    if (cells == as) {
    
    
                        Cell[] rs = new Cell[2];//创建cells数组
                        rs[h & 1] = new Cell(x);//给x创建空间
                        cells = rs;//赋值
                        init = true;//结束循环
                    }
                } finally {
    
    
                    cellsBusy = 0;//解锁
                }
                if (init)
                    break;
            }

第一段的逻辑,主要是cells在,但是cell没有存在也就是没有创建了累加值的cell

  • 如果发现没有创建槽位cell,那么就创建并且赋值累加位x给它
  • 判断是否加锁,没有上锁,那么就自己加上锁并且进入修改。但是问题是这个地方可能会在进入在之前槽位被其它线程修改,因为第一个判断if ((a = as[(n - 1) & h]) == null) 的时候可能同时进来多个线程,那么在锁上之后仍然需要判断槽位是不是被占坑了。如果没有那么就创建,并且赋值create为true。否则就重新进入循环
if ((as = cells) != null && (n = as.length) > 0) {
    
    //如果cells不为空
                if ((a = as[(n - 1) & h]) == null) {
    
    //判断槽位是不是空
                    if (cellsBusy == 0) {
    
           // Try to attach new Cell
                        Cell r = new Cell(x);   // 创建新的累加值x的槽位
                        if (cellsBusy == 0 && casCellsBusy()) {
    
    //判断是否没有加锁,加锁后进入
                            boolean created = false;//判断是否创建成功
                            try {
    
                   // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
    
    //判断槽位是否被修改
                                    rs[j] = r;//赋值
                                    created = true;//创建成功
                                }
                            } finally {
    
    
                                cellsBusy = 0;//解锁
                            }
                            if (created)//重新进入循环
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                else if (n >= NCPU || cells != as)
                    collide = false;            // 为了跳过扩容操作
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 && casCellsBusy()) {
    
    
                    try {
    
    
                        if (cells == as) {
    
          //如果cells没有修改进行扩容操作
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
    
    
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = advanceProbe(h);
            }

第三个阶段的逻辑,还是上面的代码,主要是处理else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))失败的情况,这里实际上就是对cell做了一次cas的自增,但是由于线程竞争导致失败

  • 然后就是累加失败之后判断是不是超过CPU上限,NCPU,如果是那么就把collide设置为false,然后就可以跳过下面的else if (cellsBusy == 0 && casCellsBusy()) 扩容操作,接着就是 h = advanceProbe(h);这个就是为了换一个cell进行累加,因为不论是哪个cell都可以最后会汇总到一起。
  • 如果累加成功那么就直接结束了
  • 如果不是cpu的问题,那么就去扩容,扩容之后还失败那么就换一个cell进行累加。如果太多线程的情况下,可能多个线程围绕着cell来进行处理。

image-20211015151121349

 for (;;) {
    
    
            Cell[] as; Cell a; int n; long v;
            if ((as = cells) != null && (n = as.length) > 0) {
    
    //如果cells不为空
                if ((a = as[(n - 1) & h]) == null) {
    
    //判断槽位是不是空
                    if (cellsBusy == 0) {
    
           // Try to attach new Cell
                        Cell r = new Cell(x);   // 创建新的累加值x的槽位
                        if (cellsBusy == 0 && casCellsBusy()) {
    
    //判断是否没有加锁,加锁后进入
                            boolean created = false;//判断是否创建成功
                            try {
    
                   // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
    
    //判断槽位是否被修改
                                    rs[j] = r;//赋值
                                    created = true;//创建成功
                                }
                            } finally {
    
    
                                cellsBusy = 0;//解锁
                            }
                            if (created)//重新进入循环
                                break;
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 && casCellsBusy()) {
    
    
                    try {
    
    
                        if (cells == as) {
    
          // Expand table unless stale
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
    
    
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                h = advanceProbe(h);
            }
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
    
    //如果未加锁、而且cells没有被其他线程创建或者是修改、那么就给casBusy赋值1加锁
                boolean init = false;
                try {
    
                               // Initialize table
                    if (cells == as) {
    
    
                        Cell[] rs = new Cell[2];//创建cells数组
                        rs[h & 1] = new Cell(x);//给x创建空间
                        cells = rs;//赋值
                        init = true;//结束循环
                    }
                } finally {
    
    
                    cellsBusy = 0;//解锁
                }
                if (init)
                    break;
            }
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }

最后的操作就是累加的操作,把之前累加的x全部加到目标base上面

 public long sum() {
    
    
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
    
    
            for (int i = 0; i < as.length; ++i) {
    
    
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

6.8Unsafe

作用

主要是用来处理底层的os和多线程的操作。

使用案例

它只能通过反射来获取私有对象,并且需要对象的属性偏移值才能够线程安全地修改变量

public class MyTestUnsafe {
    
    
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    
    
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        System.out.println(unsafe);


        Teacher1 teacher1 = new Teacher1();
        long id = unsafe.objectFieldOffset(Teacher1.class.getDeclaredField("id"));
        long name = unsafe.objectFieldOffset(Teacher1.class.getDeclaredField("name"));
        unsafe.compareAndSwapObject(teacher1,id,null,1);
        unsafe.compareAndSwapObject(teacher1,name,null,"好人");
        System.out.println(teacher1);
    }
}

@Data
class Teacher1{
    
    
    Integer id;
    String name;

}

自己写一个通过unsafe处理的AtomicInteger,其实大部分操作就是unsafe+cas的while机制

public class MyUnsafeAccessor {
    
    
    public static void main(String[] args) {
    
    
        Account.demo(new MyAtomicInteger1(10000));
    }
}

class MyAtomicInteger1 implements Account{
    
    
    private volatile int value;
    private static final long valueOffset;
    private static final Unsafe unsafe;

    static {
    
    
        unsafe= UnsafeAccessor.getUnsafe();
        try {
    
    
            valueOffset=unsafe.objectFieldOffset(MyAtomicInteger1.class.getDeclaredField("value"));

        } catch (NoSuchFieldException e) {
    
    
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    public int getValue() {
    
    
        return value;
    }

    public MyAtomicInteger1(int value) {
    
    
        this.value = value;
    }

    public void decrease(int amount){
    
    
        while (true){
    
    
            int prev=this.value;
            int next=prev-amount;

            if( unsafe.compareAndSwapInt(this,valueOffset,prev,next)){
    
    
                //修改成功
                break;
            }
        }


    }


    @Override
    public Integer getBalance() {
    
    
        return getValue();
    }

    @Override
    public void withdraw(Integer amount) {
    
    

        decrease(amount);
    }
}

思维导图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g0FavymR-1634366844219)(…/…/…/…/AppData/Roaming/Typora/typora-user-images/image-20211015162414529.png)]

7.并发模型之不可变

7.1日期转换问题

SimpleDateFormat和DateTimeFormatter

SimpleDateFomat可变,但是DateTimeFormatter是不可变的而且线程安全

@Slf4j(topic = "c.Test1")
public class Test1 {
    
    
    public static void main(String[] args) {
    
    
        DateTimeFormatter stf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
//        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(() -> {
    
    
                TemporalAccessor parse = stf.parse("1951-04-21");
                log.debug("{}", parse);
//                try {
    
    
//                    synchronized (Test1.class){
    
    
//                        Date parse = sdf.parse("1951-04-21");
//
//                    }
//
//                } catch (ParseException e) {
    
    
//                    e.printStackTrace();
//                }

            }).start();
        }
    }

    private static void test() {
    
    
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(() -> {
    
    
                synchronized (sdf) {
    
    
                    try {
    
    
                        log.debug("{}", sdf.parse("1951-04-21"));
                    } catch (Exception e) {
    
    
                        log.error("{}", e);
                    }
                }
            }).start();
        }
    }
}

7.2String

不变性的原因

  • class是final防止方法继承之后的重写导致共享
  • char[]数组是final不可变只能够构造,或者是新创建
  • substring等方法都是通过重新构造Stringf防止char[]数组可变性导致被多个多线程指向引用
  • 保护性拷贝
public String substring(int beginIndex, int endIndex) {
    
    
        if (beginIndex < 0) {
    
    
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
    
    
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
    
    
            throw new StringIndexOutOfBoundsException(subLen);
        }
        //新创建的String
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }
public String(char value[], int offset, int count) {
    
    
        if (offset < 0) {
    
    
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
    
    
            if (count < 0) {
    
    
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
    
    
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
    
    
            throw new StringIndexOutOfBoundsException(offset + count);
        }
    //实际上就是copy,然后重新创建一个char数组。
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

7.3享元模式

作用

可以节省内存,如果对象频繁创建,可以先把它存入缓存中。以Long为例子,使用了cache,存入-128到127的Long.实际上valueOf就是这个享元模式的体现。

  • Byte,Short,Long都是-128-127
  • Character是0-127
  • 原子类不能保护多个方法的线程安全。但可以保护单个方法的,比如bigDecimal。如果要保证方法组合线程安全可以使用AtomicReference
 public static Long valueOf(long l) {
    
    
        final int offset = 128;
        if (l >= -128 && l <= 127) {
    
     // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }
private static class LongCache {
    
    
        private LongCache(){
    
    }

        static final Long cache[] = new Long[-(-128) + 127 + 1];

        static {
    
    
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Long(i - 128);
        }
    }

7.4自定义连接池

需要定义borrow方法和free。而且需要用到Connection数组和状态state数组。状态数组使用的AtomicIntegerArray,保证在修改数组的时候是一个原子操作。但是这里为了防止多线程问题还需要使用CAS的操作,获取旧值之后对比,如果符合才能赋值新值,while就是如果返回失败也就是状态修改失败,可以选择再来一次,但是在这里如果判断失败或者是遍历完发现没有空闲的那么就要进入阻塞。优化的地方可以是在统计state全部都是1的时候那么才阻塞,如果不是那么就再来一次修改(CAS防止并发修改问题).最后再free的时候是一个线程的操作,并且修改状态之后还需要唤醒线程重新争夺资源。

为什么有乐观锁还需要使用monitor?

原因就是乐观锁会导致在所有连接不是空闲的时候就会一直运转等待,耗费cpu资源,这个时候可以使用monitor来让它先阻塞,释放锁,让出cpu资源。等待到有人释放conn的时候在唤醒竞争。

  • con的增加和缩小
  • conn的活跃性
  • 超时等待就结束线程的等待。ReentrantLock
@Slf4j
public class MyPool {
    
    
    public static void main(String[] args) {
    
    
        Pool pool = new Pool(2);

        for(int i=0;i<5;i++){
    
    
            new Thread(()->{
    
    
                Connection conn = pool.borrow();
                try {
    
    
                    Thread.sleep(new Random().nextInt(1000));
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                pool.free(conn);
            },"t"+i).start();
        }

    }
}
@Slf4j(topic = "c.Pool")
class Pool{
    
    
    private final int poolsize;

    private Connection[] connections;

    private AtomicIntegerArray states;

    public Pool(int size){
    
    
        this.poolsize=size;
        connections=new MyConnection[size];
        int[] ints = new int[size];
        Arrays.fill(ints,0);
        states=new AtomicIntegerArray(ints);
        for(int i=0;i<poolsize;i++){
    
    
            connections[i]=new MyConnection("连接" + (i+1));
        }
    }

    public Connection borrow() {
    
    
        //为什么要while(true)?
        while(true){
    
    
            //尝试找到state并且修改,如果找不到那么就结束,如果被修改那么再次寻找
            for(int i=0;i<poolsize;i++){
    
    
                //如果能够修改成功那么才能够获取连接
                //原子操作
                if(states.get(i)==0){
    
    
                    if(states.compareAndSet(i,0,1)){
    
    
                        log.debug("获取到连接{}",i+1);
                        return connections[i];
                    }
                }

            }

            synchronized (this){
    
    
                try {
    
    
                    log.debug("wait。。。");
                    this.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }

    public void free(Connection conn){
    
    
        for(int i=0;i<poolsize;i++){
    
    
            if(connections[i]==conn){
    
    
                log.debug("释放连接{}",i+1);
                states.set(i,0);
                synchronized (this){
    
    
                    //唤醒那些因为找不到连接的线程
                    this.notifyAll();
                }
                break;
            }
        }


    }

}

7.5final的原理

设置的原理

final变量之前会有一个写屏障,也就是前面的赋值操作不可能走到后面保证了a的值是最新的值。如果不加上a,那么就有可能在多线程调用的时候,a初始值是0,但是还需要赋值一个20,但是由于线程切换,而且a已经算是赋值成功,那么就会直接被使用。如果加上final,那么就会直接给变量赋值20而不是先赋值0再赋值20。

优化原理

如果是final被线程方法调用,那么就会直接把final的值复制一份给这个线程的栈。如果是值太大,那么就会通过字节码指令ldc来存入串池中。如果没有final那么是直接从堆内存中拿,很容易出现线程的问题

第七章思维导图

image-20211016144448149

猜你喜欢

转载自blog.csdn.net/m0_46388866/article/details/120798620