多线程学习-第一天

多线程学习-第一天

1 多线程的实现方式

1.1 继承Thread类

通过继承Thread类并重写run方法实现:

public class MyThread1 extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread() + "run...");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.start();
    }
}

1.2 实现Runnable接口

通过继承实现虽然简单但是由于java是单继承的,继承了Thread类就不能继承其他类了,所以一般不使用继承Thread去实现多线程,所以一般使用实现接口的方式,然后通过new Thread(Runnable)创建线程

public class MyRunnableThread implements Runnable{
    @Override
    public void run() {
        System.out.println("Runnable接口实现线程创建...");
    }

    public static void main(String[] args) {
        MyRunnableThread runnableThread = new MyRunnableThread();
        Thread t2 = new Thread(runnableThread);
        t2.start();
    }
}

也可以通过匿名内部类结合lambda表达式实现:

public class MyRunnableThread{
    
    public static void main(String[] args) {
        Thread t2 = new Thread(() -> {
            System.out.println("Runnable创建线程...");
        });
        t2.start();
    }
}

以上方式使用最多

1.3 实现Callable接口

上面两种方式都无法获得线程的执行结果,所以可以通过实现Callable接口结合Future实现返回值的获取:

public class MyCallableThread implements Callable {
    @Override
    public Integer call() throws Exception {
        System.out.println("Callable创建线程...");
        return 0;
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyCallableThread callableThread = new MyCallableThread();
        FutureTask futureTask = new FutureTask<>(callableThread);
        Thread t3 = new Thread(futureTask);
        t3.start();
        t3.join();
        System.out.println(futureTask.get());
    }
}

通过lambda表达式创建

public class MyCallableThread{

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        FutureTask futureTask = new FutureTask<>(() -> {
            System.out.println("Callable接口创建线程...");
            return 0;
        });
        Thread t3 = new Thread(futureTask);
        t3.start();
        t3.join();//等线程t3执行完再执行主线程,保证能get到值
        System.out.println(futureTask.get());
    }
}

这里使用了FutureTask类我们看看其继承结构

多线程学习-第一天_第1张图片
FutureTask继承结构图.png

通过图看出FutureTask类可以将Callable封装,而FutureTask类又是一个Runnable,所以可以通过FutureTask来创建线程,下面通过源码分析FutureTask是如何获得Callable的返回结果的

public FutureTask(Callable callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

//run方法
public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        Callable c = callable;
        //Callable不为空并且状态是NEW
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();//调用call方法,并将结果封装到result
                ran = true;//记录执行是否成功
            } catch (Throwable ex) {
                result = null;//执行失败返回空
                ran = false;
                setException(ex);
            }
            //执行成功设置result的值
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

//set方法
//通过CAS将设置状态为COMPLETING,如果成功将结果封装到outcome变量中,然后再将state设置为NORMAL
//state状态
//private static final int NEW          = 0;新建
//private static final int COMPLETING   = 1;完成
//private static final int NORMAL       = 2;正常结束
//private static final int EXCEPTIONAL  = 3;异常结束
//private static final int CANCELLED    = 4;取消
//private static final int INTERRUPTING = 5;中断
//private static final int INTERRUPTED  = 6;响应中断
protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

以上分析了如何获取返回值,下面看看如何get

public V get() throws InterruptedException, ExecutionException {
    int s = state;//获取状态
    if (s <= COMPLETING)//如果没完成,等待完成
        s = awaitDone(false, 0L);
    return report(s);
}

//reports方法
private V report(int s) throws ExecutionException {
    Object x = outcome;//获取返回值
    if (s == NORMAL)//正常结束直接返回
        return (V)x;
    if (s >= CANCELLED)//CANCELLED、INTERRUPTING、INTERRUPTED抛出CancellationException异常
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

1.4 线程池创建

如果使用new线程的方式创建线程,每执行一个任务就创建一个线程,这样会十分消费资源,所以一般使用线程池来创建线程

public class MyThreadExecutor {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.execute(() -> {
            System.out.println("执行线程任务...");
        });
    }
}

上述是使用Executors框架来创建线程池,创建一个固定数量的线程池,除此之外该工具类还提供很多线程池

newFixedThreadPool(int nThreads) //固定数量线程的线程池
newWorkStealingPool(int parallelism)//通过并发量创建线程池  jdk1.8才有
newSingleThreadExecutor()//单线程化线程池
newCachedThreadPool()//创建一个可缓存的线程池,线程可以重复使用,如果没有就创建一个
newScheduledThreadPool(int corePoolSize)//创建一个定时及周期性执行任务的线程池

下面看看以上线程池的实现

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue());
}

public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool
        (Runtime.getRuntime().availableProcessors(),
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue()));
}


public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue());
}

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

这里的线程池的参数下面再说

2 线程池的七大参数

2.1 corePoolSize

核心线程数,以银行服务窗口为例,corePoolSize就是服务窗口的数量

2.2 maximumPoolSize

最大线程数,随着银行中排队的人越来越多,corePoolSize数量的窗口已经无法处理,这时需要另外开maximumPoolSize - corePoolSize 个窗口来应急

2.3 keepAliveTime

核心线程存活时间

2.4 unit

核心线程存活时间单位

2.5 workQueue

阻塞队列,相当于大于corePoolSize时,后面来的线程在队列里排队

2.6 threadFactory

线程工厂,创建线程的工厂默认使用DefaultThreadFactory

2.7 handler

拒绝策略,当队列满了,maximumPoolSize也满了,之后的线程采用拒绝策略进行处理。

线程池的执行过程:

(1) corePoolSize数量的线程直接执行

(2)其他的线程进入阻塞队列中排队等待,当阻塞队列满之后,线程数量如果大于maximumPoolSize则直接执行,如果小于maximumPoolSize将会执行拒绝策略

3 线程的状态

线程一共有六种状态分别是:NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED

3.1 NEW

线程初始状态,线程刚刚创建还未调用start方法

3.2 RUNNABLE

运行状态,其中包含就绪和运行,当调用start之后线程进入就绪状态,这时候如果线程获得了CPU资源,,将转为运行状态

3.3 BLOCKED

阻塞状态,线程进入等待状态,放弃CPU资源给其他线程,阻塞分为以下几种情况:

  1. 等待阻塞

    线程运行wait方法,此时放弃CPU资源进入阻塞,JVM将当前线程放入等待队列等待其他线程唤醒

  2. 同步阻塞

    运行的线程获取锁失败时(存在竞争),JVM会将该线程放入锁池中

  3. 其他阻塞

    Thread.sleep()、join方法、发出I/O请求时,JVM会将当前线程设置为阻塞状态,相应方法执行结束线程恢复(不放弃锁)

3.4 WAITING

其他阻塞,等待结束之后继续执行(不释放对象锁)

3.5 TIME_WAITING

超时等待,超时之后自动返回

3.6 TERMINATED

终止状态,线程执行完毕

多线程学习-第一天_第2张图片
线程状态转换图.png

4 synchronized锁

在jdk1.6之前synchronized是一个重量级锁,而在jdk1.6时底层对synchronized进行了优化

4.1 synchronized用法

synchronized修饰实例方法,要想进入该方法必须先获得该方法的对象锁

synchronized修饰静态方法,要想进入该方法必须先获得该方法所属的类锁

synchronized修饰同步代码块,给指定代码块加锁

4.2 synchronized锁的实现

锁是在对象头中存储的,首先分析对象头信息

在Hotspot虚拟机中,对象在内存中的存储布局分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
Mark Word记录了对象和锁有关的信息,Mark Word在32为虚拟机中存储的是32位,在64位虚拟机中存储64位,下面研究32位虚拟机中Mark Word的五种情况

多线程学习-第一天_第3张图片

我们可以观察到这里hashCode只有在无锁的情况下存储在对象头中,也就意味着一旦对象计算了hashCode,对象在也不可能成为偏向锁 ,轻量级锁和重量级锁是可以实现的,这个下面再说。

4.3 synchronized锁的升级过程

在JDK1.6之后,系统底层对synchronized关键字做出了优化,引入了偏向锁和轻量级锁的概念,这一引入极大的提高了synchronized锁的性能。所以现在的synchronized锁有四种状态:无锁、偏向锁、轻量级锁、重量级锁。

偏向锁

从上面的表格可以看出,偏向锁记录了线程ID,如果该线程再次进入该同步代码块时不需要去获得锁,意味着该锁偏向于该线程。

偏向锁的获取和撤销

1 首先获取锁对象的Mark Word判断是否处于可偏向状态(可偏向标志位为1,线程ID为空)

2 若该对象锁是可偏向状态,则使用CAS将当前线程ID写入锁对象的Mark Word中

2.1 写入成功,表示该线程获得了偏向锁

2.2 写入失败,说明偏向锁已经被其它线程获取,存在竞争,此时需要将偏向锁撤回,将其升级为轻量级锁(这里必须在没有其他线程执行的时候升级)

3 若该对象锁已经是偏向状态(其他线程已经获得了偏向锁)

3.1 若该线程ID与Mark Word中的线程ID相等,该线程可直接进入

3.2 若该线程ID与Mark Word中的线程ID不等,偏向锁撤销,升级成轻量级锁

偏向锁的撤销逻辑

偏向锁的撤销并不是将偏向锁降级成无锁,锁是不存在降级的,偏向锁的撤销直接将偏向锁升级成轻量级锁,这里撤销有两种情况:

1 如果获得偏向锁的线程同步代码块执行完毕,这时候将线程ID置为空,可偏向状态置为0,接着唤醒持有偏向锁的线程,接着执行获取偏向锁逻辑

2 如果获得偏向锁的线程同步代码块未执行完毕,这时候升级为轻量级锁

ps:实际应用中一定存在两个或者两个以上的线程,偏向锁的撤销会增加资源消耗,所以一般关闭偏向锁使用JVM参数UseBiasedLocking设置关闭偏向锁

多线程学习-第一天_第4张图片
偏向锁.png
轻量级锁

当锁存在竞争时,会升级成轻量级锁,此时锁标志位置为00,会采用自旋的方式尝试获得锁

轻量级锁的加锁

当升级为轻量级锁时,Mark Word也会相应变化,其中过程如下:

1 线程在自己的栈帧中创建LockRecord

2 将Mark Word中的信息(hashCode、分代年龄等)复制到LockRecord中

3 将栈中LockRecord的Owner指针指向锁对象

4 将对象头的Mark Word替换成指向LockRecord的指针

多线程学习-第一天_第5张图片
轻量级锁过程图-1.jpg
多线程学习-第一天_第6张图片
轻量级锁过程图-2.jpg

轻量级锁的解锁

轻量级锁的解锁过程和加锁是逆向的,首先使用CAS尝试将LockRecord中的Mark Word信息写回原对象头中,如果成功表示没有竞争,否则失败升级为重量级锁

自旋锁

众所周知,线程从阻塞切换成运行状态需要消耗极大的CPU资源,所以当存在竞争时,不让线程直接阻塞,而是让线程不段尝试获取锁,一旦获取到锁,就避免了线程进入阻塞,这就是自旋锁,就是写一个死循环,一旦CAS获取到锁之后,跳出循环,但是这样会让CPU一直坐着无意义的事情,JDK1.6之前默认自旋次数是10次,如果自旋10次还没有获取到锁,将直接阻塞。JDK1.6做出了优化,提出了自适应自旋锁,可以通过之前锁的状态来自适应自旋次数。也可以使用JVM参数preBlockSpin来修改自旋次数。

重量级锁

重量级锁主要是通过monitor来实现的,在加入同步代码块后,字节码会在同步代码块前后分别加monitorenter和monitorexit,每一个java对象都有一个monitor监视器与其相关联,当一个线程想要执行一段被synchronized修饰的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。monitorenter表示去获得一个对象监视器。monitorexit表示释放monitor监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器。

monitor依赖操作系统的MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

重量级锁的加锁过程

线程访问synchronized修饰的对象,首先先去获得该对象的监视器monitor,如果获取成功,线程进入synchronized修饰的同步代码块执行,如果获取失败,直接阻塞,并放入同步队列中。

当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。其流程图如下:

多线程学习-第一天_第7张图片
重量级锁加锁流程.png

当只有一个线程A进入同步代码块,此时会将锁对象的Mark Word可偏向标志记为1,将线程ID置为空,然后通过CAS将线程A的线程ID记录到Mark Word中,此时如果线程B进入,发现锁是可偏向状态,会用CAS尝试获取,但是由于锁是线程A占有着,所以线程B只能通过自旋的方式尝试获得锁,此时锁取消偏向状态,升级为轻量级锁,一旦自旋还是获取失败,会升级为重量级锁。

你可能感兴趣的:(多线程学习-第一天)