三篇搞定Java高并发笔记【第一篇】

本文Java高并发的内容将从三个阶段记录,参考资料【Java并发编程详解】:

  • 多线程基础
  • Java内存模型(高并发设计模式)
  • Java并发包JUC
  • Java并发包源码AQS

什么是线程

相信学过操作系统的同学都知道线程和进程的关系,对于计算机来说一个任务就是一个进程,一个进程里面至少有一个线程。想必学习的时候会不会问,一个APP就对应一个进程,一个进程难道就是一个JVM吗?那经常写的函数是不是就是一个线程呢?

通常来说,一个APP是一个进程,但是也有可能多个进程。一个进程就是一个JVM(虚拟机),里面有很多个线程运行,接下来就是操作系统的知识了。
每个线程都有自己的局部变量表、程序计数器以及生命周期,线程的生存状态分为以下五个主要的阶段:

  • NEW状态
    当我们用关键字new创建一个Thread对象时,此时并不处于执行状态,因为没有调用start方法。此时只是一个对象的状态,就跟平常创建一个对象一样,当用start方法时就会进入到`RUNNABLE``状态。
  • RUNNABLE状态
    此时才是真正的在JVM中创建了一个线程,线程一经启动就立即执行吗?肯定不是的,因为还有个RUNNING状态,当调用run方法才真正开始执行。线程的执行与否都需要听从CPU的调度,只有CPU调度了才有执行资格。
  • RUNNING状态
    一旦CPU通过轮询从队列中选中了线程,此时才是真正的执行逻辑代码。
  • BLOCKED状态
    线程可能因为某种原因进入阻塞状态,就会进入BLOCKED状态。比如调用了sleep,或者wait方法而加入了waitSet中。比如为获得锁资源,而被进入阻塞队列中等待。
  • TERMINATED状态
    TERMINATED状态是线程的最终状态,在状态中线程不会切换到其他状态,线程进入TERMINATED状态,意味着该线程的整个生命周期都结束了。比如线程正常结束任务,比如JVM crash,所有线程结束。

在面试过程中,我们都会遇到这样的面试题,怎样创建一个线程?
答:
1)继承 Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池例如用Executor框架

其实在JDK中代表线程的就只有Thread这个类,线程执行单元就是run方法。所以创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元则有两种方式,第一种就是重写Thread的run方法,第二种实现runnable接口方法,并将Runnable实例用作构造Thread的参数。

我们接入一个例子来引入上面的实例:假设一个银行的办事大厅,有三个柜台,每个柜台需要为顾客办理业务。顾客依次进门取号,等待办理,那么可以怎样模仿这个实例呢?

public class demo implements Runnable{
    private int index = 1;//当前的顾客
    private int MAX = 50;
    @Override
    public void run() {
        while (index < MAX){
            System.out.println(Thread.currentThread() + "的号码是:"+(index++));
            try{
                Thread.sleep(100);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String arg[]){//多线程测试hashmap 容易出现死循环
        final demo runnable = new demo();
        Thread thread_1 = new Thread(runnable,"线程一");
        Thread thread_2 = new Thread(runnable,"线程一");
        Thread thread_3 = new Thread(runnable,"线程一");
        thread_1.start();
        thread_2.start();
        thread_3.start();
    }
}

只贴部分输出情况,可以看到,某几个线程重复的index情况。

Thread[线程四,5,main]的号码是:17
Thread[线程二,5,main]的号码是:17

为什么会这样呢?

相信看过我的ConcurrentHashMap高并发讲解 的就知道,线程执行的时候会有缓存,还没等线程修改刷新至内存就被其他线程修改了,所以导致重复!那么怎么解决呢,一是定义static共享变量,二是使用volatile关键字强制刷新到内存。

Thread与JVM虚拟机

研究线程那么必然要懂得JVM的内存模型,以及各个内存区域之间的关系。JVM在执行Java程序的时候会把对应的物理内存划分为不同的区域,每一个区域都存放着不同的数据。

JVM内存模型

  1. 程序计数器
    相信这个大家都不陌生,操作系统都需要通过控制总线向CPU发送机器指令,而程序计数器就是当前执行指令地址。每个线程都需要一块独立的程序计数器,因此内存区域是线程私有的。

  2. Java虚拟栈
    线程创建时,都会为其创建一个虚拟机栈,虚拟机栈大小可以用-xss来配置。在线程中,方法执行会创建一个名为栈帧的东西,主要用于存放局部变量表操作栈动态链接等信息,其区域也是私有

  3. 本地方法栈
    Java提供了调用本地方法的接口(JNI Java Native Interface),也就是操作系统程序方法。大家都知道,JVM是运行在操作系统上的,而操作系统是最终的管理。在JVM中经常会使用JNI方法,比如网络通信,文件操作系统的底层,甚至String的intern等都是JNI方法。

  4. 堆内存
    堆内存是JVM中最大的一块内存区域,被所有的线程共享,Java创建的所有对象几乎都存在这里。该内存区域也会垃圾回收器经常照顾的对象,所以有时候被称为“GC堆”

  5. 方法区
    方法区也是被多个线程共享的内存区域,主要用于存储被虚拟机加载的类信息、常量(运行常量池)、静态变量即时编译器(JIT)编译后的代码等数据。

所以可以得出进程内存大小为:堆内存+线程数量*栈内存

那么JVM中可以创建多少个线程呢,我们是可以通过公式算出来的。
线程数量=(最大内存空间-JVM堆内存-ReserverOsMemory)/ThreadStackSize(xss) 当然线程数量还跟操作系统的一些内核配置有很大的关系。

守护线程

你知道JVM在什么情况下会退出吗?
我们说的是正常的退出,而不是调用System.exit()。正常退出就需要理解守护线程,守护线程是一类比较特殊的线程,一般用于处理后台的一些工作,比如JDK的垃圾回收。若线程中没有非守护线程就会,则JVM的进程就会退出。
设置守护线程的方法就是thread.setDaemon(true),true就是代表守护线程,false就是正常线程。主要注意的就是,setDaemon方法只在线程启动之前才能失效,如果一个线程死亡,那么设置setDaemon则会抛出IllegalThreadStateException异常。

  • 线程sleep
    sleep方法会使线程进入指定的毫秒数的休眠,暂停执行。休眠有一个重要的特性就是不会放弃monitor锁的所有权。
  • 使用TimeUntil替代Thread.sleep
    在使用线程休眠方法Thread.sleep,可以完全用TimeUnit来代替,因为sleep能做的事,TimeUnti都能做。比如线程想休眠3小时24分17秒88毫秒
        TimeUnit.HOURS.sleep(3);
        TimeUnit.MINUTES.sleep(24);
        TimeUnit.SECONDS.sleep(17);
        TimeUnit.MILLISECONDS.sleep(88);
  • 线程yield
    yield方法是属于一种启发式的方法,调用yield方法会使当前线程从RUNNING状态切换到RUNNABLE状态

总结
1.sleep会导致当前线程暂停指定的时间,没有CPU的时间消耗。
2.yield只是对CPU调度器的一个提示,如果CPU没有忽略这个提示,他会导致线程上下文切换
3.sleep会使线程短暂block,会在给定的时间内释放CPU的资源
4.sleep会百分百的完成给定的时间消耗,而yield不一定担保
5.一个线程sleep另一个线程调用interrupt会捕获中断信号,而yield不会。

线程安全与数据同步

多线程里最重要的内容之一,那就是数据同步线程安全等内容。在串行化任务执行时,由于不存在资源的共享,线程安全的问题几乎不用担心。但是现在都是追求高效率的执行,都需要满足多线程对共享资源的竞争。
在前面的例子中,讲了多个线程对index变量的竞争引起的,解决竞争问题可以用synchronized关键字,synchronized提供了一种排他机制,也就是在同一时间只能有一个线程执行操作。

什么是synchronized关键字?
synchronized就是同步的意思,可以实现一种简单的策略来防止线程干扰和内存一致性错误,具体表现为如下:

  • synchronized关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致的出现。
  • synchronized关键字包括monitor entermonitor exit两个JVM指令,能够保证随时都能执行到monitor enter之前都能必须从内存中获取数据。
  • synchronized关键字的指令严格遵守java happens-before规则,一个monitor exit指令之前必定有一个monitor enter。

Monitorenter
每一个对象都与一个monitor关联,一个monitor的lock锁只能被一个线程在同一时间获得 。synchronized关键字获得锁,其实就是获取对象的monitor。

点赞加关注!不能白嫖!没动力了!

点赞加关注!不能白嫖!

点赞加关注!不能白嫖!

猜你喜欢

转载自blog.csdn.net/liyuanbo1997/article/details/107696916