多线程第一章-基础知识

线程基础

2,线程的等待与唤醒

wait

当一个线程调用一个共享变量的时候,当前线程会被挂起,直到有其他线程使用notify()方法唤醒后才会执行

使用wait()和notify()方法时当前线程需要获取共享对象的监视器锁,不然调用会出现IllegalMonitorStateException异常,举例:

public class WaitTest {
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    ToWork.getInstance().everyWork();
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            ToWork.getInstance().notify();
        }).start();

            ToWork.getInstance().wait();

        System.out.println("休眠结束");
    }
}
public class ToWork {
    private static final ToWork toWork = new ToWork();

    public static ToWork getInstance(){
        return toWork;
    }
    public void everyWork() {
        System.out.println("every day to Work");
    }
}

运行结果:

正确的使用方式是再wait和notify时都要加上锁

public class WaitTest {
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (ToWork.getInstance()) {
                for (int i = 0; i < 10; i++) {
                    try {
                        ToWork.getInstance().everyWork();
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                ToWork.getInstance().notify();
            }
        }).start();

        System.out.println("开始 blocking。。。。。");

        synchronized (ToWork.getInstance()) {
            ToWork.getInstance().wait();
        }
        System.out.println("休眠结束");
    }
}

所以wait和notify的使用一定时伴随着synchroniezd一起使用的,或者使用Lock锁也是一样

示例写一个生产者和消费者模型

/**
 * @Author liucy
 * @Date 2024/4/25 15:07
 * @Desc 生产者,消费者
 */
public class QueueTest {

    private static LinkedBlockingQueue<String> queue = new LinkedBlockingQueue();

    public static void main(String[] args) {
        Thread product = new Thread(() ->{
            while (true) {
                synchronized (queue) {
                    while (queue.size() == 10) {
                        try {
                            //队列数据满了,挂起线程,释放锁
                            queue.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    //如果生产者队列有空闲就一直写入元素,并且唤醒线程
                    queue.add(String.valueOf(1));
                    queue.notify();
                }
            }
        });


        Thread consumer = new Thread(() ->{
            while (true) {
                synchronized (queue) {
                    while (queue.size() == 0) {
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    try {
                        System.out.println("consumer : " +queue.take());
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    queue.notifyAll();
                }
            }
        });

        product.start();
        consumer.start();
    }
}

3,线程的join

需要在几个线程执行完毕之后再执行,例如加载资源等,join方法可以让线程顺序执行
例如

public class Example_1 {

    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程1启动");
        },"thread-1");

        Thread threadTwo = new Thread(() -> System.out.println("线程2启动"),"thread -2 ");

        threadOne.start();
        threadTwo.start();

        threadOne.join();
        threadTwo.join();
        System.out.println("所有子线程执行完毕");
    }
}

join是thread类自带的方法,后续更好的CountDownLatch来使用

4,yeild方法

yeild方法是线程让出当前cpu的执行权,当前线程会由执行状态转换为就绪状态,cpu会从就绪的线程中选择一个线程继续执行,代码示例:

public class YieldTest implements Runnable{

    public YieldTest() {
        Thread thread = new Thread(this);
        thread.start();
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            if (i % 5 == 0) {
                System.out.println(Thread.currentThread() + " yield cup");
            }
            Thread.yield();
        }
        System.out.println(Thread.currentThread() + " is over");
    }
}

5,线程的中断

Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

  • void interrupt()方法: 中断线程
  • boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。
  • boolean interrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false

6,线程死锁

6.1 产生的原因

  1. 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源
  2. 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源
  3. 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源
  4. 环路等待条件:指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0, T1, T2,…, Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被T0占用的资源

死锁代码示例

public class DeadLockTest {

    public static void main(String[] args) {
        new Thread(() ->{
            synchronized (ResourceA.class) {
                System.out.println(Thread.currentThread().getName() + "get resource A");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (ResourceB.class) {
                    System.out.println(Thread.currentThread().getName() + "get resource B");
                }
            }
        }).start();

        new Thread(() ->{
            synchronized (ResourceB.class) {
                System.out.println(Thread.currentThread().getName() + "get resource B");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (ResourceA.class) {
                    System.out.println(Thread.currentThread().getName() + "get resource A");
                }
            }
        }).start();
        
    }
    
    class ResourceA {

    }

    class ResourceB{

    }
}

6.2 如何破坏死锁

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件

7,守护线程和用户线程

java中的线程主要分为守护线程和用户线程,当启动一个main函数之后就启动了一个用户线程,JVM内部也存在很多守护线程,例如GC回收线程,他们的区别就是当最后一个非守护线程退出之后,JVM会正常退出,不管守护线程是否退出

7.1 创建守护线程

public class DaemonThread {
    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
           for (;;) {

           }
        });
        //设置该线程为守护线程
        thread.setDaemon(true);
        thread.start();
    }
}

8,ThreadLocal

ThreadLocal主要作用是创建线程本地变量,是变量在每个线程本地的一个副本

public class ThreadLocalTest {

    private static ThreadLocal<String> localVariable = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            localVariable.set("thread one local variable ");
            print("threadOne");
            System.out.println("threadOne remove after" + ":" + localVariable.get());
        }).start();

        new Thread(() -> {
           localVariable.set("thread two local variable");
           print("threadTwo");
            System.out.println("threadTwo remove after" + ":" + localVariable.get());
        }).start();

    }
    
    private static void print(String str) {
        System.out.println(str + ":"+ localVariable.get());
        //清除当前线程的本地内存中的值
        localVariable.remove();
    }
}

第二节

2.1 volatile关键字

因为synchronized是一种重量级的锁,且使用的时候会引起线程的上下文切换开销,性能影响较大,java提供了一种弱形式的同步,使用volatile关键字来保证共享变量的内存可见性和防止指令重排序
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值
使用volatile的场景

写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取—计算—写入三步操作,这三步操作不是原子性的,而volatile不保证原子性

2.2 CSA非阻塞实现原子性

CAS即Compare and Swap,其是JDK提供的非阻塞原子性操作,它通过硬件保证了比较—更新操作的原子性。JDK里面的Unsafe类提供了一系列的compareAndSwap*方法

  • boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update)方法:其中compareAndSwap的意思是比较并交换。CAS有四个操作数,分别为:对象内存位置、对象中的变量的偏移量、变量预期值和新的值。其操作含义是,如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect。这是处理器提供的一个原子性指令

CAS操作有个经典的ABA问题,具体如下:假如线程I使用CAS修改初始值为A的变量X,那么线程I会首先去获取当前变量X的值(为A),然后使用CAS操作尝试修改X的值为B,如果使用CAS操作成功了,那么程序运行一定是正确的吗?其实未必,这是因为有可能在线程I获取变量X的值A后,在执行CAS前,线程II使用CAS修改了变量X的值为B,然后又使用CAS修改了变量X的值为A。所以虽然线程I执行CAS时X的值是A,但是这个A已经不是线程I获取时的A了。这就是ABA问题。ABA问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从A到B,然后再从B到A。如果变量的值只能朝着一个方向转换,比如A到B, B到C,不构成环形,就不会存在问题。JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生。

2.3 Unsafe类

public class TestUnSafe {
    /**
     * TestUnSafe会报异常
     */
//    static Unsafe unsafe = Unsafe.getUnsafe();
    static Unsafe unsafe;
    public static final long stateOffset;
    public volatile long state = 0;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
    
    public static void main(String[] args) {
        TestUnSafe test = new TestUnSafe();
        boolean success = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(success);
    }
}

2.4 指令重排序

Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题

public class Securiter {
    private static int num;
    private static boolean ready = false;

    public static void main(String[] args) throws InterruptedException {
        Thread readThread = new Thread(() -> {
            while (! Thread.currentThread().isInterrupted()) {
                if (ready) {//(1)
                    System.out.println(num + num);//(2)
                }
                System.out.println("read Thread");
            }
        });

        Thread writeThread = new Thread(() -> {
           num = 2;//(3)
           ready = true;//(4)
           System.out.println("write Thread set over");
        });
        readThread.start();
        writeThread.start();
        Thread.sleep(100);
        readThread.interrupt();
        System.out.println("main exit");
    }
}

在不考虑内存可见性问题的情况下上面代码一定会输出4?答案是不一定,由于代码(1)(2)(3)(4)之间不存在依赖关系,所以写线程的代码(3)(4)可能被重排序为先执行(4)再执行(3),那么执行(4)后,读线程可能已经执行了(1)操作,并且在(3)执行前开始执行(2)操作,这时候输出结果为0而不是4。

2.5 锁的概念

2.5.1 乐观锁与悲观锁

  • 悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。悲观锁的实现往往依靠数据库提供的锁机制,即在数据库中,在对数据记录操作前给记录加排它锁。如果获取锁失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常。如果获取锁成功,则对记录进行操作,然后提交事务后释放排它锁。
  • 乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测

2.5.2 公平锁与非公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。而非公平锁则在运行时闯入,也就是先来不一定先得。

2.5.3 独占锁与共享锁

根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock就是以独占方式实现的。共享锁则可以同时由多个线程持有,例如ReadWriteLock读写锁,它允许一个资源可以被多线程同时进行读操作。

2.5.4 可重入锁

当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时是否会被阻塞呢?如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数(在高级篇中我们将知道,严格来说是有限次数)地进入被该锁锁住的代码

2.5.5 自旋锁

由于Java中的线程是与操作系统中的线程一一对应的,所以当一个线程在获取锁(比如独占锁)失败后,会被切换到内核状态而被挂起。当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程。而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是10,可以使用-XX:PreBlockSpinsh参数设置该值),很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。由此看来自旋锁是使用CPU时间换取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费了。

3,多线程高级

3.1 原子操作类(AtomicLong)

3.2 JDK1.8的LongAdder(原子操作类)

AtomicLong通过CAS提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说它的性能已经很好了。但是,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS操作会成功,这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试CAS的操作,而这会白白浪费CPU资源。

3.2 并发List

线程安全的并发List只有CopyOnWriteArrayList,是通过将数据从一个数组复制到另一个数组实现的

posted @   浪成于微澜之间  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示