java并发编程

一、java线程

1.三种创建线程的方式

Integer sum = futureTask.get(); 会等待其对应的线程执行完 ,即阻塞 再获得结果。

所以我在测试时,出现一个小插曲

java并发编程_第1张图片


@Slf4j
public class ThreeWays {
    //1.自定义MyThread进行继承Thread
    static void test001(){
        Thread thread = new MyThread();
        thread.setName("t1");
        thread.start();
    }
    //2.实现Runnable接口
    static void test002(){
        Thread thread=new Thread(()-> {
            for (int i = 0; i <1000 ; i++) {
                log.info("{}",i);
            }
        });
        thread.setName("t2");
        thread.start();
    }
    //3.实现
    static Integer test003() throws ExecutionException, InterruptedException {

        FutureTask futureTask=new FutureTask<>(new Callable() {
            @Override
            public Integer call() throws Exception {
                int num=0;
                for (int i = 0; i < 1000000; i++) {
                    num+=i;
                    log.info("{}",i);
                }
                return num;
            }
        });
        Thread thread=new Thread(futureTask);
        thread.start();
        Integer sum = futureTask.get();
        log.info("sum为:{}",sum);
        return sum;
    }
    public static void main(String[] args) throws InterruptedException, ExecutionException {
//        test001();
//        test002();
          test003();
        log.info("123");
    }
}

2. Thread跟Runable的关系

当使用Runable作为参数时 ,即 new Thread( runnable )

把runnable 赋给 target

再运行时,去判断,究竟是否调用 targert.run()

java并发编程_第2张图片

小结


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

3.linux查看线程信息

①ps -ef  查看所有进程的信息,一般会跟grep 连用。

e代表 every  ,  f代表格式化输出

ps -fT -p 查看某个进程(PID)的所有线程

②kill 杀死进程

kill -9 是强制杀死进程的意思。

top 按大写 H 切换是否显示线程
top -H -p 查看某个进程(PID)的所有线程


Java
jps 命令查看所有 Java 进程
jstack 查看某个 Java 进程(PID)的所有线程状态
jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

4.引入docker来验证

在目录下新建一个  Dockerfile

FROM openjdk:8-jdk-slim-buster
COPY . /usr/src/myapp
WORKDIR /usr/src/myapp
RUN javac Hello.java
CMD ["java","Hello"]

新建一个 Hello.java, 我们在主线程开启两个线程,分别为t1,t2

import java.util.Random;
public class Hello {
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while(true){
                try {
                    Thread.sleep(new Random().nextInt(100)+900);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
        }); 
        t1.setName("t1"); 
        t1.start(); 
        Thread t2=new Thread(()->{ 
            while (true){ 
                try { 
                    Thread.sleep(new Random().nextInt(100)+900); 
                } catch (InterruptedException e) { 
                    throw new RuntimeException(e); 
                } 
                System.out.println(2); 
            } 
        }); 
        t2.setName("t2"); 
        t2.start(); 
        System.out.println("main"); 
 
    } 
 
} 

执行        docker build -t   自定义镜像的名字  .   

                docker  run  自定义镜像的名字

注意第一个命令有  句点

现在我们去用命令看一下

5.线程运行原理

执行类加载,把字节码加载到java虚拟机中,放到方法区

java并发编程_第3张图片

线程上下文切换(待补充)

其实就是栈帧,每当调用一个方法,就加到栈上面。每个线程都有一个程序计数器和对应的栈。

这句话基本上是正确的,但需要进一步解释和澄清:

- 栈帧:每当线程调用一个方法时,虚拟机为该方法创建一个新的栈帧,并将其压入当前线程的Java虚拟机栈顶。栈帧中包含了局部变量表、操作数栈、动态链接信息(指向常量池的方法引用)、方法返回地址和其他额外的数据区域,如用于异常处理的信息等。

- 程序计数器:每个线程确实都有自己的程序计数器(PC寄存器),它记录了当前线程执行指令的位置,即下一条要执行的字节码指令地址。当线程在不同的方法之间切换时,程序计数器会保存并恢复相应方法的入口地址或返回地址。

- 线程与栈的关系:每个线程拥有独立的Java虚拟机栈,这个栈就是用来存储该线程所调用的所有方法的栈帧。因此,每个线程的栈都是私有的,这样可以保证不同线程间的本地变量相互独立,不会互相干扰。

总结一下,对于每个线程来说,其执行过程中的每一步都可以对应到某个栈帧,而程序计数器则指示着当前线程正在执行哪一段代码。当方法调用发生时,新的栈帧会被添加到相应的线程栈上,而当方法结束时,对应的栈帧将被弹出。

6.常见方法

java并发编程_第4张图片

java并发编程_第5张图片

java并发编程_第6张图片

7. start和run的区别

run()和调用一个对象的方法没区别,就是去调用,不会开启一个线程。

而 start会去开启一个线程去调用run()

8.sleep状态

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

java并发编程_第7张图片

②其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
③睡眠结束后的线程未必会立刻得到执行
④建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读

9.yield

让出当前线程的cpu使用权。但这不意味着不会再执行该线程,因为 此时 线程处于就绪状态,意味着调度器可以去随机选择线程。

1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
2. 具体的实现依赖于操作系统的任务调度器

yield跟sleep对比: ①所更改的线程状态不同。②yiled不需要指定等待时间。

10. 线程优先级


线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。

线程名.setPriority()

下面是线程优先级的范围。 最小,默认,最大

java并发编程_第8张图片

11.join

join就是等待指定线程结束。

join()

join( final long millis )   指定等待线程结束,如果等待时间超过mills ,那么不再等待。

看下面这个案例很有意思!

t1线程等待2秒,此时t2线程等待1秒,t1线程执行完后,t2线程肯定是执行完的,t2.join()会立即执行完,无需等待。

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.start();
log.debug("结果为:{}", r);
log.debug("结束");
}

java并发编程_第9张图片

12. interrupt

对于正常执行的线程,使用 interrupt() 打断后, isInterrupted() 的结果为true, 即被打断。

对于sleep,wait,join 的线程 ,使用 interrupt() 打断后,会清空标记状态,抛出中断异常, isInterrupted() 的结果为false,即标记状态仍未变,为false.

比较有趣的一点:

对于正常运行的线程(没有sleep),当被打断后,并不会像我们想的那样直接被打断,而是 正常执行,只不过是它的标记状态isInterrupted() 变为false

13. 两阶段终止-设计模式

场景:一个监控线程,不断检测主机状态,但我们可以随时去停掉这个监控线程,怎样优雅的停。

java并发编程_第10张图片

java并发编程_第11张图片

如下方代码,实现监控:

@Slf4j
public class Test {
    private static Thread monitor;
    public static void main(String[] args) {
        monitor=new Thread(()->{
            Thread curr = Thread.currentThread();
            while (true){
                if(curr.isInterrupted()){
                    log.info("料理后事");
                    break;
                }
                try {
                    Thread.sleep(100);
                    log.info("执行监控");
                } catch (InterruptedException e) {
                    //正在睡眠时time_waited 被打断,此时会抛出异常,清楚中断标记
                    //因此在这里要重新打断一下
                    curr.interrupt();
                }
            }

        });
        //中断监控,此时如果未睡眠,结果为true
        monitor.interrupt();
    }
}

当打断标记为真,park()就会失效

14.守护进程(舔狗进程)

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

下面这段代码,如果不设置守护进程,就会不断运行。

@Slf4j
public class Tea {
  
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while (true){
                log.info("我守护你!");
            }
        });
        t1.setDaemon(true);
        t1.start();

    }
}

15.五种状态与六种状态

从操作系统层面来讲:

java并发编程_第12张图片

从java虚拟机来讲

先看Thread.state 枚举类

  public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * 
    *
  • {@link Object#wait() Object.wait} with no timeout
  • *
  • {@link #join() Thread.join} with no timeout
  • *
  • {@link LockSupport#park() LockSupport.park}
  • *
* *

A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called {@code Object.wait()} * on an object is waiting for another thread to call * {@code Object.notify()} or {@code Object.notifyAll()} on * that object. A thread that has called {@code Thread.join()} * is waiting for a specified thread to terminate. */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: *

    *
  • {@link #sleep Thread.sleep}
  • *
  • {@link Object#wait(long) Object.wait} with timeout
  • *
  • {@link #join(long) Thread.join} with timeout
  • *
  • {@link LockSupport#parkNanos LockSupport.parkNanos}
  • *
  • {@link LockSupport#parkUntil LockSupport.parkUntil}
  • *
*/ TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; }

java并发编程_第13张图片

sleep属于timed_waiting 有时间的等待

join属于waiting

join(时间) 属于timed_waiting

二、共享模型之管程

关于synchronized 锁对象的一些理解。

在Java中,`synchronized`关键字可以用来实现锁机制,它可以锁定的是对象或者类的**方法**或**代码块**。

1. **同步方法**:当`synchronized`修饰一个方法时,它锁住的是调用该方法的对象实例(即对象锁)。这意味着在同一时间,只能有一个线程访问该对象的这个同步方法。例如:

   ```java
   public class MyClass {
       public synchronized void myMethod() {
           // 这个方法被synchronized修饰,锁住的是当前MyClass对象实例
           // ...
       }
   }
   ```

   在上述例子中,任何时刻只有一个线程能够执行`myMethod`方法,对于不同的`MyClass`实例则是互不影响的。

2. **同步代码块**:更细粒度的控制可以通过指定特定对象来锁定代码块:

   ```java
   public class MyClass {
       private final Object lock = new Object();
       
       public void myMethod() {
           synchronized (lock) {
               // 这个代码块被synchronized修饰,锁住的是变量lock引用的对象
               // ...
           }
       }
   }
   ```

   在这种情况下,锁住的是`lock`对象,所有通过该对象进行同步的代码块将共享同一把锁。

另外,还可以使用`synchronized`锁定类的**静态方法**,这时获取的是类锁(也称为类级锁或全局锁),意味着无论多少个对象实例,同一时间内只有一个线程可以访问此类的任何一个静态同步方法:

```java
public class MyClass {
    public static synchronized void myStaticMethod() {
        // 静态同步方法,锁住的是MyClass类本身
        // ...
    }
}
```

总结来说,`synchronized`既可以作用于对象实例的方法上获得对象锁,也可以作用于类的静态方法上获得类锁。

1.共享带来的问题

java并发编程_第14张图片

java并发编程_第15张图片

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

java并发编程_第16张图片

java并发编程_第17张图片

临界区 Critical Section
一个程序运行多个线程本身是没有问题的
问题出在多个线程访问共享资源
多个线程读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错(不按顺序执行),就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

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

如下图,未产生指令交错,就可以正常执行。

java并发编程_第18张图片

但下面这张图,就会 出现问题

java并发编程_第19张图片

2.synchronized 解决方案

java并发编程_第20张图片

2.1 加在代码中

java并发编程_第21张图片

java并发编程_第22张图片

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

思考一下 下面的问题:

2.2 加在方法上

当在方法上加synchronized时,是锁对象,还是锁类,显而易见。

java并发编程_第23张图片

java并发编程_第24张图片

2.3 看锁类和锁对象的几个案例

java并发编程_第25张图片

java并发编程_第26张图片

情况5 一个是锁的类,一个是锁的对象,因此是不会冲突的。

public class Tea {

    public static synchronized void a() {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("1");
    }

    public synchronized void b() {
        log.info("2");
    }

    public static void main(String[] args) {
        Tea n1 = new Tea();
        new Thread(() -> {
            n1.a();
        }).start();
        new Thread(() -> {
            n1.b();
        }).start();
    }
}

java并发编程_第27张图片

java并发编程_第28张图片

2.4 变量是否线程安全

java并发编程_第29张图片

java并发编程_第30张图片

java并发编程_第31张图片

2.5 常见的线程安全类

java并发编程_第32张图片

java并发编程_第33张图片

java并发编程_第34张图片

private   final 让子类不能重写

String Integer 等是线程安全的,因为不可变。

java并发编程_第35张图片

3.Monitor

3.1 对象头

java并发编程_第36张图片

java并发编程_第37张图片

3.2 工作原理

java并发编程_第38张图片

java并发编程_第39张图片

java并发编程_第40张图片

java并发编程_第41张图片

3.3 轻量级锁

没有竞争,访问时间错开。

java并发编程_第42张图片

java并发编程_第43张图片

java并发编程_第44张图片

 java并发编程_第45张图片

java并发编程_第46张图片

java并发编程_第47张图片

java并发编程_第48张图片

java并发编程_第49张图片

java并发编程_第50张图片

因为此时Object变为了重量级锁,因此,此时虽然Thread0原本持有轻量级锁,但Object中已经变化,Thread0 如果解锁就会失败。

3.4 自旋优化

java并发编程_第51张图片

java并发编程_第52张图片

3.5 偏向锁

缺点:每次发生锁重入,都会产生一条锁记录,

java并发编程_第53张图片

java并发编程_第54张图片

3.6 偏向状态

java并发编程_第55张图片

对于hashcode,当调用对应的hashcode()方法,时,才会把对应的值写到标记头中。

对于开启偏向锁的对象,调用hashcode(),会禁用偏向锁。

对于 轻量锁 会存在锁记录里,因为进行交换了。

对于 重量级锁 hashcode会存在 Monitor里。

3.7 偏向锁的撤销

java并发编程_第56张图片

3.8 锁消除

jit 会对java字节码进行进一步优化,对于反复调用的方法,就会进行优化。

如下方代码,o是一个局部变量,并不会被共享,因此需要用 o加锁没有什么用,因此就会进行优化。

java并发编程_第57张图片

3.9 wait-notify

首先,只有进入 synchronized的块 内 ,才能够调用,wait()和notify()

即 只有获得了synchronized(Object lock)  lock这个锁,然后才能去指向lock.wait();

此时,waitset就有用了!

当使用wait()后,会进入monitor的waitset进行等待,并释放掉当前的锁,等到满足条件,lock.notify() ,随机唤醒一个线程,此时被唤醒的线程就去竞争锁,如果获取成功,就继续执行刚才未执行的代码。

看下图片:

java并发编程_第58张图片

使用while循环 其实是一个不错 的选择

java并发编程_第59张图片

3.10 保护性暂停

3.11 生产者消费者

不管同步代码块正常或者异常,synchonized 都会解锁。

直接调用wait时会报错的!必须先进入synchonized中,获得了Object锁,然后才能去执行Object.wait()方法,进入waitSet 。

Object.wait(等待时间)   如果等待时间没被唤醒,那么就不再等待,去执行它下面的代码,但是 此时应该不是直接获得锁,而是去争夺。

sleep与wait的区别,当在synchronized

java并发编程_第60张图片

4)都是timed-waiting  有时限的等待。

保护性暂停

java并发编程_第61张图片

java并发编程_第62张图片

对上面的代码进行优化一下

java并发编程_第63张图片

join原理

保护性暂停,就是不满足条件时,进行wait()等待

java并发编程_第64张图片

java并发编程_第65张图片

java并发编程_第66张图片

park  unpark

java并发编程_第67张图片

原理:

调用多次unpark效果一样,干粮还是1

java并发编程_第68张图片

java并发编程_第69张图片

重新审视线程状态转换

new->runable->blocked->waiting->timedwaiting->termited

runnable()

java并发编程_第70张图片

4.ReentrantLock

java并发编程_第71张图片

4.1 可重入

java并发编程_第72张图片

 static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        method1();
    }
    public static void method1() {
        lock.lock();
        try {
            log.debug("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }
    public static void method2() {
        lock.lock();
        try {
            log.debug("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }
    public static void method3() {
        lock.lock();
        try {
            log.debug("execute method3");
        } finally {
            lock.unlock();
        }
    }

4.2 可打断

打断后,就不会去获得锁了。

可打断的意义是让 它停止无休止的等待

注意:要调用

lock.lockInterruptibly();

才行。

不然,即使去打断了线程,也不会停止该线程在等待获得锁。

@Slf4j
public class Test002 {
    static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                log.info("被中断");
                e.printStackTrace();
                return;
            }
            //未被中断,获得锁
            log.info("获得锁");
            lock.unlock();
        }, "t1");

        //主线程获得锁
        lock.lock();
        t1.start();
        //打断t1线程
        t1.interrupt();
//        主线程释放锁
        lock.unlock();
    }

4.3 锁超时

tryLock 不带时间,没有获得到锁,立即返回false

tryLock带时间,在对应时间段内没有获得锁,返回false

用trylock解决哲学家就餐问题。

筷子继承了 ReentrantLock,因此可以执行trylock()方法。

class Chopstick extends ReentrantLock {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;
    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }
    @Override
    public void run() {
        while (true) {
// 尝试获得左手筷子
            if (left.tryLock()) {
                try {
// 尝试获得右手筷子
                    if (right.tryLock()) {
                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    left.unlock();
                }
            }
        }
    }
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(1);
    }
}

4.4 公平锁

公平锁:先入先得。(必要性不大,降低并发度)。需要在构造方法时进行指定。

自旋重试,在单核cpu中没啥用,

有趣的实验

1.记录请求次数

对于下面这个,由于指定了多例,每个请求都会创建一个新的 TestController 实例,结果总是返回0

@RestController
@RequestMapping("/add")
@Scope("prototype")
public class TestController {
  private  int a=0;
  @GetMapping
  public int add(){

      return a++;
  }

}

当去掉注解   @Scope("prototype")

此时就是单例了,每个请求都用的同一个对象。

十、船新版本

你可能感兴趣的:(java,开发语言)