JUC并发编程 深入学习Java并发编程【上】

news2024/11/15 17:34:26

 JUC并发编程,深入学习Java并发编程,与视频每一P对应,全系列6w+字。

P1-5 为什么学+特色+预备知识 进程线程概念

进程:

一个程序被运行,从磁盘加载这个程序的代码到内存,就开起了一个进程。

进程可以视为程序的一个实例,大部分程序可以同时运行多个实例进程(笔记本,记事本,图画,浏览器等),也有的程序只能启动一个实例进程(网易云音乐,360安全卫士等)。

线程:

一个进程内可以分为一到多个线程。

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。

Java中线程是最小调度单元,进程作为资源分配的最小单位。在windows中进程是不活动的,知识作为线程的容器。

对比:

进程拥有共享的资源,如内存空间等,供其内部线程共享。

进程间通信较为复杂:同一台计算机的进程通信称为IPC。不同计算机之间的进程通信,需要通过网络,遵守共同的协议。

线程通信简单,因为共享进程内的内存,多个线程可以访问同一个共享变量。

线程更轻量,上下文切换成本要比进程上下文切换低。

给项目引入如下pom依赖:

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.10</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
</dependencies>

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration
 xmlns="http://ch.qos.logback/xml/ns/logback"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 <encoder>
 <pattern>%date{HH:mm:ss} [%t] %logger - %m%n</pattern>
 </encoder>
 </appender>
 <logger name="c" level="debug" additivity="false">
 <appender-ref ref="STDOUT"/>
 </logger>
 <root level="ERROR">
 <appender-ref ref="STDOUT"/>
 </root>
</configuration>

P6 并发并行概念

操作系统任务调度器,可以把CPU时间交给不同线程使用,线程可以轮流使用CPU资源。

假如CPU为单核,同一时间段应对多件事情叫并发。同一时间段处理多件事情的能力。一个人做多件事。

假如CPU为多核,多个核心同时执行任务,叫作并行。同一时间同时做多件事情的能力。多个人做多件事。

P7 线程应用异步调用

同步:需要等待结果返回,才能继续运行。

异步:不需要等待结果返回,就能继续运行。

多线程可以让方法执行变为异步的,不会干巴巴等着,比如读取磁盘要花费5秒,如果没有线程调度机制,这5秒什么事情都做不了。

视频文件要转换格式操作比较费时,可以开一个新线程处理视频转换,避免阻塞主线程。

P8 线程应用提升效率

P9 P10 线程应用提升效率验证和小结

单核多线程比单核单线程的速度慢。

多核多线程比多核单线程快。

P11 创建线程方法1

源代码是在如下位置:

一开始默认有一个主线程在运行。

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

    public static void main(String[] args){
        Thread t = new Thread(){
            @Override
            public void run(){
                log.debug("running");
            }
        };
        t.setName("t1");
        t.start();
        log.debug("running");
    }
}

P12 创建线程方法2

使用Runnable配合Thread创建线程:

@Slf4j(topic="c.Test2")
public class test2 {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                log.debug("running");
            }
        };
        Thread t = new Thread(r,"t2");
        t.start();
    }
}

将任务和线程分离: 

P13 创建线程lambda简化

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

超级简化版:

@Slf4j(topic="c.Test2")
public class test2 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            log.debug("running");
        },"t2");
        t.start();
    }
}

P14 创建线程方法1,2-原理

P15 创建线程方法3

FutureTask配合Thread,FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况。

@Slf4j(topic="c.Test2")
public class Test3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.debug("running...");
                Thread.sleep(2000);
                return 100;
            }
        });
        Thread t1 = new Thread(task,"t1");
        t1.start();
        log.debug("{}",task.get());//阻塞住,等待线程,直到线程返回结果
    }
}

P16 线程运行现象

交替运行。

P17 线程运行windows查看和杀死

查看方式:1.通过任务管理器。2.在控制台输入tasklist

找到java进程:

tasklist | findstr java

 查看所有java进程:

jps

杀死某个进程:

taskkill /F /PID PID号

P18 线程运行linux查看和杀死

列出所有正在执行的进程信息:

ps -fe

 用grep关键字进行筛选:

ps -fe | grep 关键字

查看java进程页可以用Jps。

杀死某个进程:

kill PID号

查看进程内的线程信息:

top -H -p PID号

P19 线程运行jconsole

输入win+r,键入jconsole,可以打开图形化界面。

可以远程连接到服务器监控信息。

P20 线程运行原理栈帧debug

JVM由堆、栈、方法区组成。栈内存是给线程用的,每个线程启动后,虚拟机会为其分配一块栈内存。

栈由栈帧组成,对应每次方法调用时所占用的内存。

每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

P21 线程运行原理栈帧图解

 

返回地址对应的是方法区中的方法,局部变量对应的是堆中的对象。

P22 线程运行原理多线程

P23 线程运行原理上下文切换

CPU不再执行当前的线程,转而执行另一个线程的代码:

1.线程的CPU时间片用完。

2.垃圾回收。暂停当前所有的工作线程,让垃圾回收的线程去回收垃圾。

3.有更高优先级的线程需要运行。

4.线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法。

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

状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址。

P24 常见方法概述

start() 启动一个新线程,在新的线程运行run方法中的代码。start方法只能让线程进入就绪,代码不一定立即执行(只有等CPU的时间片分配给它才能运行)。每个线程对象的start方法只能调用一次。

join()等待线程运行结束。假如当前的主线程正在等待某个线程执行结束后返回的结果,就可以调用这个join方法。join(long n)表示最多等待n毫秒。

getId()获得线程id,getName()获得线程名称,setName()设置线程名称,getPriority()获得优先级,setPriority(int)设置线程优先级,getStatus()获取线程状态,isInterupted()判断是否被打断,isAlive()判断线程是否存活,interrupt()打断线程,interrupted()判断当前线程是否被打断。

currentThread()获取当前正在执行的线程,sleep(long n)让当前执行的线程休眠n毫秒,休眠时让出其cpu的时间片给其它线程。

yield()提示线程调度器让出当前线程对CPU的使用。

P25 常见方法start vs run

用run时是主线程来执行run方法。无法做到异步。

@Slf4j(topic="c.Test4")
public class Test4 {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        t1.run();
    }
}

下面是使用start方法启动,可以异步执行任务。

@Slf4j(topic="c.Test4")
public class Test4 {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        System.out.println(t1.getState());
        t1.start();
        System.out.println(t1.getState());
    }
}

在new之后start之前是NEW状态,在start之后是RUNNABLE状态。 

P26 常见方法sleep状态

sleep让线程从running状态变成time waiting状态,从运行状态变到有时限(因为会传递一个参数)的等待状态。

P27 常见方法sleep打断

正在睡眠的线程可以由其它线程用interrupt方法打断唤醒。此时睡眠的方法会抛出InterruptException。

程序思路,t1.start执行完,输出begin,然后休眠,执行t1的run方法输出enter slee...,然后休眠,1秒到后输出interrupt,最终t1.interrupt方法被调用,休眠线程立刻被打断,开始执行wake up....

@Slf4j(topic="c.Test7")
public class Test6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1") {
            public void run() {
                log.debug("enter sleep....");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    log.debug("wake up...");
                    throw new RuntimeException(e);
                }
            }
        };
        t1.start();
        log.debug("begin");
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
    }
}

P28 常见方法sleep可读性

建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。

@Slf4j(topic = "c.Test8")
public class Test7 {
    public static void main(String[] args) throws InterruptedException {
        log.debug("enter");
        TimeUnit.SECONDS.sleep(1);
        log.debug("end");
    }
}

 

P29 常见方法yield_vs_sleep

1.yield

某个线程调用yield,可以让出CPU的使用权。

调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其它线程。

2.sleep

调用sleep会让当前线程从Running进入Timed Waitring状态(阻塞)

P30 常见方法线程优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅只是一个提示,调度器可以忽略它。

如果cpu较忙,优先级高的线程会获得更多的时间片,但cpu如果闲时,优先级几乎没作用。

@Slf4j(topic="c.Test4")
public class test4 {
    public static void main(String[] args) {
        Runnable task1 =()->{
            int count=0;
            for(;;){
                System.out.println("------>1"+count++);
            }
        };
        Runnable task2 =()->{
            int count=0;
           for(;;){
               //Thread.yield();
                System.out.println("          ------>2"+count++);
            }
        };
        Thread t1 = new Thread(task1,"t1");
        Thread t2 = new Thread(task2,"t2");
        //t1.setPriority(Thread.MIN_PRIORITY);
        //t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

P31 常见方法sleep应用

在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或sleep来让出cpu的使用权给其它程序。

可以用wait或者条件变量达到类似的效果。但需要加锁,并且需要设置相应的唤醒操作,一般适用于要进行同步的场景。sleep适合无锁同步的场景。

P32 常见方法join

join等待某个线程执行结束。

下面这个例子因为t1线程睡了1秒,对r的更改不会发生,主线程会直接输出r的结果r=0。此时若想让r=10,则需要在t1.start()的下面加上t1.join()表示等待t1执行结束返回结果,主线程再执行。

@Slf4j(topic="c.Test5")
public class test5 {
    static int r=0;

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

    public 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("结果");
    }
}

P33 常见方法join同步应用

需要等待结果返回,才能继续运行是同步。

不需要等待结果返回,就能继续运行是异步。

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

P34 常见方法join限时同步

下面给t1.join()设置了1500毫秒等待时间,因为小于线程睡眠时间,所以没法能线程苏醒改变r,输出结果为r1=0。

@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 {
        test3();
    }
    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(1500);
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }
}

P35 常见方法interrupt打断阻塞

如果线程是在睡眠中被打断会以报错的形式出现,打断标记为false。

@Slf4j(topic="c.Test6")
public class test6 {
    public static void main(String[] args) throws InterruptedException{
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}",t1.isInterrupted());
    }
}

P36 常见方法interrupt打断正常

如果在main方法中调用t1的interrupt方法,t1线程只是会被告知有线程想打断,不会强制被退出。此时isinterrupted状态会被设为true,此时可以利用该状态来让线程决定是否退出。

@Slf4j(topic="c.Test7")
public class Test7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(true){
                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted){
                    log.debug("被打断了,退出循环");
                    break;
                }
            }
        },"t1");
        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
    }
}

P37 设计模式两阶段终止interrupt

在一个线程T1中如何优雅的终止线程T2,这里的优雅指的是给T2一个料理后事的机会。

错误思路:

1.使用线程对象的stop方法停止线程。stop方法会真正杀死线程,如果线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。

2.使用System.exit(int)方法会直接把方法停止,直接把进程停止。

P38 设计模式两阶段终止interrupt分析

在工作中被打断,打断标记是false,会进入到料理后事。

在睡眠是被打断,会抛出异常,此时打断标记是true,此时可以重新设置打断标记为false。

P39 设计模式两阶段终止interrupt实现

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination{
    private Thread monitor;
    public void start(){
        monitor = new Thread(()->{
            while(true) {
                Thread current = Thread.currentThread();
                if (current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("执行监控记录");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //重新设置打断标记
                    current.interrupt();
                }
            }
        });
        monitor.start();
    }
    public void stop(){
        monitor.interrupt();
    }
}

P40 设计模式两阶段终止interrupt细节

P41 常见方法interrupt打断park

@Slf4j(topic="c.Test9")
public class java9 {
    public static void main(String[] args) throws InterruptedException{
        test1();
    }
    public static void test1() throws InterruptedException{
        Thread t1 = new Thread(()->{
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}",Thread.interrupted());
            LockSupport.park();
            log.debug("unpark...");
        },"t1");
        t1.start();
        sleep(1);
        t1.interrupt();
    }
}

P42 常见方法过时方法

 切忌用stop,suspend方法。

P43 常见方法守护线程

默认情况下,Java进程需要等待所有的线程都运行结束,才会结束。

有一种特殊的线程叫守护线程,只要其它非守护线程执行结束了,即时守护线程的代码没有执行完,也会强制结束。

在t1启动前调用setDaemon方法开启守护线程,如果主线程运行结束,守护线程也会结束。

@Slf4j(topic="c.Test15")
public class Test10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
           while(true){
               if(Thread.currentThread().isInterrupted()){
                   break;
               }
           }
        });
        t1.setDaemon(true);
        t1.start();
        Thread.sleep(1000);
        log.debug("结束");
    }
}

垃圾回收器线程是一种守护线程。如果程序停止,垃圾回收线程也会被强制停止。

P44 线程状态五种

初始状态:在语言层面上创建线程对象,还没与操作系统中的线程关联,仅停留在对象层面。比如new了一个Thread对象,但没调用start方法。

可运行状态:就绪状态,线程已经被创建,与操作系统线程关联,可以由CPU调度器调度执行,可以获得CPU时间片,但暂时没获得时间片。

运行状态:指获取了CPU时间片,运行中的状态。

阻塞状态:调用了阻塞API,比如BIO读写文件,线程不会用到CPU,会导致上下文切换,进入阻塞状态。等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态。

终止状态:线程已经执行完毕,生命周期结束,不会再转换为其它状态。

P45 线程状态六种

从Java的层面进行描述:

NEW:指被创建,还没调用Start方法。

RUNNABLE:涵盖了操作系统层面的可运行、运行、阻塞状态。

TERMINATED:指被终止状态,不会再转化为其它状态。

3种阻塞的状态:

BLOCKED(想获得锁,但获得不了,拿不到锁会陷入block状态)

WAITING(这个是join等待时的状态)

TIMED_WAITING(这个是sleep时的状态,有时限的等待)

P46 线程状态六种演示

P47 习题应用之统筹分析

P48 习题应用之统筹实现

@Slf4j(topic = "c.Test16")
public class Test11 {
    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) {
                throw new RuntimeException(e);
            }
            System.out.println("泡茶");
        },"小王");
        t1.start();
        t2.start();
    }
}

缺点:上面模拟的是小王等老王的水烧开了,小王泡茶,如果反过来要实现老王等小王的茶叶拿过来,老王泡茶呢?代码最好能适应2种情况。

上面的两个线程各执行各的,如果要模拟老王把水壶交给小王泡茶,或模拟小王把茶叶交给老王泡茶呢?

P49 第三章小节

P50 本章内容

P51 小故事线程安全问题

多线程下访问共享资源,因为分时系统导致的数据不一致等安全问题。

P52 上下文切换分析

@Slf4j(topic="c.Test12")
public class Test12 {
    static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter++;
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter--;
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}", counter);
    }
}

结果并不唯一如下: 

 

i++和i--编译成字节码不是一条代码:

 

造成数据不一致的原因是:

某个线程的事情还没干完,数据还没来得及写入,上下文就切换了。根本原因:上下文切换导致指令交错。

P53 临界区与竞态条件

问题出现在多个线程访问共享资源。

在多个线程对共享资源读写操作时发生指令交错,出现问题。

一段代码内如果存在对共享资源的多线程读写操作,称这段代码为临界区。

竞态条件:多个

P54 上下文切换synchronized解决

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

1.阻塞式的解决方案:synchronized,Lock。

2.非阻塞式的解决方案:原子变量。

本次课使用的是synchronized来解决问题,即对象锁,它采用互斥的方式来让同一时刻至多只能有1个线程持有对象锁,其它线程想获取对象锁会被阻塞。这样保证拥有锁的线程可以安全的执行临界区内的代码,不用担心上下文切换。

synchronized(对象){
    临界区
}
@Slf4j(topic="c.Test12")
public class Test12 {
    static int counter = 0;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock){
                    counter++;
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock){
                    counter--;
                }
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}", counter);
    }
}

P55 上下文切换synchronized理解

假如t1通过synchronized拿到锁以后,但是时间片不幸用完了,但这个锁仍旧是t1的,只有时间片下次重新轮到t1时才能继续执行。

只有当t1执行完synchronized()块内的代码,会释放锁。其它线程才能竞争。

P56 上下文切换synchronized理解

当锁被占用时,就算指令没执行完上下文切换,其它线程也获取不到锁,只有当拥有锁的线程的所有代码执行完才能释放锁。

 P57 上下文切换synchronized思考

1.把加锁提到for循环外,相当于5000次for循环都视为一个原子操作。

2.如果线程1加锁,线程2没加锁会导致的情况:线程2去访问临界资源时,不会尝试获取对象锁,因此不会被阻塞住,仍然能继续访问。

P58 锁对象面向对象改进

@Slf4j(topic="c.Test12")
public class Test12 {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room){
                    room.increment();
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room){
                    room.decrement();
                }
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}", room.getCounter());
    }
}

class Room{
    private int counter = 0;
    public void increment(){
        synchronized (this){
            counter++;
        }
    }
    public void decrement(){
        synchronized (this){
            counter--;
        }
    }
    public int getCounter(){
        synchronized (this){
            return counter;
        }
    }
}

P59 synchronized 加在方法上

synchronized可以加在方法上,相当于锁住方法。

synchronized加在静态方法上,相当于所住类。

P60 P61 P62上下文切换synchronized习题1~8

线程1锁的是类,线程2锁的是方法。

下面一个锁类一个锁方法,锁的仍然是不同对象,所以会并行执行。

锁的是同一个Number对象,锁的是静态方法,所以锁的是类。

P63 线程安全分析

P64 线程安全分析局部变量

局部变量的i++只有一行字节码,不同于静态变量的i++。

P65 线程安全分析局部变量引用

创建2个线程,然后每个线程去调用method1:

如果method1还没把数据放入,method2就要取出数据,此时集合为空,会报错。

将list改为局部变量后,放到方法内:

list是局部变量,每个线程会创建不同实例,没有共享。

method2和method3的参数从method1中传递过啦,与method1中引用同一个对象。

P66 线程安全分析 局部变量暴露引用

因为下面ThreadSafeSubClass继承了ThreadSafe类,然后重写了method3方法,导致出现了问题。

必须要改为private和final防止子类去重写和修改,满足开闭原则,不让子类改变父类的行为。

P67  线程安全分析 局部变量组合调用

常见线程安全的类:

String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent包下的类

注意:每个方法是原子的,单多个方法的组合不是原子的。

下面Hashtable的get和put单个方法是线程安全的,但二者组合在一起仍然会受到线程上下文的切换的影响。

P68  线程安全分析 常见类 不可变

String、Integer等都是不可变类,即时被线程共享,因为其内部的状态不可以改变,因此它们的方法是线程安全的。

String的replace和substring等方法看似可以改变值,实则是创建了一个新的字符串对象,里面包含了截取后的结果。

P69 线程安全分析 实例分析1~3

线程不安全:Map<String,Object> map = new HashMap<>();

线程不安全:Date

下面这段非线程安全:

下面这段非线程安全:

Spring里某一个对象没有加Scope都是单例的,只有1份,成员变量需要被共享。

P70 线程安全分析 实例分析4~7

下面这个方法是线程安全,因为没有成员变量,也就是类下没有定义变量。变量在方法内部,各自都在线程的栈内存中,因此是线程安全的。

下面是线程安全的,因为UserDaoImpl里面没有可以更改的成员变量(无状态)。

下面是线程安全的,因为是通过new来创建对象,相当于每个线程拿到的是不一样的副本。

P71 习题 卖票 读题

证明方法:余票数和卖出去的票数相等,代表前后一致,没有线程安全问题。

@Slf4j(topic="c.ExerciseSell")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        //模拟多人买票
        TicketWindow window = new TicketWindow(100000);
        //所有线程的集合
        List<Thread> threadList = new ArrayList<>();
        //卖出的票数统计
        List<Integer> amountList = new Vector<>();
        for(int i=0;i<20000;i++){
            Thread thread = new Thread(()->{
                int amount = window.sell(randomAmount());//买票
                try {
                    Thread.sleep(randomAmount());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                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());
    }
    static Random random = new Random();
    public static int randomAmount(){
        return random.nextInt(5)+1;
    }
}
class TicketWindow{
    private int count;
    public TicketWindow(int count){
        this.count = count;
    }
    public int getCount(){
        return count;
    }
    public int sell(int amount){
        if(this.count >= amount){
            this.count -= amount;
            return amount;
        }else{
            return 0;
        }
    }
}

P72 习题 卖票 测试方法

老师用的是一个测试脚本进行测试。

P73 习题 卖票 解题

临界区:多个线程对共享变量有读写操作。

在sell方法中存在对共享变量的读写操作,因此只需要在方法上加synchronized:

P74 习题 转账

这道题的难点在于有2个共享变量,一个是a的账户中的money,一个是b的账户中的money。

@Slf4j(topic="c.ExerciseTransfer")
public class ExerciseTransfer1{
    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();
        log.debug("total:{}",(a.getMoney()+b.getMoney()));
    }
    static Random random = new Random();
    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);//对方余额加上转账金额
            }
        }
    }

}

偷懒的方法加入下面:synchronized(Account.class),相当于锁住两个账户的临界资源,缺点是n个账户只能有2个账户进行交互。

P75 Monitor 对象头

Klass word是一个指针,指向某个对象从属的Class,找到类对象,每个对象通过Klass来辨明自己的类型。

在32位虚拟机中:Integer:8+4,int:4。

P76 Monitor 工作原理

Monitor是锁,Monitor被翻译为监视器或管程。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。

obj是java的对象,Monitor是操作系统提供的监视器,调用synchronized是将obj和Monitor进行关联,相当于在MarkWord里面记录Monitor里面的指针地址。

Monitor里面的Owner记录的是当前哪个线程享有这个资源,EntryList是一个线程队列,来一个线程就进入到阻塞队列。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1487003.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人工智能_大模型015_RAG量化检索增强002_AIGC大模型_本地知识库实时问答_私域和实时场景_量化检索增强---人工智能工作笔记0151

由于上一节我们提到的,关键词检索的局限性,现在我们引出向量检索, 关键词检索有语义上的缺陷,因为我们说法不一样,但是意思一样的话,那么,关键词如果在es库中没有,那么会导致,找不到答案的情况.所以我们引出向量检索,要求语义一样的词,去检索都能找到答案. 我们来说一下这个文…

突破编程_前端_JS编程实例(网站标题栏TAB组件)

1 开发目标 实现如下网站标题栏 TAB 组件&#xff1a; 在点击"页面2"选项卡后&#xff0c;TAB 组件会切换对应的面板&#xff1a; 2 详细需求 网站标题栏 TAB 组件该组件需根据客户端提供的参数创建&#xff0c;具备动态构建 TAB 区域、选项卡切换及自定义内容…

Django模型进阶(Mysql配置、模型管理,表关联、一对一、一对多,多对多)

模型进阶&#xff1a; Mysql配置&#xff1a; 1.安装mysql 2安装MySQL驱动&#xff0c;使⽤mysqlclient pip install mysqlclient pip install -i https://pypi.douban.com/simple mysqlclientLinux Ubuntu下需要先安装&#xff1a;apt install libmysqld-dev 再安装: apt…

[BJDCTF2020]EzPHP1 --不会编程的崽

有一说一&#xff0c;这题还是有难度的 base32解码url编码绕过$_SERVER换行符绕过preg_match相同参数&#xff0c;post请求覆盖get请求&#xff0c;绕过$_REQUESTphp伪协议利用sha1数组绕过create_function代码注入 Level 1 右键源码里又发现&#xff0c;拿去base32解码即可…

w30使用python调用shell脚本

使用python脚本去实现永恒之蓝漏洞攻击 实验环境 攻击工具&#xff1a;pythonmsfconsole 靶场&#xff1a;win7 和 kali实验目的 演示python脚本调用过程 实验步骤 1.写一个永恒之蓝的攻击脚本&#xff0c;定义为blue.rc use exploit/windows/smb/ms17_010_eternalblue …

Microsoft PyRIT能自动化完成AI红队的任务

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

【算法集训】基础算法:枚举

一、基本理解 枚举的概念就是把满足题目条件的所有情况都列举出来&#xff0c;然后一一判定&#xff0c;找到最优解的过程。 枚举虽然看起来麻烦&#xff0c;但是有时效率上比排序高&#xff0c;也是一个不错的方法、 二、最值问题 1、两个数的最值问题 两个数的最小值&…

【Android】View事件体系基础

文章目录 坐标系View滑动layout方法offserLeftAndRight() 和 offsetTopAndBottom()LayoutParams(布局参数)View动画scrollTo/scrollBy 解析Activity的构成 坐标系 分为Android坐标系和View坐标系 可以用 getWidth() 和 getHeight() 获取View自身的宽度和高度 对于ViewgetX() …

求Sn=a+aa+aaa+aaaa+aaaaa的前n项之和

求Snaaaaaaaaaaaaaaa的前5项之和&#xff0c;其中a是一个数字&#xff0c; 例如&#xff1a;222222222222222 int main() {int a;scanf("%d", &a);int n;scanf("%d", &n);int sum 0;int tmp 0;for (int i 0; i < n; i){tmp tmp * 10 a;sum…

bxCAN总线的工作模式和测试模式(STM32F4xx)

概述 本文主要介绍STM32F4XX的bxCAN知识&#xff0c;包括bxCAN的概念&#xff0c;各种工作模式下特性&#xff0c;如何配置各类工作模式等内容&#xff0c;还介绍了bxCAN的测试模式&#xff0c;bxCAN测试模式有3种工作类型&#xff0c;每种类型有什么特性&#xff0c;以及如何配…

C语言之指针习题一

1. 解析&#xff1a;全选 2. 解析&#xff1a;A.当内存空间释放后&#xff0c;指针将指向其他的区域&#xff0c;成为野指针 3. 解析&#xff1a;B&#xff0c;assert只会在调试模式&#xff08;debug&#xff09;下使用&#xff0c;release不会使用 4. 解析&#xff1a; A…

IDEA集成Tomcat服务器指南

目录 一.概述 二.下载安装 三.启动Tomcat服务器 四.修改端口号 五.Maven Web项目 六.创建项目 1.使用骨架 2.不使用骨架 七.Idea中集成Tomcat 1.集成本地Tomcat 2.使用Maven插件 一.概述 所谓的Web服务器软件&#xff0c;功能是对HTTP协议的操作进行封装&#xff0…

ros2的nav2_map_server导入地图时,rviz2显示no map received

ros2的nav2_map_server导入地图时&#xff0c;rviz2显示no map received 步骤&#xff1a; 1、打开终端&#xff0c;进入src/map/运行下面指令&#xff1a; ros2 run nav2_map_server map_server --ros-args --param yaml_filename:fishbot_map.yaml2、打开rviz2 rviz2通过a…

leetcode 热题 100_无重复字符的最长子串

题解一&#xff1a; 滑动窗口哈希表&#xff1a;假设字符串为"abcabcbb"&#xff0c;遍历循环所有子串&#xff0c;以不同位置作为子串的起始&#xff0c;则会得到以下最长子串。其中最长子串的右端点会随着左端点的右移而右移&#xff0c;也可能不变&#xff0c;原因…

跨平台指南:在 Windows 和 Linux 上安装 OpenSSL 的完整流程

Windows安装 一&#xff1a;找到安装包&#xff0c;双击即可 https://gitee.com/wake-up-again/installation-package.git 二&#xff1a;按照提示&#xff0c;一步一步来&#xff0c;就可以啦 三&#xff1a;此界面意思是&#xff0c;是否想向创作者捐款&#xff0c;自己视情…

访问修饰符、Object(方法,使用、equals)、查看equals底层、final--学习JavaEE的day15

day15 一、访问修饰符 含义&#xff1a; 修饰类、方法、属性&#xff0c;定义使用的范围 理解&#xff1a;给类、方法、属性定义访问权限的关键字 注意&#xff1a; ​ 1.修饰类只能使用public和默认的访问权限 ​ 2.修饰方法和属性可以使用所有的访问权限 访问修饰符本类本包…

消息中间件之RocketMQ源码分析(二十八)

延迟消息存储机制 概述 什么是延迟消息呢?延迟消息也叫定时消息&#xff0c;一般地&#xff0c;生产者在发送消息后&#xff0c;消费者希望在指定的一段时间后再消费。常规做法是&#xff0c;把信息存储在数据库中&#xff0c;使用定时任务扫描&#xff0c;符合条件的数据再…

js字符串转json的3种方法

1.eval方式解析 function strToJson(str){var json eval("(" str ")");return json;}console.log(strToJson("{int:1, string:demo}")); 运行截图&#xff1a; 注&#xff1a; 记得别忘了str两旁的小括号。 永远不要使用 eval !!! eval() 是一…

最短路径(2.19)

目录 1.网络延迟时间 弗洛伊德算法 迪杰斯特拉算法 2. K 站中转内最便宜的航班 3.从第一个节点出发到最后一个节点的受限路径数 4.到达目的地的方案数 1.网络延迟时间 有 n 个网络节点&#xff0c;标记为 1 到 n。 给你一个列表 times&#xff0c;表示信号经过 有向 边的…

3、JavaWeb-Ajax/Axios-前端工程化-Element

P34 Ajax介绍 Ajax:Asynchroous JavaScript And XML&#xff0c;异步的JS和XML JS网页动作&#xff0c;XML一种标记语言&#xff0c;存储数据&#xff0c; 作用&#xff1a; 数据交换&#xff1a;通过Ajax给服务器发送请求&#xff0c; 并获取服务器响应的数据 异步交互&am…