“多线程”重点概念整理

2018-12-09  本文已影响0人  落雨松

一、volatile关键字内存可见性

当程序运行时,JVM会为每一个执行任务的线程分配一个独立的缓存空间,用于提高效率。这个线程会先从主内存中拿到变量到自己的缓存中,然后将改变后的值提交到主内存,这是两个操作,如果在第一个操作后,第二个线程闯入,这个时候第二个线程从主内存中读取的变量就是未改变之前的变量,那么两个线程最后拿到的值便是不一样的,产生冲突。

产生此种问题是因为这个变量并不是都在主内存中操作的,要解决这个问题,便是在“共享变量” 定义的时候在之前添加一个 volatile 关键字。

图示:


捕获.PNG

二、原子变量-CAS算法

volatile关键字保证内存可见性,也就是说可以保证变量都在主内存中进行,但是不能保证原子性。

(一)那么什么是原子性问题?

举例:i++操作
i++操作实际上是“读-改-写”三个操作:

int temp = i;
i=i+1;
temp = i;

当我们运行程序:

int i = 10;
i=i++;  
System.out.println(i); //这个时候输出打印的应该是 10 

原因在于:此时 i++操作返回的是底层“读”的时候的 i ,而不是“写” 完后的 i
: 这就是原子性问题

(二)原子变量(解决原子性问题)

为了解决这个问题,jdk 1.5之后,在java.util.concurrent.atomic包下提供了常用的数据类型的原子变量(最近更新:不过atomic...类也有它的局限性,比如AtomicLong的实现方式是内部有个value 变量,当多线程并发自增,自减时,均通过CAS 指令从机器指令级别操作保证并发的原子性。唯一会制约AtomicLong高效的原因是高并发,高并发意味着CAS的失败几率更高, 重试次数更多,越多线程重试,CAS失败几率又越高,变成恶性循环,AtomicLong效率降低,Java8对此有了新的解决方案:LongAdder)。

它的底层实现是:
1、首先用volatile保证内存的可见性
2、然后用CAS算法保证数据原子性,CAS算法是硬件对于并发操作共享数据的支持,CAS包括三个操作:
①内存值(从主存中读取值)、
②预估值(读取旧值)、
③更新值(如果满足条件就替换主内存中的值)
只有当内存值==预估值的时候,内存值才能够等于更新值,否则将不作任何操作。
demo:

/**
 * @Author : WJ
 * @Date : 2018/11/18/018 12:26
 * <p>
 * 注释:
 */
public class Test2 {

    public static void main(String [] a) throws InterruptedException {
        AtomicDemo atomicDemo = new AtomicDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(atomicDemo).start();
        }
    }
}
class AtomicDemo implements Runnable {

    //用volatile保证内存可见性:无法保证原子性
    //private volatile int number = 0;

    //使用原子变量解决原子性问题
    private AtomicInteger number = new AtomicInteger();

    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getAdd());
    }

    public int getAdd(){
        //使用原子变量提供的相关API进行对原子变量数据的操作,API文档里面有详细介绍
        //这里使用“从主内存中获取和自增”一起的方法返回number自增的值。
        return number.getAndIncrement();
        //return number++;
    }
}

三、ConcurrentHashMap锁分段机制

Java 5.0 在 java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能。
ConcurrentHashMap 同步容器类是Java 5 增加的一个线程安全的哈希表。对与多线程的操作,介于 HashMap 与 Hashtable 之间。内部采用“锁分段”机制替代 Hashtable 的独占锁。进而提高性能。
此包还提供了设计用于多线程上下文中的 Collection 实现:
ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。

JDK1.8以后ConcurrentHashMap由锁的分段机制变为CAS。
CopyOnWriteArrayList "写入并复制" 是个复合操作,当每次写入时,都会复制。添加操作比较多时效率较低。并发迭代操作多时,可以提高效率。

这里推荐一篇博客详细介绍了ConcurrentHashMap:
https://blog.csdn.net/yansong_8686/article/details/50664351

四、CountDownLacth 闭锁

闭锁是一个同步工具类,它是用来保证一组线程全部执行完成才能进行下一步操作的工具。就比如一组多线程,我们想要获取他们全部执行的时间,寻常操作时无法完成的,因为获取时间存在于主线程,随时可能拿到cpu的使用权利,所以这个工具类,就可以实现要全部线程执行完,才执行下一步。

闭锁状态包含一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示已经有一个事件已经发生了。而await方法等待计数器达到0,这表示所有需要等待的事件都已经发生。如果计数器的值非0,那么await会一直阻塞直到计数器为0,或者等待中的线程中断或者超时。

当然采用join方式也可以完成,但是效率是远远不及的。

demo:

/**
 * @Author : WJ
 * @Date : 2018/11/18/018 12:26
 * <p>
 * 注释:
 */
public class Test2 {

    public static void main(String [] a) throws InterruptedException {
        //闭锁工具类,设需要等待事件数完成的数量为 5
        final CountDownLatch countDownLatch = new CountDownLatch(5);
        DownLatchDemo downLatchDemo = new DownLatchDemo(countDownLatch);

        //开始时间
        long start = System.currentTimeMillis();

        for (int i = 0; i < 5; i++) {
            new Thread(downLatchDemo).start();
        }
        //等待上面的5个线程执行完成,也就是事件数为 0 后才执行await之后main线程的代码
        countDownLatch.await();

        //结束时间
        long end = System.currentTimeMillis();
        System.out.println("执行时间为:"+(end - start));


    }
}
class DownLatchDemo implements Runnable {
    private CountDownLatch latch;
    DownLatchDemo(CountDownLatch latch){
        this.latch = latch;
    }
    public void run() {
        synchronized (this){
            try{
                //执行一个耗时的操作
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName());
                }
            }finally {
                //每一次线程的调用,使闭锁操作事件数减 1
                latch.countDown();
            }
        }
    }
}

五、创建实现线程的方式

创建实现线程的方式有四种:
1、继承Thread接口
2、实现Runable接口
3、实现Callable接口
4、线程池

对于Callable接口:

1、需要实现Callable接口,这里可以实现泛型接口Callable<?>
2、重写call方法,call方法可以返回泛型接口里面的数据类型数据
3、然后在启动这个线程的时候需要FutureTask类的支持,当然这个类就是获取线程返回值的类

demo:

/**
 * @Author : WJ
 * @Date : 2018/11/18/018 12:26
 * <p>
 * 注释:
 */
public class Test2 {

    public static void main(String [] a) throws InterruptedException, ExecutionException {
        CallableDemo callableDemo = new CallableDemo();

        //要启动实现Callable 接口的线程类 需要 FutureTask 类的支持,用于接收运算结果
        FutureTask futureTask1 = new FutureTask(callableDemo);

        //启动线程
        new Thread(futureTask1).start();

        //获取线程返回值
        System.out.println(Thread.currentThread().getName()+"得到返回值:"+futureTask1.get());
    }
}
//实现Callable泛型接口,也可不实现泛型,call返回的将是一个Object类型的数据
class CallableDemo implements Callable<Integer> {

    private volatile int number;

    //重写call方法
    public synchronized Integer call() throws Exception {
        number = number +1;
        System.out.println(Thread.currentThread().getName()+"为:"+number);
        return number;
    }
}

六、java中实现同步的两种方式: syschronized 和 lock

syschronized 实现同步的方式分为:同步方法 和 同步代码块
lock 是一个接口 ,通过:

private Lock lock = new ReentrantLock();
//然后lock调用:  
lock.lock();
//....同步代码 
lock.unlock();  
//获得锁和释放锁

还可以:

private Condition condition = lock.newCondition();

调用:

condition.await();
condition.signal(); 
condition.signalAll();

等待和唤醒单个或所有线程,与wait 和notify、notifyAll不同的是:可以实现多路分用,也就是说将多个线程拆分等待,可以唤醒某一个确定同步线程。

但是syschronized 可以自动的获取和释放锁,而lock则需要显示的获取和释放,释放锁lock.unlock(); 必须放在try ... finally 的finally里面执行。

当线程竞争较激烈的话,Lock 性能优于 syschronized 。两者取决于业务的需求。

七、读写锁ReadWriteLock

保证 :读读、读写不是互斥的,写写是互斥(事件不能同时发生)的。

/**
 * @Author : WJ
 * @Date : 2018/11/18/018 12:26
 * <p>
 * 注释:
 */
public class Test2 {

    public static void main(String [] a) throws InterruptedException, ExecutionException {
        final DemoClass demoClass = new DemoClass();
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                public void run() {
                    demoClass.get();
                }
            }).start();
        }

        new Thread(new Runnable() {
            public void run() {
                double number = Math.random()*100;
                demoClass.set((int)number);
            }
        }).start();
    }
}

/**
 * 读写锁实例
 */
class DemoClass  {

    private int number;
    //创建读写锁实例
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    //读
    public void get() {
        lock.readLock().lock();
        try{
            System.out.println("读--操作:number = "+ number);
        }finally {
            lock.readLock().unlock();
        }
    }
    //写
    public void set(int number){
        lock.writeLock().lock();
        try{
            this.number = number;
            System.out.println("写++操作:number = "+ number);
        }finally {
            lock.writeLock().unlock();
        }
    }
}

八、线程池

1、为什么要用线程池?
当我们想要多次启动同一线程时,每一次启动都有创建和销毁操作,这样对于高并发的情况是不利的,所以就有了线程池的概念。

2、什么是线程?
线程池底层是实现一个对列,这个对列里面存放着多个线程,这样就不要每次创建都要销毁,影响效率。

3、线程池核心接口:Executor (位于Java.util.concurrent包下)

4、线程池体系结构
java.util.concurrent.Excutor :负责线程的使用与调度的接口
|------ExecutorService 子接口:线程池主要接口
|-------------ThreadPoolExecutor :线程池的实现类
|-------------ScheduledExecutorService:子接口,负责线程的调度
|--------------------ScheduledThreadPoolExecutor:继承ThreadPoolExecutor ,实现 ScheduledExecutorService

5、工具类:Executors
|----ExecutorService newFixedThreadPool(); 创建固定大小的线程池
|----ExecutorService newCachedThreadPool(); 缓存线程池,线程池数量不确定,可以根据需要自动的更改数量
|----ExecutorService newSingleThreadPoolExecutor(); 创建固定大小的线程,可以延迟或定时的执行任务。

6、线程池的使用:

        //实现Runable的线程类
        final DemoClass demoClass = new DemoClass();
        //创建固定大小为5的线程池
        final ExecutorService pool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            //为线程池中的线程分配任务
            pool.submit(new Thread(demoClass));
        }
        //关闭线程池
        pool.shutdown();

九、线程调度(这里摘自百度百科)

计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权。

有两种调度模型:分时调度模型和抢占式调度模型。

分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。

java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。

一个线程会因为以下原因而放弃CPU。

1 java虚拟机让当前线程暂时放弃CPU,转到就绪状态,使其它线程获得运行机会。
2 当前线程因为某些原因而进入阻塞状态
3 线程结束运行

上一篇下一篇

猜你喜欢

热点阅读