Java多线程基本概念、线程的创建方式(有、无返回值)、常用方法、synchronized锁、线程安全问题、死锁以及如何避免

3 篇文章 0 订阅

基本概念

进程和线程

首先需要了解下进程和线程的区别:

  • 进程其实就是程序,每个进程中的代码和数据空间(进程上下文)都是独立的,进程间的切换会有较大的开销,一个进程中包含 >= 1个线程(进程是资源分配的最小单位)
  • 一个进程会包含多个线程,每个线程间都有独享的空间和共享的内存,在java中独享的就是线程栈(每个线程都有一个线程栈),共享的就是堆内存、方法区等。(线程是CPU调度的最小单位)

以windows系统为例,看下图

进程

在这里插入图片描述

线程

在这里插入图片描述

如上图是我启动的一个java程序,最少会创建两个线程:一个用户线程(Main方法主线程)、一个守护线程(GC垃圾回收线程)

并发和并行

  • 并行:多个线程跑在多个CPU的操作系统上,这些线程是同时执行的,不需要进行CPU时间片的抢夺,不需要进行线程间的上下文切换,理解为:同一时间点,有多个任务同时在执行,互不干扰。
  • 并发:线程数量较多时,CPU的数量是有限的,这些线程需要进行抢夺CPU时间片执行自己的任务,要进行线程间的上下文切换,同一时间点,有多个任务需要执行,但是只能一个一个执行,抢夺到CPU时间片的线程可以进行执行任务。后面我们使用SpringBoot时可以整合线程池进行管理这些线程

Java线程的创建是依赖于系统内核的,通过JVM调用系统库创建内核线程,内核线程与Java线程是一对一的映射关系

线程的状态/阶段

  • 创建

    当一个线程对象被创建时,如:new Thread(),创建了一个对象,那么这个线程对象是创建状态
    
  • 就绪

    线程创建完毕后,调用对象的start()方法后,会在jvm中新开一个线程并且处于就绪状态,可以随时调用run()方法执行任务(在有CPU资源的时候,假如是多线程并发,那么需要抢夺到CPU时间片才能执行)
    
  • 运行

    就绪状态的线程获取了CPU,执行程序代码
    
  • 阻塞

    阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
    阻塞的情况:
      1.等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
      2.同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中
      3.其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
    
  • 死亡

    线程执行完了或者因异常退出了run()方法,该线程结束生命周期
    

线程的优先级

Java线程具有优先级,优先级高的线程可以有更多的机会获取到CPU时间片执行任务。

Java线程的优先级是用整数来表示,取值范围是 1~10,在Thread类中有三个优先级的静态常量:

/**
  * The minimum priority that a thread can have.
  * 最低优先级 1
  */
 public final static int MIN_PRIORITY = 1;

/**
  * The default priority that is assigned to a thread.
  * 默认优先级 5
  */
 public final static int NORM_PRIORITY = 5;

 /**
  * The maximum priority that a thread can have.
  * 最高优先级 10
  */
 public final static int MAX_PRIORITY = 10;
  • Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
  • 每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
  • 线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。

如何创建一个线程

  • 继承Thread类,重写run(),无返回值,不能抛出异常
  • 实现Runnable接口,重写run(),无返回值,不能抛出异常
  • 实现Callable接口,重写call(),有返回值,可放入FutureTask中阻塞获取返回值,可以抛出异常

为什么继承Thread类和实现Runnable接口重新的run()不能抛出异常,而call()方法可以呢,看源码

Thread类就是实现自Runnable接口,重写的run()方法

public
class Thread implements Runnable {
    
    ....
    
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}


看下Runnable接口的run(),一个抽象方法,且没有抛出任何异常,所以子类不能抛出比父类更多、更大范围的异常

@FunctionalInterface
public interface Runnable {
	
    public abstract void run();
}

再看Callable的call(),是抛出一个Exception异常,所以子类重写时可以抛出范围 <= Exception 的异常。

@FunctionalInterface
public interface Callable<V> {

    V call() throws Exception;
}

Thread类的构造函数

构造方法名备注
Thread()
Thread(String name)name为线程名字
Thread(Runnable target)
Thread(Runnable target, String name)name为线程名字

第一二构造函数,适用于继承自Thread类,直接将任务和线程放在了一起,可扩性较低

第三四构造函数,适用于实现Runnable接口,这种是任务和线程分开放置,扩展性较高,并且方便实现资源对象共享

如果需要某些计算或需要线程执行完要获取返回值,可以用实现Callable接口方式,且放入到FutureTask中,有get()方法能阻塞获取返回值。

而且要注意,创建线程一定是Thread类对象的start()方法,而不是调用run()方法,调用start()时,jvm会去创建一个线程然后再去调用对象的run()方法,如果直接调用run()方法,那只是进行了普通的方法调用,不会创建新的线程。

看几个简单的小例子

  • 继承自Thread类,简单打印
public class MyThreadTest {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        // 启动线程
        thread.start();

        // 主线程执行打印
        for(int i = 0; i < 100; i++){
            System.out.println("主线程线程--->" + i);
        }

    }
}

class MyThread extends Thread{

    @Override
    public void run() {
        // 分支线程打印
        for(int i = 0; i < 100; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}
  • 实现Runnable接口,设置线程名称
public class MyRunnableTest {

    public static void main(String[] args) {
        MyRunnable rn = new MyRunnable();
        Thread t = new Thread(rn, "s2");
        t.start();

        for(int i = 0; i < 100; i++){
            System.out.println("主线程线程--->" + i);
        }

    }
}

class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("分支线程" + Thread.currentThread().getName() + "--->" + i);
        }
    }

}
  • 实现Callable接口,可以有返回值,可以抛出异常,需要放入FutureTask中,可以阻塞当前线程获取返回值
public class MyCallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(new MyCallable());
        // 创建线程,为就绪状态
        new Thread(task).start();

        // 阻塞主线程,获取返回值
        Integer integer = task.get();
        System.out.println(integer);
        
    }
}

class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("call...被调用");
        return 2022;
    }
}

继承Thread和实现Runnable的区别

回忆下:一个线程启动肯定需要用到Thread类的start()方法,如果A类继承自Thread的(任务和线程都在一起),并且A类中进行了重写run(),那么我们直接创建一个A类,启动线程直接调用A类的start()方法即可。

而如果B类实现Runnable接口(只是一个任务类),重写run()方法,我们仍需要一个Thread类,调用Thread类对象的start()启动线程。

可以理解为:

  • 继承自Thread类的A类,是一个任务和线程集一体的类
  • 实现Runnable接口的B类,是一个任务类,这个任务需要一个线程去执行它

总结:实现Runnable接口具有的优势:

  • 适合多个相同的程序代码的线程去处理同一个资源
  • 可以避免java中的单继承的限制
  • 可以用线程池管理(线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类)

常用方法

Thread类

方法名解释
static Thread currentThread()获取当前线程对象
String getName()获取线程对象名字
void setName(String name)修改线程对象名字
void sleep(long millis)当前线程休眠(毫秒),放弃CPU时间片的使用,让给其他线程,注意:sleep不会释放对象锁的。
void yield()暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程
join()在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态
interrupt()将会设置该线程的中断状态位,即设置为true,只是改变了中断状态,不会中断一个正在运行的线程
setPriority(int newPriority)设置线程优先级
int getPriority()获取线程的优先级
setDaemon(boolean on)设置是否为守护线程

详解

yield()方法

暂停当前正在执行的线程,让此线程回到就绪状态,以允许具有相同优先级的其他线程获得运行机会。

因此,yield()的目的是让具有相同优先级别的线程获取CPU资源,但是实际中无法保证yield()百分百达到礼让目的,很可能当前线程刚回到就绪状态立刻又抢夺到了CPU的时间片,执行了任务。

sleep()方法

当前线程进入睡眠状态(阻塞状态),直至达到了设置的毫秒数时间后该线程进入就绪状态,可随时执行任务。而且在线程进入阻塞状态时,不会释放已经持有的对象锁,来个例子

public class MyRunnableTest {

    public static void main(String[] args) {
        MyRunnable rn = new MyRunnable();
        Thread t1 = new Thread(rn, "任务1");
        Thread t2 = new Thread(rn, "任务2");
        t1.start();
        t2.start();
    }
}

class MyRunnable implements Runnable {

    private final Object o = new Object();

    @SneakyThrows
    @Override
    public void run() {
        synchronized (o) {
            System.out.println("分支《" + Thread.currentThread().getName() + "》在执行,进入睡眠...");
            Thread.sleep(5000);
            System.out.println("分支《" + Thread.currentThread().getName() + "》睡眠结束,开始释放锁...");
        }
    }
}

看结果

分支《任务1》在执行,进入睡眠...
分支《任务1》睡眠结束,开始释放锁...
分支《任务2》在执行,进入睡眠...
分支《任务2》睡眠结束,开始释放锁...

任务1线程在进入阻塞状态时,对象锁并没有被释放掉,那么任务2只能也一起进入了阻塞状态,等待任务1将锁释放。

join()方法

强制将线程插入到当前线程中,当前线程进入阻塞,直至join()进来的线程执行完毕,来例子

public class MyRunnableTest {

    public static void main(String[] args) throws InterruptedException {
        MyRunnable rn = new MyRunnable();
        Thread t1 = new Thread(rn, "任务1");
        Thread t2 = new Thread(rn, "任务2");
        t1.start();
        t2.start();

        // t1 t2强制加入
        t1.join();
        t2.join();
        System.out.println("我是主线程");
        
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程《" +Thread.currentThread().getName()+ "》执行" + i);
        }
    }

}

结果,主线程最后执行。

....
线程《任务2》执行89
线程《任务2》执行90
线程《任务2》执行91
线程《任务2》执行92
线程《任务2》执行93
线程《任务2》执行94
线程《任务2》执行95
线程《任务2》执行96
线程《任务2》执行97
线程《任务2》执行98
线程《任务2》执行99
我是主线程
interrupt()

将会设置该线程的中断状态位,即设置为true,线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)

interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。可以根据标记位,进行手动停止任务,例子如下

public class MyRunnableTest {

    public static void main(String[] args) throws InterruptedException {
        MyRunnable rn = new MyRunnable();
        Thread t1 = new Thread(rn, "任务1");
        t1.start();
        System.out.println("我是主线程");
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行" + i);

        }
        t1.interrupt();

    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程《" +Thread.currentThread().getName()+ "》执行" + i);
            if (Thread.interrupted()) {
                break;
            }
        }
    }
}
我是主线程
主线程执行0
线程《任务1》执行0
主线程执行1
线程《任务1》执行1
线程《任务1》执行2
主线程执行2
线程《任务1》执行3
主线程执行3
线程《任务1》执行4
线程《任务1》执行5
线程《任务1》执行6
主线程执行4
线程《任务1》执行7
线程《任务1》执行8

Object类

方法名解释
wait()使获取到此对象锁的线程进入无限休眠状态,且释放当前持有的对象锁,直至其他持有相同对象锁线程调用notify()或notifyAll()来唤醒此线程
notify()唤醒一个在这个对象的监视器上等待的单个线程
notifyAll()唤醒正在等待此对象监视器上的所有线程

详解

wait()方法、notify()

Obj.wait(),Obj.notify()必须要与synchronized(Obj)一起使用,其实就是wait()等方法必须要在synchronized代码块中使用,且代码块中的锁对象必须是当前次Obj对象,wait()与notify()都只能针对已经获取了Obj锁进行操作。

从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。

但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。

Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

来个案例,三个线程分别打印A,B,C各打印十次,需要使用wait()和notify()进行线程间的等待/唤醒交互。

public class MyABCTest {

    public static void main(String[] args) throws InterruptedException {
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();

        MyABC printA = new MyABC("A", c, a);
        MyABC printB = new MyABC("B", a, b);
        MyABC printC = new MyABC("C", b, c);

        // 确保顺序是按照 A,B,C执行
        new Thread(printA).start();
        Thread.sleep(300);
        new Thread(printB).start();
        Thread.sleep(300);
        new Thread(printC).start();
    }
}
class MyABC implements Runnable {
    private String name;
    private final Object prev;
    private final Object self;
    MyABC(String name, Object prev, Object self) {
        this.name = name;
        this.prev = prev;
        this.self = self;
    }
    @Override
    public void run() {
        int i = 10;
        while (i > 0) {
            synchronized (prev) {	// 现获取 上一个对象监视器(对象锁)
                synchronized (self) {	// 获取当前对象监视器(对象锁)
                    System.out.println(name);
                    i--;
                    self.notify();
                }
                try {
                    prev.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。

程序解读:

第一轮,为了确保程序执行是从 A->B->C开始的,我们需要先确保第一个执行的是线程A,A线程先执行时先拿到c对象和a对象的对象锁,MyABC("A", c, a)其中c对象和a对象对于B、C线程都是被锁住的,那么B、C线程是执行不了的,因为B、C线程都需要c对象或a对象的锁;A执行完毕后将a对象线程唤醒,将A线程c对象陷入无限等待,那么此时A线程已经无法执行了,除非被其他线程c对象唤醒。主程序睡眠0.3s后B线程开始执行,MyABC("B", a, b)中拿到a对象和b对象的锁,B线程执行完毕后将b对象线程唤醒,B线程a对象陷入无限等待,此时B线程已经无法执行了,除非被其他线程a对象唤醒。主程序再次睡眠0.3s后B线程开始执行C线程,MyABC("C", b, c)拿到b对象和c对象的锁开始执行任务,执行完毕后唤醒c对象陷入等待的线程,C线程b对象陷入无限等待。

注意:此时一波执行下来后,a对象和b对象都已经陷入无限等待了(也就是B线程和C线程现在已经执行不了了,只能等待被唤醒),然后c对象是被唤醒的,那么后面第二轮轮中A线程就可以先执行,A执行完再锁住A线程的c对象,唤醒a对象(A线程和C线程此时是陷入无限等待的),那么B线程就可以执行了…以此类推,完成分别打印A、B、C。

notifyAll()

notifyAll()与notify()的功能都是一样的,都是唤醒其他线程,然后不同的是notifyAll()是唤醒所有在此对象上陷入无限等待的线程,而notify()是随机唤醒一个

sleep()和wait()区别

也算是一个比较常见的面试题了,大概区别就是:

相同点:

  • 都是在多线程环境下,能阻塞线程
  • wait()和sleep()都能通过interrupt()打断线程的暂停状态,使线程立刻抛出异常InterruptedException

不同点:

  • sleep()源自Thread类,wait()源自Object类
  • sleep()可以在线程的任意地方使用,wait()必须在同步方法或同步代码块中使用,wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
  • sleep()睡眠时不会释放对象监视器(对象锁),仍然占用锁;而wait()进入睡眠时会释放锁

synchronized

作用域

synchronized是java中线程同步的关键字,作用域有两种:

  • synchronized修饰普通实例方法,作用于某个对象实例内,同一个实例如果有两个synchronized方法,一个线程只要访问了其中一个,那么其他线程就不能同时再访问此实例中的任何synchronized方法;而如果是不同实例,那么是互不影响的
  • synchronized修饰静态方法,作用于整个类,一个类中只有一个类锁,防止多个线程同时访问这个类中的synchronized static 方法,可以对类的所有实例起作用。

synchronized对象锁

总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。

  • synchronized关键字无论是放在方法上还是代码块中,它取得的锁都是对象,不是把一段代码或方法作为锁
  • 一个对象只有一个锁,同理,一百个对象就有一百个锁
  • 在能不加锁的情况下就别加,共享变量可以设置为局部变量或者别的解决方式

以下几种情况,锁对象的情况:

  • 一个普通的同步方法

    class A {
        public synchronized void aaa() {
    		...
        }
    }
    

    那么它的锁对象是谁呢?它锁定的是调用这个同步方法的对象

    A a = new A();
    // 锁住的就是a对象
    a.aaa();
    
  • synchronized代码块锁定this

    class A {
        public void aaa() {
    		synchronized(this) {
                
            }
        }
    }
    

    此例子和上述一致,this锁的就是当前调用此方法的对象

    A a = new A();
    // 锁住的就是a对象
    a.aaa();
    
  • synchronized代码块中锁定指定对象

    class A {
        public Object a = new Object();
        public void aaa() {
    		synchronized(a) {
                ...
            }
        }
    }
    

    这种就是在代码块中锁住指定的对象,只有获取到a对象的对象锁时才能执行代码块中的内容。

    A a = new A();
    // 锁的是A类中的a对象
    a.aaa();
    

    当场景中没有明确的对象作为锁时,只是为了达到让代码同步执行,可以创建一个特殊的变量来当做锁。

    
    class Foo implements Runnable
    {
        private byte[] lock = new byte[0];  // 特殊的instance变量
        public void methodA()
        {
            synchronized(lock) {
                
            }
        }
    }
    

总结

  • 每一个对象都有一个锁,当一个线程获取到对象锁时,这个对象已经被第一个线程锁住了,其他线程是无法访问此对象中任意方法的(同步和非同步都不行)。
  • 线程同步就是为了解决多线程间数据共享带来的数据安全性问题的。
  • 当一个线程获得了对象锁,访问一个同步方法,假如同步方法中又调用了其他的同步方法(不是本对象的),那么这个线程拥有两把锁。
  • 对于静态的同步方法,它锁住的是Class类对象(每个类只有一个Class类对象),非静态的方法,锁住的是当前的对象;静态同步方法和非静态同步方法的锁互不干扰。
  • 必须要明确到底是要锁住哪个对象来达到同步手段。

线程安全问题

Java中的三大变量

  • 实例变量:在中。
  • 静态变量:在方法区中。
  • 局部变量:在中。

以上三大变量中:

局部变量永远都不会存在线程安全问题。

  • 因为局部变量不共享。(一个线程一个栈。)

  • 局部变量在中。所以局部变量永远都不会共享。

  • 实例变量在堆中,堆只有1个。

  • 静态变量在方法区中,方法区只有1个。

堆和方法区都是多线程共享的,所以可能存在线程安全问题。

如何解决线程安全问题

只要是涉及到加锁的,都是通过时间来换取安全性,不到万不得已不要加锁。

  • 尽量使用局部变量,局部变量没有线程安全问题
  • 实例变量的话,考虑多创建对象,不同的对象引用不同,那么变量间都是互不干扰的
  • 采用锁机制

死锁

死锁其实就是在多线程环境下,发生资源抢夺,双方都不愿意释放锁,一直僵持着。

面试:死锁代码,两线程发生资源抢夺。

public class DeadLockTest {

    public static void main(String[] args) {
        Car car = new Car();
        Bike bike = new Bike();
        Thread carT = new Thread(new LockCar(car, bike));
        Thread bikeT = new Thread(new LockBike(car, bike));
        carT.start();
        bikeT.start();
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class LockCar implements Runnable {
    private Car car;
    private Bike bike;
    @SneakyThrows
    @Override
    public void run() {
        synchronized (car) {
            System.out.println("LockCar拿到car了");
            Thread.sleep(1000);
            synchronized (bike) {
                System.out.println("LockCar拿到bike了");
            }
        }
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class LockBike implements Runnable {
    private Car car;
    private Bike bike;
    @SneakyThrows
    @Override
    public void run() {
        synchronized (bike) {
            System.out.println("LockBike拿到bike了");
            Thread.sleep(1000);
            synchronized (car) {
                System.out.println("LockBike拿到car了");
            }
        }
    }
}
class Car {
}

class Bike {
}

如何避免死锁

  • 按照顺序加锁,每个线程都按照相同的顺序加锁不会造成死锁,多个线程先去抢夺同一个资源,再去抢夺其他。
  • 给锁加时限,如果超时那就放弃获取锁。
  • 按照线程间获取锁的关系检查是否会死锁,如果发生死锁执行一定的回滚策略,如中断线程。

参考

Java多线程学习(吐血详细总结)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
线程的状态以及各状态之间的转换详解.mp4 线程的初始化,中断以及其源码讲解.mp4 多种创建线程方式案例演示(一)带返回值方式.mp4 多种创建线程方式案例演示(二)使用线程池.mp4 Spring对并发的支持:Spring的异步任务.mp4 使用jdk8提供的lambda进行并行计算.mp4 了解多线程所带来的安全风险.mp4 从线程的优先级看饥饿问题.mp4 从Java字节码的角度看线程安全问题.mp4 synchronized保证线程安全的原理(理论层面).mp4 synchronized保证线程安全的原理(jvm层面).mp4 单例问题线程安全性深入解析.mp4 理解自旋死锁与重入.mp4 深入理解volatile原理与使用.mp4 JDK5提供的原子类的操作以及实现原理.mp4 Lock接口认识与使用.mp4 手动实现一个可重入.mp4 AbstractQueuedSynchronizer(AQS)详解.mp4 使用AQS重写自己的.mp4 重入原理与演示.mp4 读写认识与原理.mp4 细读ReentrantReadWriteLock源码.mp4 ReentrantReadWriteLock降级详解.mp4 线程安全问题简单总结.mp4 线程之间的通信之wait notify.mp4 通过生产者消费者模型理解等待唤醒机制.mp4 Condition的使用及原理解析.mp4 使用Condition重写waitnotify案例并实现一个有界队列.mp4 深入解析Condition源码.mp4 实战:简易数据连接池.mp4 线程之间通信之join应用与实现原理剖析.mp4 ThreadLocal 使用及实现原理.mp4 并发工具类CountDownLatch详解.mp4 并发工具类CyclicBarrier 详解.mp4 并发工具类Semaphore详解.mp4 并发工具类Exchanger详解.mp4 CountDownLatch,CyclicBarrier,Semaphore源码解析.mp4 提前完成任务之FutureTask使用.mp4 Future设计模式实现(实现类似于JDK提供的Future).mp4 Future源码解读.mp4 ForkJoin框架详解.mp4 同步容器与并发容器.mp4 并发容器CopyOnWriteArrayList原理与使用.mp4 并发容器ConcurrentLinkedQueue原理与使用.mp4 Java中的阻塞队列原理与使用.mp4 实战:简单实现消息队列.mp4 并发容器ConcurrentHashMap原理与使用.mp4 线程池的原理与使用.mp4 Executor框架详解.mp4 实战:简易web服务器(一).mp4 实战:简易web服务器(二).mp4 JDK8的新增原子操作类LongAddr原理与使用.mp4 JDK8新增StampedLock详解.mp4 重排序问题.mp4 happens-before简单概述.mp4 的内存语义.mp4 volatile内存语义.mp4 final域的内存语义.mp4 实战:问题定位.mp4

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值