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{
    @Override
    public 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{
    @Override
    public 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> {
    @Override
    public 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对象

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

    @Override
    public 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 {
    @Override
    public 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 {

    @Override
    public 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;
    }
    @Override
    public 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;
    }
    @Override
    public 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());
    else
        acquire(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改为1
        if (compareAndSetState(0, acquires)) {
            // 修改成功,将 exclusiveOwnerThread 设置为当前线程
            setExclusiveOwnerThread(current);
            // 返回true,拿锁成功
            return true;
        }
    }
    // 当前锁资源被占用,占用锁资源的是不是我自己。。。。
    else if (current == getExclusiveOwnerThread()) {
        // 所重入的逻辑,直接对state + 1
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 赋值给state
        setState(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,直接返回true
    if (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,将上一个节点装填修改为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

14、锁

1、ReentrantLock可重入锁

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

2、ReentrantReadWriteLock读写锁

例如:

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

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

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

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

读锁不能升级为写锁

3、阻塞队队列

BlockingQueue的方法

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值