JUC并发编程之Thread&Volatile

252 阅读7分钟

JUC并发编程基础

什么是JUC?

在java中JUC是关于线程的的,特指java.util.concurrent工具包的简称,是原生的并发包和一些常用工具类。

线程概念?

一个进程中可以包含多个线程,一个进程至少有一个线程,比如聊天软件为线程,聊天软件中自动保存为线程,在java中至少有两个线程GC、Main.

线程的并发和并行?

并发:多个线程操作同一个资源,交替执行的过程。

并行:多个线程同时执行,只有在多核CPU下才能完成。

因此我们使用多线程、使用并发编程的目的在于提高效率,让CPU一只工作以达到最大处理性能。

以上我对线程基本知识的认知,现在开始我们JUC之旅,我们在面试过程中一般问到多线程总会问你如何实现,其中就不可避免的说到Thread这个类,那么我们的JUC也从这个类开始!

A <i>thread</i> is a thread of execution in a program. The Java
  Virtual Machine allows an application to have multiple threads of
  execution running concurrently.
 //这是thread中第一行注释,从注释中我们很容易看到java虚拟机允许应用程序有多个线程并发执行。

线程状态:thread源码大约2000多行,我们从头看很容易发蒙,先看第一个线程状态

/* @since   1.5
     * @see #getState
     */
    public enum State {
        /**
         * Thread state for a thread which has not yet started.
         * 线程的一个状态,一个尚未启动的状态
         * 延伸:java能够创建线程?   不能
         * 线程是属于本地资源,java是无法创建的
         *线程只有调用了start()后才算准备就绪可以运行
         */
        NEW,

        /**
         * 运行或等待操作系统进行调度
         */
        RUNNABLE,

        /**
        *线程阻塞状态
        *线程等待监视器锁在synchronized块/方法上等待获取锁,或者调用了wait方法,等待重新获取锁进入同步块
        */
        BLOCKED,

        /**
         * 线程等待
         * 调用object.wait(),Thread.join()和locksupport.park()会进入该状态,注没有设置超时
         * 所以线程是正在等待其他线程进行特定操作来唤醒,比如object.notify来唤醒线程
         *Thread.join()的线程在等待指定线程停止,其内部实现方式也是object.wait
         */
        WAITING,

        /**
        * 线程延时等待
        * 调用thread.sleep(),Object.wait(long),Thread.join(long)
        * LockSupport.parkNanos(long),LockSupport.parkUntil(long)都可以进入该状态,
        * 这个地方设置了一个超时
         */
        TIMED_WAITING,

        /**
         *终止,线程执行完成退出
         */
        TERMINATED;
    }

以上为线程的各种状态,要了解线程我们知道了线程有那么多状态,接下来需要进一步看thread源码。

构造方法

public Thread(Runnable target)  //传入runable接口实现,为线程默认名称生产规则。
public Thread(ThreadGroup group, Runnable target)//通过线程用户组合runable实现
public Thread(Runnable target, String name)//带入一个线程名以及实现了runable接口
public Thread(ThreadGroup group, Runnable target, String name)//在上面用户组基础上添加了线程名
public Thread(ThreadGroup group, Runnable target, String name, long stackSize)//增加了线程栈大小

线程实现,是通过thread中的init方法来进行实现的

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals){
    //我们在thread源码中的所有的构造方法均调用了这个方法。
    //给线程进行一个命名,如果为null就会抛出空指针异常,而我们所有的构造方法中对线程进行了一个默认命名
    //"Thread-" + nextThreadNum()
    if (name == null) {
            throw new NullPointerException("name cannot be null");
      }
    //创建线程的子线程
    Thread parent = currentThread();
    //创建线程组,如果在构造方法中没有传入线程组,则通过下面的流程来决定其线程组
     SecurityManager security = System.getSecurityManager();
    if (g == null) {
        
        if (security != null) {
            g = security.getThreadGroup();
        }
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    //java编程思想中将threadgroup当做是java一次不成功的尝试,不需要理会。
    this.group = g;
    //子线程默认拥有父线程的优先级、访问权限和daemon属性。
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    //指定线程栈大小,如果没有指定会在JVM中进行声明,XSS,一般我们不会用到
    this.stackSize = stackSize;
}

线程启动

线程启动是调用run()或者start()方法,一般会要求大家知道区别,这一句话概括下:start方法会创建一个新的线程,然后执行run方法,但是我们直接调用run方法是在当前调用线程本身执行,不会创建新线程,不会触发多线程。

//启动线程,java虚拟机会调用当前thread的run()
public synchronized void start() {

}
//线程打断,如果当前线程阻塞在Object.wait(),Thread.join(),Thread.sleep()上会被打断
public void interrupt(){
    
}
//调度器让出当前线程执行时间片,可用来进行并发优化。
public static native void yield();

//线程休眠,在指定毫秒,纳秒内不释放任何当前线程所持有的锁,被打断会抛出InterruptedException异常
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException{}
//同步方法,同步当前线程对象
public final synchronized void join(long millis) throws InterruptedException;
public final synchronized void join(long millis, int nanos)
throws InterruptedException{};

在开启下面的内容前先了解下JMM,java内存模型,java中内存分为主内存和工作内存

主内存:共享内存,所有线程均可以访问

工作内存:每个线程会创建一个工作内存。

线程对变量的操作是在线程自己的工作内存中,完成相关操作变量后,会将变量写回主线程,保证变量可见性,变量变化后会通过主内存通知其他线程,也就是说线程之间通信是通过主内存来完成的。

volatile

volatile 是java虚拟机提供的轻量级同步机制,保证了可见性,但是不保证原子性,并且禁止指令重排

volatile不保证原子性

原子性:不可分割性,完整性,即每个线程在业务处理时中间是不允许被分割的,要么成功,要么失败以保证数据的完整性。但是在多线程环境下操作主内存数据会出现丢失写值的。

为了方便给大家介绍volatile不保证原子性,我们做一个启动20个线程然后计算从0+1+2...+100

class MyData1{

    volatile int num =0; //volatile 增强了线程得可见性
    public void addplus(){
        num++;
    }
}
public class VolatileDemo1 {
    public static void main(String[] args) {
        MyData1 myData = new MyData1();
        for (int i=0;i<=20;i++){
            new Thread(()->{
                for (int j=0;j<=100;j++){
                    myData.addplus();
                }
            },"线程"+i).start();
        }
        System.out.println(Thread.currentThread().getName()+"得到数据"+myData.num+"得到最后得结果");
    }
}

这个地方我们是否能拿到我们期望的数值2000呢?

因为volatile不具备原子性,我们每次得到的结果都是不一样的,并不能拿到我们期望的数值。

volatile可见性

当我们在业务类中定义了一个变量如果在子线程中进行更改,主线程中获取数据也会随之改变。

/**
 * volatile 可见性
 */
class MyData{

    volatile int num =0; //volatile 增强了线程得可见性

    public void addTobig(){
        this.num = 60;
    }
}
public class VolatileDemo {
    public static void main(String[] args) {
        MyData data = new MyData();

        //子线程
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"comein");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            data.addTobig();
            System.out.println(Thread.currentThread().getName()+"数据已经更新"+data.num);

        },"a").start();

        //主线程
        while(data.num == 0){
            //只有主线程中得数据num
        }
        System.out.println(Thread.currentThread().getName()+"mission is over"+data.num);
    }

}

我在子线程a中将数值num进行了更改,同时睡了3秒钟,主线程做程序等待,大家可以考虑下最后的数据更新是什么?主线程中的data.num是否是60

volatile禁止指令重排

计算机在执行程序时,为了提高性能,编译器和处理器常常会对运行指令做重排

源代码--->编译器优化的重排--->指令并行的重排--->内存系统的重排--->最终执行的指令

在单线程环境里面确保程序最终执行结果和代码顺序的执行结果是一致。

但是在多线程环境下线程交替执行,由于编译器优化重排的存在,两个线程使用变量能否保证一致性是无法确定的,处理器在进行重排时会考虑指令之间数据依赖性。

public class valitleDemo3{
    int a=0;
    boolean flag = false;
    
    public void method1(){
        a=1;
        flag = true;
    }
    public void method2(){
        if(flag){
            a=a+5;
            System.out.println("return value "+a);
        }
    }
}

在多线程条件下执行上面的两个方法是无法保证a的最终值的。我们可以使用加锁或使用volatile关键字来解决数据指令重排。

那么如何解决上面多线程环境下的问题呢

1:使用synchronized,lock加锁

2:使用juc下面的atomicinteger原子类操作

今天是JUC的第一篇文章主要是和大家看了下thread的源码以及volatile在多线程环境下的应用。

如果您对今天的分享感兴趣请关注公众号领取历史文章及后续更新!