当前位置: 首页 > news >正文

Java基础学习-多线程

多线程

1、实现多线程的方式

1. 通过Thread类
public class A_通过Thread实现 {public static void main(String[] args) {/** 1 继承Thread* 2 实现类去重写Thread中的抽象方法:run()* 3 创建实现类的对象,开启线程*/MyThread th1 = new MyThread();MyThread th2 = new MyThread();th1.start();th2.start();}
}
class MyThread extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + " " + i);}}
}
2. 通过Runnable接口
public class B_通过Runnable方法实现 {public static void main(String[] args) {/** 1 自己造一个类,实现Runnable接口* 2 实现类去实现Runnable中的抽象方法:run()* 3 创建实现类的对象* 4 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象* 5 通过Thread类的对象调用start()*/MyRunnable myRunnable = new MyRunnable();Thread th = new Thread(myRunnable);th.start();}
}class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + " " + i);}}
}
3. 利用Callable接口和Future接口
public class C_通过CallableFuture实现 {public static void main(String[] args) throws ExecutionException, InterruptedException {/*** 1 实现Callable接口* 2 实现call()方法* 3 创建MyCallable实体* 4 创建FutureTask实体* 5 创建线程* 6 开启线程* 7 调用get()方法,获取结果*/MyCallable call = new MyCallable();FutureTask<Integer> fu = new FutureTask<>(call);Thread th = new Thread(fu);th.start();Integer result = fu.get();System.out.println(result);}
}class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {//计算1~100的和int sum = 0;for (int i = 1; i <= 100; i++) {sum += i;}return sum;}
}

2、线程的常见方法

  1. String getName()
    • 获取线程的名称
  2. void setName(Stirng name)

    • 设置线程的名称
  3. static Thread currentThread()

    • 获取当前的线程对象
  4. static void sleep(long time)

    • 让线程休眠指定的时间,单位为毫秒
  5. setPriority(int newPriority)

    • 设置线程的优先级
    • 线程的调度
      • 抢占式调度:每个线程随机执行,执行的时间和顺序是随机的 — java 采用抢占式的调度方法
      • 非抢占式调度:每个线程轮流执行,而且每个线程执行的时间也基本是一样的
    • Java 中,优先级越高,抢占线程的概率越大
  6. final int getPriority()

    • 获取线程的优先级
    • Java 中默认的有优先级是 5
  7. final void setDaemon(boolean on)

    • 设置为守护线程
    • 当其他的非守护线程执行完毕后,守护线程就会陆续结束
    • 通俗:当女神线程结束了,那末备胎线程也没有存在的必要了
  8. public static void yield()

    • 出让线程
    • 出让当前线程所抢占的CPU,让线程重新抢占CPU
  9. public static void join()

    • 插入线程

    • 表示把t线程,插入到当前线程之前。

      MyThread t = new MyThread();
      t.start();
      t.join(); //表示把这个线程,插入到main线程之前,即:等t线程执行完毕后,再执行main线程
      

3、线程的生命周期

在这里插入图片描述

4、synchronized用法

1. 同步代码块
public class synchronizedTest {public static void main(String[] args) {MyThread th1 = new MyThread();MyThread th2 = new MyThread();MyThread th3 = new MyThread();th1.setName("窗口1");th2.setName("窗口2");th3.setName("窗口3");th1.start();th2.start();th3.start();}
}class MyThread extends Thread{//表示这个类的所有对象都共享ticket数据static int ticket = 0;//锁对象,一定是唯一的static Object obj = new Object();//一般用,当前类的Class对象@Overridepublic void run() {while(true){//同步锁synchronized (MyThread.class){if(ticket < 100){try {Thread.sleep(100);} catch (Exception e){e.printStackTrace();}ticket ++;System.out.println(getName() + "卖出了第" + ticket + "张票!!!");} else {break;}}}}
}
2. 同步方法
  • 就是把synchronized关键字加到方法上
    • **格式:**修饰符 synchronized 返回值类型 方法名 (参数列表){…}
    • 特点:
      • 同步方法是锁住方法里面所有的代码
      • 锁对象不能自己指定
        • 非静态方法的锁对象是:this
        • 静态方法的锁对象是:当前类的字节码文件
3. Lock锁
public class LockTest {public static void main(String[] args) {MyThread2 th1 = new MyThread2();MyThread2 th2 = new MyThread2();MyThread2 th3 = new MyThread2();th1.setName("窗口1");th2.setName("窗口2");th3.setName("窗口3");th1.start();th2.start();th3.start();}
}class MyThread2 extends Thread {static int ticket = 0;static Lock lock = new ReentrantLock();@Overridepublic void run() {while(true){lock.lock();try {if(ticket == 100) {break;} else {Thread.sleep(10);ticket ++;System.out.println(getName() + "买了第" + ticket + "张票!!!");}} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}}
}

5、多线程的等待唤醒机制

1. 生产者和消费者
public class Test {public static void main(String[] args) {Cook cook = new Cook();Foodie foodie = new Foodie();cook.start();foodie.start();}
}class Desk {// 0:表示没有食物,1:表示有食物public static int foodFloat = 0;//锁对象public static Object lock = new Object();//消费的数量public static int count = 10;
}class Foodie extends Thread {@Overridepublic void run() {//死循环while(true){synchronized (Desk.lock){if(Desk.count == 0){break;} else {if(Desk.foodFloat == 0){try {Desk.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}} else {Desk.count --;System.out.println("吃货还能吃" + Desk.count + "碗饭");Desk.foodFloat = 0;Desk.lock.notifyAll();}}}}}
}class Cook extends Thread {@Overridepublic void run() {while(true){synchronized (Desk.lock){if(Desk.count == 0){break;} else {if(Desk.foodFloat == 1){try {Desk.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}} else {System.out.println("厨师开始做食物");Desk.foodFloat = 1;Desk.lock.notifyAll();}}}}}
}
2. 阻塞队列
public class Test {public static void main(String[] args) {ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);Cook cook = new Cook(queue);Foodie foodie = new Foodie(queue);cook.start();foodie.start();}
}class Cook extends Thread {ArrayBlockingQueue<String> queue;public Cook(ArrayBlockingQueue<String> queue) {this.queue = queue;}@Overridepublic void run() {while(true){try {queue.put("面条");System.out.println("厨师开始做面条");} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}class Foodie extends Thread {ArrayBlockingQueue<String> queue;public Foodie(ArrayBlockingQueue<String> queue) {this.queue = queue;}@Overridepublic void run() {while(true){try {String s = queue.take();System.out.println("吃" + s);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}

6、线程的状态

7、多线程的可见性

每个线程去使用共享内存的时候,是把共享内存的内容复制一份副本进入自己的内存。
1:第一次访问这个共享内存的时候,加载进入副本,只要不修改 就一直使用这个副本,不会重新去拿。

2:当修改了副本之后,他会立刻同步到共享区域中,后续使用的就是修改后的这副本。

在这里插入图片描述

8、多线程的指令重排

  1. Java会把有关联的代码放在一起,提高CUP的执行效率。

  2. 在多线程模式下,可能导致这个线程还没有修改变量的值,另一个线程就获取到旧的值进行判断。

9、原子性与原子类

  1. 当两个线程同时修改同一个共性资源时(static修饰的),可能导致有些的修改不起作用。

    例如:线程1,将 a 自增 100次,线程2,将 a 自增100次,可能最终只是自增了不到200次

  2. 因此,原子性就是指,把 a++的三步合成一步,只有三步全部执行完成了才会去释放CUP的执行权。

  3. 解决方法:

    1. 加锁,即:将a++加一个锁,保证这个代码块执行完成后,才会释放CPU的执行权
      在这里插入图片描述
  1. 原子类:保证数据修改的原子性,即:上述结果的三步合成一步

    1. int - AtomicInteger

    2. long - AtomicLong

    3. boolean - AtomicBoolean

    4. 引用数据类型 - AtomicReference

    5. 原子类源码:

      在这里插入图片描述

11、volatile的使用

  • **使用范围:**只能修饰堆内存或方法区中的变量,即:static 修饰或成员变量。
  • 作用:
    • 使被修饰的变量具有可见性,即:当多个线程同时获取一个变量时,每次都会去主内存中获取这个变量,而不是从自己栈帧中的副本中获取。
    • 阻止代码重排

12、ThreadLocal

  1. 作用:提供线程内的局部变量,不同的线程之间不会互相干扰,这种变量在线程的生命周期内起作用,减少同一线程内多个函数或组件之间一些公共变量传递的复杂度。

    • 线程并发:在线程并发的场景下

    • 传递数据:我们可以通过ThreadLoacl在同一线程,不同组件中传递公共变量

    • 线程隔离:每个线程的变量都是独立的,不会互相影响

      /*
      线程隔离:在多线程的场景下,每个线程中的变量都是相互独立的线程A:设置(变量1) 获取(变量1)线程B:设置(变量2) 获取(变量2)ThreadLocal:1. set():将变量绑定到当前线程中 (在别的线程中获取不到)2. get():获取当前线程绑定的变量
      */
      
  2. 用法
    在这里插入图片描述

13、AQS

1、什么是AQS?

ASQ本质就是JUC包下的一个抽象类,AbstractQueuedSynchronizer类,它是一个基础类,本身并没有实现什么具体的并发共功能,但是很多

JUC包下的工具都是基于AQS实现的。

2、AQS的核心内容是什么?

AQS内部有三个核心内容,一个属性,两个结构。

第一个核心属性:state,就拿ReentrantLock来说,比如 state 为0,代表当前没有线程持有这个lock锁

private volatile int state;

第二个核心结构:一个同步队列(双向链表):拿ReentrantLock来说,如果某个线程想获取锁资源,但发现这个锁资源已经被占用了,那末,这个线程就会被封装成Node对象,进入同步队列的尾部,排队,并挂起等待锁资源。

    private transient volatile Node head;private transient volatile Node tail;

第三个核心结构:Condition的单线链表:当持有lock锁的线程,执行了await方法,会将当前线程封装为Node,插入到单向链表中,等待被其他线程执行signal方法唤醒,然后扔到同步队列中等待竞争锁资源。

本质就是AQS内部类,ConditionObject中提供的一个基于Node对象组成的单线链表。

private transient Node firstWaiter;private transient Node lastWaiter;

该链表的作用是,存放等待被唤醒的线程。

在这里插入图片描述

3、Lock锁和AQS的继承关系?

ReentrantLock锁为例

在这里插入图片描述

整个ReentrantLock获取锁的逻辑:

CSA解释:CAS就是一个CPU支持的一个原语,在经过编译后,CAS指令会被编译成cmpxchg,这个就是CPU支持的原语,CAS本质就是Compare And Swap

在这里插入图片描述

4、公平锁和非公平锁的直观体现?

从源码的层面来看,公平锁与非公平锁只有一点点的不同。

lock 方法一般是获取锁资源的入门方法。

通过源码可以看到,非公平锁会直接尝试抢一次锁资源。

//非公平锁
final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}//公平锁
final void lock() {acquire(1);
}
5、AQS为acquire方法?

acquire方法就是前面的核心处理逻辑。

1、先走tryAcquire方法,再次尝试强锁,抢到了,走人。

2、没抢到走addWaiter方法,准备排队,将线程封装为Node对象,添加到AQS的同步队列中。

3、再走acquireQueued方法,再次获取锁,还是挂起线程,就要看方法内部的具体逻辑。

//acquire 方法实现
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}//方便观察
public final void acquire(int arg) {if (!tryAcquire(arg)){//拿锁失败Node node = addWaiter(Node.EXCLUSIVE);acquireQueued(node, arg))selfInterrupt();}
}
6、AQS的tryAcquire底层逻辑?

tryAcquire有两种实现,公平锁和非公平锁。

// 非公平锁的 tryAcquire 的实现逻辑 (java 8)
final boolean nonfairTryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();//获取state属性的值int c = getState();if (c == 0) {// 当前锁资源没有线程持有。执行CAS,尝试将state从0改为1if (compareAndSetState(0, acquires)) {// 修改成功,将 exclusiveOwnerThread 设置为当前线程setExclusiveOwnerThread(current);// 返回true,拿锁成功return true;}}// 当前锁资源被占用,占用锁资源的是不是我自己。。。。else if (current == getExclusiveOwnerThread()) {// 所重入的逻辑,直接对state + 1int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");// 赋值给statesetState(nextc);// 锁重入成功return true;}// 那锁失败return false;
}// 公平锁
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// hasQueuedPredecessors() 如果当前锁资源没有被占用,需要通过一定的判断才可以尝试抢锁。// 1、如果AQS的同步队列没有排队的Node,直接抢锁!执行CAS!// 2、如果有排队的Node,并且排在第一名的是当前线程,直接抢锁,执行CAS!// 3、如果有排队的Node,并且排在第一名的不是当前线程,不能抢锁,直接返回false if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}
7、AQS的addWaiter的底层逻辑?

addWaiter 操作中,只需要关注一个事情,如果出现了并发插入到AQS同步队列这个操作。

需要保证原子性。而保证原子性的方式,就是哪个线程成功的基于CAS将tail的指向换成了自己,谁就优先插入到同步队列的末尾。

在这里插入图片描述

如果CAS失败,会走到一个死循环,不断重试1,2操作

8、AQS的acquireQueued底层逻辑
final boolean acquireQueued(final Node node, int arg) {// 这里省略了取消节点和中断相关的信息for (;;) {// 获取当前 Node 节点的 prev 节点(上一个节点)final Node p = node.predecessor();// p == head,说明当前节点就是head.next,排在第一名的节点if (p == head) {//直接走 tryAcquire 抢锁。if(tryAcquire(arg)){// 到这,说明抢锁成功!// 抢锁成功后,做换头操作。// 会将获取资源的 node,作为新的head节点,并且将prev以及之前head的next置为null,目的是为了让之前的head可以被快速回收// 换头是为了保留状态......setHead(node);p.next = null;return false; }}// 不是第一名,不让抢。第一名抢锁失败。// 挂起逻辑// Node 节点的状态// CANCELLED == 1:当前Node不排了,马上要走了// SIGNAL == -1:当前Node的next挂起了。// 其他状态 == 0...正常状态if (shouldParkAfterFailedAcquire(p, node)){// 挂起线程parkAndCheckInterrupt();}}
}// pred:上一个节点。 node:当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 获取上一个节点状态int ws = pred.waitStatus;// 上一个节点状态是-1,直接返回trueif (ws == Node.SIGNAL)return true;// 如果上一个节点状态是取消,基于do-while循环往前找,直到找到一个状态不是取消的节点。// 绕过这些取消的节点。if (ws > 0) {do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 如果上一个节点状态正常,直接基于CAS,将上一个节点装填修改为-1compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}

14、锁

1、ReentrantLock可重入锁

可重入锁分为两种:公平锁、非公平锁

2、ReentrantReadWriteLock读写锁

例如:

​ 悲观锁:效率低,不支持并发

​ 乐观锁:效率高,支持并发

特点:一个资源可以有多个线程读取,但是只能由一个线程写入

锁降级:将读锁 降级到 读锁(过程:获取写锁 - 获取读锁 - 释放写锁 - 释放读锁

读锁不能升级为写锁

3、阻塞队队列

BlockingQueue的方法

在这里插入图片描述

相关文章:

  • python+uniapp基于微信小程序健康管理系统
  • 数字电路研究的是直流信号还是交流信号
  • BUU reverse (随机做题)
  • Postman中变量的使用
  • idea中配置svn及提交提示“未检测到更改”提示
  • uni-app插件,高德地图、地图区域绘制、定位打卡
  • 海报在线制作系统小程序ThinkPHP+UniApp
  • [论文阅读] 人工智能 + 软件工程 | 用大语言模型架起软件需求形式化的桥梁
  • 无人机数据处理系统设计与难点
  • Uniapp条件编译完全指南:跨平台开发的核心技术
  • 跨越延迟障碍,从15秒到2毫秒,通过MODBUS转ETHERNET IP网关将变送器接入AB PLC
  • git操作练习(2)
  • Gartner发布网络安全组织设计指南:设计网络安全组织的五项原则和六种主要安全组织类型
  • RS232转Profinet网关推动车间数字化转型
  • Rust 机器学习
  • 基于proxysql实现MySQL读写分离
  • 1:9.7p1-7ubuntu4.3 安全加固升级9.9p2-2_SSH
  • SpringBoot 插件化架构的4种实现方案
  • 指针篇(2)- const修饰,野指针,assert断言,指针的使用和传址调用
  • Happy-LLM 第一章 NLP概述
  • wordpress仿威客插件/游戏优化软件
  • 免费注册网站怎么做链接/百度人工服务热线电话
  • 局网站建设自查/百度搜索如何去广告
  • 个人网站数据库怎么做/西安危机公关公司
  • 如何做二手车网站/天津seo外包
  • wordpress建站很麻烦/企业网站关键词优化