美文网首页
JUC学习笔记一

JUC学习笔记一

作者: Martain | 来源:发表于2020-07-08 14:22 被阅读0次

什么是JUC

Java 5.0 (JDK 1.5)提供了 java.util.concurrent( 简称JUC ) 包,在此包中增加了在并发编程中很常用的实用工具类, 用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。

Java 创建多线程的三种方式

在Java项目中参加多线程有四种方式,如果包括从线程池中创建线程的话就包含四种方式了,这里只介绍三种创建线程的方式:继承Thread类实现Runnable接口实现Callable接口

继承Thread

这里主要是继承Thread类,然后重写run方法。

public class UseJavaThread {
    public static void main(String[] args) {
        CustomThread customThread = new CustomThread();
        CustomThread customThread2 = new CustomThread();
        CustomThread customThread3 = new CustomThread();
        customThread.start();
        customThread2.start();
        customThread3.start();
    }
}

class CustomThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName()+" : "+i);
        }
    }
}

实现Runnable接口

覆写Runnable接口实现多线程可以避免单继承局限,当子类实现Runnable接口,此时子类和Thread的代理模式(子类负责真是业务的操作,thread负责资源调度与线程创建辅助真实业务。

public class UseJavaThread {
    public static void main(String[] args) {
        CustomThread2 customThread2 = new CustomThread2();
        new Thread(customThread2,"线程1").start();
        new Thread(customThread2,"线程2").start();
        new Thread(customThread2,"线程3").start();
    }
}

class CustomThread2 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName()+" : "+i);
        }
    }
}

实现Callable接口

覆写Callable接口实现多线程(JDK1.5),核心方法是call()方法,有返回值。需要有一个FutureTask对象包装,可以接收它的返回值。

public class UseJavaThread {
    public static void main(String[] args) { 
        CustomThread3 customThread3 = new CustomThread3();
        FutureTask<String> futureTask = new FutureTask<>(customThread3);
        new Thread(futureTask,"线程1").start(); 
        try { 
            System.out.println(futureTask.get());
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
} 

class CustomThread3 implements Callable<String> {

    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 3; i++) {
            sum = sum+i;
        }
        return "sum :"+sum;
    }
}

一个简单的Demo

public class DemoThreadTest {
    public static void main(String[] args) {
        System.out.println("线程启动====>");
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo).start();
        while (true){
            if (threadDemo.isFlag()){
                System.out.println("thread demo's flag is true");
                break;
            }
        }
    }
}
class ThreadDemo implements Runnable{
    /**
     * 一个标识符
     */
    private boolean flag = false;

    @Override
    public void run() {
        /**
         * 1.5秒后,将flag变量设置成true
         */
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("set flag = true");
        setFlag(true);

    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

上面这段是一段简单的代码,类ThreadDemo实现了Runnable接口,其中有个标识变量flag,线程启动1.5秒之后,会将flag变量的值设置成true。在main方法中,创建了一个线程去执行ThreadDemo的实例,并创建了一个循环去监测ThreadDemo实例中的flag变量,当flag变量为true的时候,会打印输出一段话,并跳出该循环。

我们从阅读代码,我们预测执行结果应该会是:

线程启动====>
# 1.5 秒之后
set flag = true
thread demo's flag is true
# 程序结束

实际执行结果:

线程启动====>
# 1.5秒之后
set flag = true
# 程序阻塞中...

为什么会出现这样的结果呢?明明我们创建了一个实例,一个新线程中1.5秒后修改该实例的flag,但是为什么主线程中没有监测到flag变量的改变呢?转眼给我们要回到内存可见性相关了。

内存可见性

简介

当多个线程操作共享数据时,彼此不可见。内存可见性(Memory Visibility)是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。

执行原理

多线程的使用中,当一个类实例化成功后,它的变量会初始化到主存(堆内存)中,但是当其他线程创建时,jvm会为每个线程分配独立的缓存,为其提高效率,我们称之为线程缓存

执行分析

上面我们的程序执行过程中,实例化了ThreadDemo的实例,flag变量会保存在主存中,此时的flag变量的值为false。当线程创建成功后(我们称为线程一),会将主内存中的flag变量读取到线程缓存中来,此时主存中的flag的值为false,然后主线程中的循环要获取flag变量时,也是从主内存中将flag复制到了主线程的内存中,此时主内存中的flag依然是false,1.5秒后,线程一中将线程一内存的flag变量修改为true,并更新到主存中,但是主线程中是使用while循环一直获取的是自己线程缓存中的flag,所以并不知道flag已经为true了。

要让while循环中每次都获取的都是主内存中的变量,我们可以加锁:

public class DemoThreadTest {
    public static void main(String[] args) {
        System.out.println("线程启动====>");
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo).start();
        while (true){
            synchronized (threadDemo){
                if (threadDemo.isFlag()){
                    System.out.println("thread demo's flag is true");
                    break;
                }
            } 
        }
    } 
}
...

加了锁,就可以让while循环每次都从主存中去读取数据,这样就能读取到true了。但是一加锁,每次只能有一个线程访问,当一个线程持有锁时,其他的就会阻塞,效率就非常低了。

volatile 关键字

简介

Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。当多个线程操作共享数据时,可以保证内存中的数据可见。用这个关键字修饰共享数据,就会及时的把线程缓存中的数据刷新到主存中去,并通知其他线程。可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:

  • 对于多线程,不是一种互斥关系
  • 不能保证变量状态的“原子性操作”

修改实例

package com.martain.study;

/**
 * @author Martin
 * @version 1.0
 * @date 2020/7/6 5:25 下午
 */
public class VolatileKeyWorldTest {

    /**
     * validate 关键字:当多个线程进行操作共享数据时,可以保存内存中的数据可见
     *          相较于synchronized 是一种较为轻量级的同步策略,
     * 注意:
     *  1、volatile不具备"互斥性"
     *  2、不能保证变量的原子性
     *
     * @param args
     */
    public static void main(String[] args) {
        ThreadVolatileDemo threadDemo = new ThreadVolatileDemo();
        new Thread(threadDemo).start();
        while (true){
            if (threadDemo.isFlag()){
                System.out.println("thread demo's flag is true");
                break;
            }
        }
    }
}

class ThreadVolatileDemo implements Runnable{

    private volatile boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("set flag = true");
        setFlag(true);

    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

我们只需要在变量前添加volatile关键字就可以将变量的更新操作通知到其他线程。就可以保证多个线程中所使用的变量都是最新的。

原子性与CAS算法

原子性

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

i++的原子性问题

i++的操作实际分为三个步骤”读-改-写“

// i++ 的底层操作
int temp = i
i = i + 1
i = temp

变量多线程安全性问题

public class AtomicTest {
    public static void main(String[] args) {
        ThreadAtomic threadAtomic = new ThreadAtomic();
        for (int i = 0; i < 10; i++) {
            new Thread(threadAtomic,"thread "+i).start();
        }
    }
}

class ThreadAtomic implements Runnable{
    int serial = 0;
    @Override
    public void run() {
        try {
            Thread.sleep(200);
            System.out.println(getSerial());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /**
     * 每次访问加一
     * @return
     */
    public int getSerial(){
        return serial++;
    }
}

上面这个例子中,每次创建线程执行一次getSerial方法就会获取到serial的值,并让serial自增1,创建了10个线程来启动ThreadAtomic实例,正常来讲,应该会输出0~9 这10个数字,因为线程执行顺序的原因可能顺序不是固定的,但是数字应该不会重复。但是实际的结果是有一定情况有一些数字重复的,因为不具备互斥性,导致多个线程可能有某几个线程同时获取了这个变量,导致他们获取的变量都是一样的,这就是多线程的安全问题。要解决这个问题可以对变量加锁,让同一时刻只能有一个线程对其进行操作,都是锁机制太耗费性能了,会对其他线程进行阻塞,我们可以使用性能更好的CAS算法来解决这个问题,CAS算法性能好是因为她不会阻塞线程

CAS算法

CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。

CAS 是一种无锁的非阻塞算法的实现。CAS 包含了 3 个操作数:

  • 读写的内存值 V 内存值
  • 进行比较的值 A 预估值
  • 拟写入的新值 B 更新值

当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作

原子变量

上面我们讲到了什么是原子性,java.util.concurrent.atomic 包下提供了如下一些原子操作的常用类:

  • AtomicBoolean 、 AtomicInteger 、 AtomicLong 、 AtomicReference
  • AtomicIntegerArray 、 AtomicLongArray
  • AtomicMarkableReference
  • AtomicReferenceArray
  • AtomicStampedReference

类 AtomicBoolean、 AtomicInteger、 AtomicLong 和 AtomicReference 的实例各自提供对
相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。

AtomicIntegerArray、 AtomicLongArray 和 AtomicReferenceArray 类进一步扩展了原子操
作,对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问语义方
面也引人注目,这对于普通数组来说是不受支持的。

这些原子变量的原理是使用了volatile保证了其内存的可见性,使用CAS算法保证了数据的原子性,它们的核心方法: boolean compareAndSet(expectedValue, updateValue)

使用原子变量

public class AtomicTest {
    public static void main(String[] args) {
        ThreadAtomic threadAtomic = new ThreadAtomic();
        for (int i = 0; i < 10; i++) {
            new Thread(threadAtomic,"thread "+i).start();
        }
    }
}

class ThreadAtomic implements Runnable{
    AtomicInteger serial = new AtomicInteger(0);
    @Override
    public void run() {
        try {
            Thread.sleep(200);
            System.out.println(getSerial());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /**
     * 每次访问加一
     * @return
     */
    public int getSerial(){
        return serial.getAndIncrement();
    }
}
1
0
2
3
5
9
4
7
8
6

使用原子变量修改了上面的实例之后,发现结果正常了,解决了基本变量的在多线程中的共享数据的安全问题。

相关文章

  • JUC学习笔记一

    什么是JUC 在 Java 5.0 (JDK 1.5)提供了 java.util.concurrent( 简称JU...

  • ConcurrentSkipListMap 学习笔记

    ConcurrentSkipListMap 学习笔记 标签(空格分隔): juc学习 基于跳跃表的线程安全的map...

  • JUC 学习笔记

    JUC :package java.util.concurrent package java.util.con...

  • JUC学习笔记二

    HashMap HashMap类实现了Map接口,所以实现了Map常用的一些方法,Map通常在java开发中被称为...

  • JUC学习笔记三

    JUC学习笔记三 用于解决多线程同步问题的方式 隐式锁(synchronized) 同步代码块 同步方法 显式锁(...

  • JUC学习笔记-锁

    1.为什么需要锁 解决多个线程访问同一个可变的状态变量时的安全问题。 2.内置锁 - synchronized 任...

  • JUC——检视阅读

    JUC——检视阅读 参考资料 JUC知识图参考 JUC框架学习顺序参考 J.U.C学习总结参考,简洁直观 易百并发...

  • JUC并发编程引导学习(超长篇)

    JUC并发编程学习 1、什么是JUC juc是Java并发编程的一个工具包,包名为java.util.concur...

  • JUC学习笔记(二)—ConcurentHashMap

    Unsafe:是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Un...

  • JUC学习笔记(1)-CAS

    1.什么是CAS CAS是一种无锁执行的算法原理。全称Compare And Swap其算法核心思想如下 2.Un...

网友评论

      本文标题:JUC学习笔记一

      本文链接:https://www.haomeiwen.com/subject/xzcccktx.html