关于java:Java-高并发

32次阅读

共计 4981 个字符,预计需要花费 13 分钟才能阅读完成。

上下文切换

即便是单核处理器也反对多线程执行代码,CPU 通过给每个线程调配 CPU 工夫片来实现这个机制。工夫片 是 CPU 调配给各个线程的工夫,因为工夫片十分短,所以 CPU 通过不停的切换线程来执行,让咱们感觉多个线程是同时执行的,工夫片个别是几十毫秒。

CPU 通过工夫片调配算法来循环执行工作,当前任务执行一个工夫片后会切换到下一个工作。然而,在切换前会保留上一个工作的状态,以便下次切换回这个工作时,能够再加载这个工作的状态。所以工作从 保留到加载 的过程就是一次上下文切换。

线程的状态

第一是 创立状态 。在生成线程对象,并没有调用该对象的 start 办法,这是线程处于创立状态。
第二是 就绪状态 。当调用了线程对象的 start 办法之后,该线程就进入了就绪状态,然而此时线程调度程序还没有把该线程设置为以后线程,此时处于就绪状态。在线程运行之后,从期待或者睡眠中回来之后,也会处于就绪状态。
第三是 运行状态 。线程调度程序将处于就绪状态的线程设置为以后线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。
第四是 阻塞状态 。线程正在运行的时候,被暂停,通常是为了期待某个工夫的产生(比如说某项资源就绪) 之后再持续运行。sleep,suspend,wait 等办法都能够导致线程阻塞。
第五是 死亡状态。如果一个线程的 run 办法执行完结或者调用 stop 办法后,该线程就会死亡。对于曾经死亡的线程,无奈再应用 start 办法令其进入就绪。

多线程的几种实现形式

实现 Runnable 接口

public class MyRunnable implements Runnable {
    @Override
    public void run() {// ...}
}

Runnable 实例作为 Thread 的结构参数创立一个 Thread 实例,而后调用 Thread 实例的 start() 办法来启动线程。

public static void main(String[] args) {MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();}

Thread 类局部源码:
Thread 继承了 Runnable 接口。传入的 Runnable 参数最初赋值给了 Thread 类的属性 target。

public class Thread implements Runnable {
    private Runnable target; // 以后线程将要执行的动作
    # 构造方法
    private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
    ...
     this.target = target; // 赋给了 Thread 的属性 target
    }
}

Thread.start()办法启动线程:start()实际上是通过本地办法 start0()启动一个新线程,新线程会调用 run()办法。

public synchronized void start() {  
        // 如果线程不是 "就绪状态",则抛出异样!if (threadStatus != 0)  
            throw new IllegalThreadStateException();  
        // 将线程增加到 ThreadGroup 中  
        group.add(this);  
        boolean started = false;  
        try {// 通过 start0()启动线程, 新线程会调用 run()办法  
            start0();  
            // 设置 started 标记 =true  
            started = true;  
        } finally {  
            try {if (!started) {group.threadStartFailed(this);  
                }  
            } catch (Throwable ignore) {}}  
    }  

Thread 类重写的 run() 办法:

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

继承 Thread 类

同样也须要实现 run() 办法,因为 Thread 类也实现了 Runable 接口。
当调用 start() 办法启动一个线程时,虚构机会将该线程放入就绪队列中期待被调度,当一个线程被调度时会执行该线程的 run() 办法。

public class MyThread extends Thread {public void run() {// ...}
}
public static void main(String[] args) {MyThread mt = new MyThread();
    mt.start();}

实现 Callable 接口

Callable 接口通过 FutureTask 包装器来创立 Thread 线程。

  1. Callable 接口介绍:

(1)java.util.concurrent.Callable 是一个泛型接口,只有一个 call()办法

(2)call()办法抛出异样 Exception 异样,且返回一个指定的泛型类对象

  1. 应用 Callable 接口实现多线程的步骤

(1)第一步:创立 Callable 子类的实例化对象

(2)第二步:创立 FutureTask 对象,并将 Callable 对象传入 FutureTask 的构造方法中

(留神:FutureTask 实现了 Runnable 接口和 Future 接口)

 (3)第三步:实例化 Thread 对象,并在构造方法中传入 FurureTask 对象

 (4)第四步:启动线程

public class MyCallable implements Callable<Integer> {public Integer call() {return 123;}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}

实现 Runnable 接口 VS 继承 Thread

实现接口会更好一些,因为:

  • Java 不反对多重继承,因而继承了 Thread 类就无奈继承其它类,然而能够实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

volatile

volatile 是轻量级的 synchronized,它在多处理器并发中保障了共享变量的“可见性”。可见性的意思是但一个线程批改一个共享变量时,另外一个线程能读到这个批改的值。volatile 比 synchronized 的应用和执行成本低,因为它不会引起线程上下文的切换和调度。

如果对申明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀命令。
volatile 的 Lock 前缀指令:

  • 将以后处理器缓存行的数据写回到零碎内存。
  • 这个写会内存的操作会使再其余 CPU 里缓存了该内存地址的数据有效。

每个处理器通过嗅探再总线上流传的数据来查看本人缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被批改,就会将以后处理器的缓存行设置成有效状态,当处理器对这个数据进行批改操作的时候,会从新从零碎内存中把数据督导处理器缓存里。

为了进步处理速度,处理器不晓得和内存进行通信,而是先将零碎内存的数据读到外部缓存,

ThreadLocal

多线程拜访同一个共享变量的时候容易呈现并发问题,特地是多个线程对一个变量进行写入的时候,为了保障线程平安,个别使用者在访问共享变量的时候须要进行额定的同步措施能力保障线程安全性。ThreadLocal 是除了加锁这种同步形式之外的一种躲避多线程拜访呈现线程不平安的办法,当咱们在创立一个变量后,如果每个线程对其进行拜访的时候拜访的都是线程本人的变量这样就不会存在线程不平安问题。

ThreadLocal 是 JDK 包提供的,它提供线程本地变量,如果创立一个 ThreadLocal 变量,那么拜访这个变量的每个线程都会有这个变量的一个正本,在理论多线程操作的时候,操作的是本人本地内存中的变量,从而躲避了线程平安问题. 变量是同一个,然而每个线程都应用同一个初始值,也就是应用同一个变量的一个新的正本。

CAS (Compare And Swap)

CAS 是一种乐观锁,机制当中应用了 3 个根本操作数:内存地址 V,旧的预期值 A,要批改的新值 B。更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 当中的理论值雷同时,才会将内存地址 V 对应的值批改为 B。

锁优化

Java 对象头

自旋锁

自旋锁的思维是让一个线程在申请一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能取得锁,就能够防止进入阻塞状态。
自旋锁尽管能防止进入阻塞状态从而缩小开销,然而它须要进行忙循环操作占用 CPU 工夫,它只实用于共享数据的锁定状态很短的场景。

偏差锁

轻量级锁每次申请、开释锁都至多须要一次 CAS,但偏差锁只有初始化时须要一次 CAS。
当一个线程拜访同步代码块并获取锁时,会在对象头和帧栈中的锁记录里存储锁偏差的线程 ID,当前该线程再进入和退出同步块时不须要进行 CAS 操作来加锁和解锁,只须要简略地测试一下对象头的 Mark Word 里的线程 ID 是否是以后线程。

  • 如果测试胜利,示意线程曾经取得了锁。
  • 如果测试失败,则须要再测一下 Mark Word 中偏差锁的示意是否设置成 1(示意以后是偏差锁):如果没有设置,则应用 CAS 竞争锁;如果设置了,则应用 CAS 将对象头的偏差锁执行以后线程。

轻量级锁

线程在执行同步块之前,JVM 会先在以后线程的栈帧中创立用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,官网称为 Displaced Mark Word。而后线程尝试应用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。

  1. 如果胜利,以后线程取得锁,如果失败,示意其余线程竞争锁,以后线程便尝试应用 自旋锁 来获取锁。
  2. 如果仍未获取到锁,则降级为 重量级锁

轻量级锁解锁
应用 CAS 操作将 Displaced Mark Word 替换回到对象头,如果胜利,则示意没有竞争产生。如果失败,示意以后锁存在竞争,锁就会收缩成重量级锁。

线程池

劣势
(1)升高系统资源耗费,通过重用已存在的线程,升高线程 创立和销毁 造成的耗费;
(2)进步零碎响应速度,当有工作达到时,通过 复用 已存在的线程,无需期待新线程的创立便能立刻执行;
(3)不便线程并发数的管控。因为线程若是无限度的创立,可能会导致内存占用过多而产生 OOM,并且会造成 cpu 适度切换(cpu 切换线程是有工夫老本的(须要放弃以后执行线程的现场,并复原要执行线程的现场))。
(4)提供更弱小的性能,延时定时线程池。

实现原理

1、判断外围线程池是否已满,没满则创立一个新的工作线程来执行工作。已满则。
2、判断工作队列是否已满,没满则将新提交的工作增加在工作队列,已满则。
3、判断整个线程池是否已满,没满则创立一个新的工作线程来执行工作,已满则执行饱和策略。

应用参数

  1. corePoolSize(线程池根本大小)
  2. maximumPoolSize(线程池最大大小)
  3. keepAliveTime(线程存活放弃工夫)
  4. workQueue(工作队列):用于传输和保留期待执行工作的阻塞队列。
  5. threadFactory(线程工厂):用于创立新线程。threadFactory 创立的线程也是采纳 new Thread()形式,threadFactory 创立的线程名都具备对立的格调:pool-m-thread-n(m 为线程池的编号,n 为线程池内的线程编号)。
  6. handler(线程饱和策略):当线程池和队列都满了,再退出线程会执行此策略。~~~~

参考资料

  1. https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md#%E4%B8%80%E4%BD%BF%E7%94%A8%E7%BA%BF%E7%A8%8B

2.https://www.jianshu.com/p/36e…

正文完
 0