Java 并发系列之四:java 多线程
1. 线程简介
2. 启动和终止线程
3. 对象及变量的并发访问
4. 线程间通信
5. 线程池技术
6. Timer定时器
7. 单例模式
8. SimpleDateFormat
9. txt
- java并发基础线程
- 线程简介
- 什么是多线程?
- 操作系统调度的最小单元,可以使用 JMX (ThreadMXBean)来查看一个普通的Java程序包含哪些线程
- 可以使用 jstack 查看运行时的线程信息
- 为什么要使用多线程?
- 1. 更多的处理器核心
- 2. 更快的响应时间
- 特别是多业务操作更加快速
- 3. 更好的编程模型
- Java提供了良好、考究并且一致的多线程编程模型
- 线程优先级
- 背景
- CPU分配时间片给线程,时间片用完就会发生线程调度,分配到时间片的多少决定了线程使用处理器资源的多少
- 作用
- 线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。
- Java
- 优先级的范围是1~10,默认优先级是5,优先级搞得线程分配时间片的数量要多于优先级低的线程
- 设置线程优先级时,针对频繁阻塞(休眠/IO操作)的线程需要设置设计高优先级,而偏重计算(需要较多CPU时间/偏运算)的线程需要设置较低的优先级,确保处理器不会被独占。
- ps:线程优先级不能作为程序正确性的依赖,因为有些操作系统会忽略Java线程对优先级的设定。
- 线程的状态
- 6大状态
- NEW
- 初始状态,线程被构建,但是还没有调用start()方法
- RUNNABLE
- 运行状态,Java线程将操作系统中的就绪和运行两种状态统称为“运行状态”
- BLOCK
- 阻塞状态,表示线程阻塞于锁
- WAITING
- 等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或者等待)
- TIME_WAITING
- 超时等待状态,可以在等待的时间自行返回的
- TERMINATED
- 终止状态,表示当前线程已经执行完毕
- Java状态转移
- WAITING-->RUNNABLE
- Object.notify()
- Object.notifyAll()
- LockSupport.unpark(Thread)
- TIME_WAITING-->RUNNABLE
- Object.notify()
- Object.notifyAll()
- LockSupport.unpark(Thread)
- BLOCK-->RUNNABLE
- 获取到锁
- 1. 实例化后还未start()方法时的状态 New
- 2. New-->RUNNABLE
- 系统调度
- Thread.start()
- running-->ready
- Thread.yield
- ready-->running
- 3. RUNNABLE-->WAITING
- Object.wait()
- Thread.join()
- LockSupport.park()
- 4. RUNNABLE-->TIME_WAITING
- Object.wait(long)
- Thread.sleep(long)
- Thread.join(long)
- LockSupport.parkNanos()
- LockSupport.parkUntil()
- 5. RUNNABLE-->BLOCKED
- 等待进入synchronized方法
- 等待进入synchronized块
- 6. RUNNABLE-->TERMINATED
- run方法结束
- Java 线程状态变迁
- yield
- 暂停当前正在执行的线程对象。
- yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
- yield()只能使同优先级或更高优先级的线程有执行的机会。
- 注意:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
- 注意
- 等待进入synchronized方法/块 为阻塞状态
- java.concurrent.Lock为 等待状态,因为Lock接口对于阻塞的实现使用了LockSupport类中的相关方法
- Daemon线程
- 守护线程
- 特殊的线程,陪伴的含义,当进程中不存在非守护线程了,则守护线程自动销毁。
- 典型的守护线程是垃圾回收线程
- 作用是为其他线程提供便利服务
- 是一种支持性线程,主要是用在后台程序做一些后台调度与支持性工作。这意味着当JVM中没有非Daemon线程时,JVM将自动退出。
- 可以通过调用Thread.setDaemon(true)方法将线程设为Daemon线程。(注:该方法必须在start()或者run()方法前执行,也就是说必须在线程启动前执行)
- 注:Daemon线程被用作完成支持性工作,但是在java虚拟机退出时,Daemon线程中的finally块并不一定会执行。在构建Daemon时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
- 程序 VS 进程 VS 线程
- 程序
- 一组指令的有序结合,是静态的指令,是永久存在的
- 进程
- 具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源(打开的文件,创建的socket)分配和调度的一个独立单元。进程的存在是暂时的,是一个动态的概念。
- 线程
- 线程是CPU调度和分配的基本单位,是比进程更小的能独立运行的基本单元。本身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈)。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
- 进程 VS 线程
- 定义
- 进程是资源分配和调度的基本单位;线程是CPU/任务调度和执行的基本单位
- 包含关系
- 1个进程包含有多(大于等于1)个线程; 线程是进程的一部分(轻量级进程)
- 地址空间
- 进程之间地址空间独立; 同一进程内的线程共享本进程的地址空间
- 切换开销
- 进程之间切换开销大; 线程之间切换开销小(创建和销毁)
- 创建
- 进程fork/vfork; 线程pthread_create
- 销毁
- 进程结束,它拥有的所有线程都将销毁; 线程结束,不会影响同个进程中的其他线程
- 私有属性
- 进程:PCB(进程控制块); 线程:TCB(线程控制块),线程Id,寄存器,上下文
- 一个程序至少只有一个进程,一个进程至少有一个线程
- 启动和终止线程
- 构造线程
- init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc )
- 父线程就是当前线程(开启多线程的线程),子线程对象是由其父线程来进行空间分配的,继承了parent线程是否为Daemon、优先级、加载资源的contextClassLoader以及可继承的ThreadLocal。还会分配给一个唯一的ID来标识这个child线程。
- init()运行完毕,线程对象就初始化好了,在堆内存中等待运行
- 构造函数
- Thread(Runnable target)
- Thread(Runnable target, String name)
- 因为Thread实现类Runnable,所以可以传递Thread
- 如果多个new Thread()里面的Runnable是同一个线程对象,那么那个对象的变量时被这几个新的线程共享的变量。
- 启动线程
- start() 含义:当前线程(即parent线程)同步告知java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程
- start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用run()方法,也就是使得线程得到运行,启动线程,具有异步执行的效果。而直接调用run方法不是异步而是同步
- 注意:执行start()方法的顺序不代表线程启动的顺序
- 注意:启动一个线程之前,最好起个名字
- 活动状态就是线程已经启动且尚未终止(isAlive为true),start()调用了就是活动状态了
- 理解中断
- 被其他线程中断: interrupt()
- 判断是否被中断:isInterrupted()
- 测试线程Thread对象是否已经被中断,但是不清除状态标志
- 中断表示复位:Thread.interrupted()
- 测试当前线程是否已经被中断,执行后具有将状态标志清除为false的功能
- 每次抛出InterruptedException之前,JVM会先将中断标识位清除
- 注意:不论是TERMINATED还是InterruptedException, isInterrupted都返回false
- 安全地终止线程
- 中断方式 Interrupt
- 标识位boolean true false
- 当run方法完成后线程终止
- 抛出异常 throw new InterruptedException
- 过期的
- suspend() 暂停
- 调用后,线程不会释放已经占有的资源(比如锁),而是占有资源进入睡眠状态,容易引发死锁。
- 缺点是独占、数据不同步
- resume() 恢复
- 缺点是独占、不同步
- stop() 终止
- 在终结一个线程时不会保证线程的资源正常释放
- 使用stop释放锁将会给数据造成不一致的结果
- 会抛出java.lang.ThreadDeath异常
- 暴力停止线程
- 其他
- sleep()
- 在指定的毫秒内让当前"正在执行的线程"休眠(暂停执行),即this.currentThread()返回的线程
- currentThread()
- 构造方法
- Thread.currentThread().getName()
- main
- this.getName()
- Thread-0
- 直接调用run()方法
- Thread.currentThread().getName()
- main
- this.getName()
- Thread-0
- 调用start()方法
- Thread.currentThread().getName()
- Thread-1 || setName("A")--> A 。
- this.getName()
- Thread-0
- isAlive()
- 线程处于正在运行或准备开始运行的状态就认为线程是存活的, start()之后就是活跃状态了
- Thread.sleep() false
- 构造方法||直接调用run()方法||调用start()方法
- Thread.currentThread().isAlive()
- true
- this.isAlive()
- false
- yield()
- 放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间,但放弃的时间不确定,有可能刚刚放弃,马上就又获得CPU时间片。
- 将CPU让给其他资源导致速度变慢
- 优先级
- 优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A线程是一样的
- 优先级具有规则性,让CPU尽量将执行资源让给优先级比较高的线程。线程的优先级与代码的执行顺序(start()的先后顺序)无关
- 优先级具有随机性,优先级高的线程不一定每一次都先执行完
- 有副作用
- 用等待/通知机制代替
- 对象及变量的并发访问
- synchronized
- 同步方法
- 方法内的变量为线程安全
- 实例变量非线程安全
- 多个对象多个锁
- synchronized取得的锁都是对象锁
- synchronized方法和锁对象
- A线程先持有object对象的Lock锁,B线程可以异步的方式调用object对象中的非synchronized类型的方法。
- A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的其他synchronized类型的方法则需要等待,也就是同步。
- 解决了脏读问题
- 可重入锁
- 自己可以再次获得自己的内部锁
- 当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。如果不可锁重入的话,就会造成死锁。
- 在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的
- 可重入锁也支持在父子类继承的环境中
- 子类完全可以通过可重入锁调用父类的同步方法
- 出现异常,线程持有的锁会自动释放
- 同步不具有继承性,还得在子类的方法中添加synchronized关键字
- 有弊端的,假如A线程调用同步方法执行一个长时间任务,B线程则必须等待比较长的时间,这样的情况可以用synchronized语句块来解决,虽然能实现同步,但会受到阻塞,所以影响运行效率。
- 同步语句块
- 当两个并发线程访问同一个对象object的synchronized(this)同步代码块时,一段时间内只有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行改代码块
- 当一个线程访问一个对象object的synchronized(this)同步代码块时,另一个线程仍然可以访问该object对象中的 非synchronized(this)同步代码块
- 一半同步一半异步
- 当一个线程访问一个对象object的synchronized(this)同步代码块时,另一个线程对同一个object对象中所有其他synchronized(this)同步代码块的访问将被阻塞,说明synchronized使用的对象监视器是一个
- synchronized方法和synchronized(this)同步代码块都是锁定当前对象的
- 将任意对象作为对象监视器,synchronized(非this对象anyobject)
- 优点
- synchronized(非this对象anyobject)代码块中的程序与同步方法是异步的
- 不与其他锁this同步方法争抢this锁,可以大大提高运行效率
- 结论
- 当多个线程同时执行synchronized(x)同步代码块时呈现同步效果
- 同步原因,使用了同一个对象监视器
- 当其他线程执行x对象中synchronized同步方法时呈现同步效果
- 当其他线程执行x对象方法里面的synchronized(this)同步代码块时呈现同步效果
- 针对X对象内部的同步方法和同步代码块
- 多个线程调用同一个方法是随机的
- 多个线程之间没有固定的顺序,随机的
- 静态同步synchronized方法和synchronized(class)代码块
- 应用在静态方法上
- 对当前的 *.java文件对应的Class类进行持锁
- 注意,一个是对象锁,一个是Class锁,会出现异步的情况,Class锁可以对类的所有对象实例起作用,
- synchronized(class)对class上锁后,其他线程只能以同步的方式调用class2的静态同步方法
- 两个不同的对象,但是静态同步方法还是同步运行
- 只要对象不变,即使对象的属性被改变,运行的结果还是同步
- synchronized(string)
- String常量池特性,两个线程拥有相同的锁,造成另一个线程不能运行
- 注意:给string赋值另一个值,锁对象就变了
- 大多数情况下,同步synchronized代码块都不使用String作为锁对象,而改用new Object()实例化对象
- 多线程死锁
- synchronized(lock1){
- synchronized(lock2){}
- }
- synchronized(lock2){
- synchronized(lock1){}
- }
- 监测方法
- 1. jps查看运行的线程Run的id
- 2. jstack -l id
- 3. DeadThread
- volatile
- 非原子:i++
- 解决就是用synchronized或者AtomicInteger原子类
- volatile v.s. synchronized
- 修饰
- volatile只能用于修饰变量
- synchronized可以修饰方法以及代码块
- 阻塞
- 多线程访问volatile不会发生阻塞
- 多线程访问synchronized会阻塞
- 原子性
- volatile不能保证原子性,能保证可见性
- synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步
- 作用
- volatile解决的是变量在多个线程之间的可见性
- synchronized解决的是多个线程之间访问资源的同步性
- 线程安全包含原子性和可见性两个方面,java的同步机制都是围绕这两个方面来保证线程安全的
- Atomic 原子方法和原则方法虽然都是原子的,但是原子方法和原子方法之间的调用不是原子的,解决这样的问题必须用同步,synchronized
- 线程间通信
- 不使用等待/通知机制实现多个线程之间的通信
- 传统的使用sleep()+while(true)死循环来实现多个线程间的通信
- 浪费CPU资源
- volatile & synchronized
- 共享内存和本地内存拷贝的同步更新问题,使得变量不一定能是最新的
- volatile: 保证所有线程对变量的可见性
- synchronized:保证线程对变量访问的可见性和排他性,获取monitor; 主要确保多个线程在同一时刻,智能有一个线程处于方法或者同步块中。
- synchronized
- Monitor.Enter--->get监视器Monitor
- Enter成功--->锁定对象Object----->Monitor.Exit----->通知同步队列中的线程出队列
- Enter失败---->线程进入同步队列SynchronizedQueue----->Monitor.Exit后通知,出队列
- 对象、监视器、同步队列和执行线程之间的关系
- 等待/通知机制
- 做什么和怎么做解耦,生产者/消费者模式
- 任意java对象所具备的
- 依托于同步机制,目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量作出的修改
- 等待通知机制:线程A调用了对象O的wait()方法进入了等待状态,而线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象的wait()与notify()或notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
- 方法
- notify(): 通知在一个对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁。
- notyfyAll(): 通知所有等待在该对象上的线程
- wait(): 调用该方法的线程进入waiting状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁。在从wait方法返回前,线程与其他线程竞争重新获得锁。
- wait(long): 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回(没有线程对锁进行唤醒就自动唤醒)。
- wait(long, n): 对于超时时间更细粒度的控制,可以达到纳秒。
- 注意
- 1)使用wait()、notify()、notifyAll()方法都需要先对调用对象加锁。如果没有持有适当的锁,也会抛出IllegalMonitorStateException
- 在调用condition.await()方法之前必须调用lock.lock()代码获得同步监视器,否则会报错IllegalMonitorStateException异常
- 2)调用wait()方法后,线程状态由RUNNING变为WAITTING,将锁释放,并将当前线程放到对象的等待队列。WaitingQueue
- 3)notify()或notifyAll()方法调用后,不会立刻释放锁,等待线程依旧不会从wait()返回,需要等待调用notify()、notifyAll()的线程释放锁之后,等待线程才可能会拿到锁,等待线程才有机会从wait()返回。
- 4)notify()方法将等待队列WaitingQueue中的一个等待线程从等待队列中移到同步队列SychronizedQueue中,而notifyAll()方法则是将等待队列WaitingQueue中的所有线程全部移动到同步队列SynchronizedQueue中,被移动的线程状态由WAITING变为BLOCKED。
- 5)从wait()返回的前提是获取调用对象的锁。
- 6) 执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。
- 7) wait()方法锁释放,notify()锁不释放,sleep()锁不释放
- 8) 当线程呈wait()状态时,调用线程对象的interrupt()方法会出现InterruptedException异常,导致线程终止,锁也会被释放
- 实现
- synchronized+wait/notify实现等待通知模式
- 被通知的线程是JVM随机选择的
- 所有的线程都会注册在一个对象上
- condition + lock 实现等待通知模式
- 支持选择性通知,调度线程上更加灵活
- Lock对象里面可以创建多个Condition实例(对象监视器)
- 等待/通知的经典范式
- 等待方(消费者)
- 原则
- 加锁:获取对象的锁
- 循环:如果条件不满足,那么调用对象的wait()方法,被通知后仍然要检查条件。
- 处理逻辑:条件满足则执行对应的逻辑
- 伪代码
- synchronized(对象) {
- while(条件不满足) {
- 对象.wait();
- }
- 对应的处理逻辑
- }
- 通知方(生产者)
- 原则
- 获取对象的锁
- 改变条件
- 通知所有等待在对象上的线程notifyAll()
- 伪代码
- synchronized(对象) {
- 改变条件
- 对象.notifyAll();
- }
- 一个生产者一个消费者
- 条件不同,一个满足条件,一个不满足条件
- 伪代码
- synchronized(对象) {
- if(条件满足) {
- 对象.wait();
- }
- 跟上面条件一致
- 对应的处理逻辑
- 对象.notify();
- }
- 操作数
- synchronized代码块
- 操作栈
- synchronized方法
- 多生产者多消费者
- 容易出现假死情况
- 假死状态的线程都呈WAITING状态
- 原因:notify唤醒的可能是异类,也可能是同类;比如生产者唤醒生产者
- 解决:将notify()改成notifyAll(); 不光通知同类,也通知异类
- synchronized(对象) {
- while(条件满足) {
- 对象.wait();
- }
- 对应的处理逻辑
- 跟上面条件一致
- 对象.notifyAll();
- }
- 操作数
- synchronized代码块
- 操作栈
- synchronized方法
- 等待超时模式
- 等待超时模式就是在等待/通知范式基础上增加了超时控制,避免执行时间过长,也不会“永久”阻塞调用者,而是按照调用者的要求返回。
- 超时等待:调用一个方法时,等待一段时间(一般给定一个时间段),如果该方法能够在给定的时间段内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
- 伪代码
- //对当前对象加锁
- public synchronized Object get(lon mills) throws InterruptedException{
- long future = System.currentTimeMillis() + mills;//超时时间
- long remaining = mills;//等待持续时间
- //当超时大于0并且result返回值不满足要求则继续等待
- //当时间到了或者返回结果满足要求则不再等待
- while((result == null) && remaining > 0){
- wait(remaining);
- remaining = future - System.currentTimeMillis();
- }
- return result;
- }
- 应用场景:针对昂贵资源(比如数据库的连接)的获取都应该加以超时限制,是系统的一种自我保护机制
- 管道输入/输出流
- 管道输入输出流与普通的文件输入输出流或者网络输入输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介是内存
- 一个线程发送数据到输出管道,另一个线程从输入管道中读取数据
- 4种具体的实现
- PipedOutputStream
- write(string.getBytes())
- PipedInputStream
- read(byte[] byteArray)
- PipedReader
- read(char[] xx)
- PipedWriter
- write(string)
- 面向字节
- 面向字符
- 注意:out.connect(in); 对于Piped类型的流,必须先要进行绑定,也就是调用connect方法,如果没有将输入/输出流绑定起来,对于该流的访问将会抛出异常
- 读取线程启动后如果没有数据被写入,线程会阻塞在in.read()代码中,直到有数据被写入,才继续往下运行。
- Thread.join()的使用
- 本质
- 涉及了等待/通知机制,等待前驱线程结束,接收前驱线程结束通知,源码本质也是wait()和notifyAll()
- join方法的作用是等待线程对象销毁
- while(isAlive()){
- wait(0);//表示永远等下去
- }
- 指导join线程终止后,线程的this.notifyAll()方法会被调用,调用notifyAll()方法是在JVM里实现的,所以在JDK中看不到
- CountDownLatch可以实现join的功能,并且比join的功能更多
- 方法
- join(): 当前线程A等待 thread线程终止 之后才从 thread.join 返回
- join(long millis): 如果在给定的时间内没有终止,那么将会从该超时方法中返回,毫秒
- join(long millis,int nanos): 如果在给定的时间内没有终止,那么将会从该超时方法中返回,纳秒
- join过程中,如果当前线程对象被中断,则当前线程出现InterruptedException异常
- join v.s. synchronized
- 共同点
- 具有使线程排队运行的作用,有点类似同步的运行效果
- 区别
- join在内部使用wait()方法进行等待,具有释放锁的特点
- synchronized关键字使用的是 对象监视器原理作为同步
- join(long) v.s. sleep(long)
- join(long)在内部使用wait(long)方法进行等待,具有释放锁的特点
- sleep(long)不释放锁
- 注意:join后面的代码提前运行可能会出现陷阱意外,原因在于join方法先运行抢到锁,然后释放锁;之后再次争抢锁,发现时间已经过去,就会释放锁执行后面的代码,导致出现意外。
- ThreadLocal的使用
- 背景
- 变量值的共享可以使用public static变量的形式,所有的线程都使用同一个public static变量
- threadLocal实现每一个线程都有自己的共享变量
- ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
- ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。作用:提供一个线程内公共变量,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
- 方法
- initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。如果set()方法没有调用,第一次get()方法调用时会进行初始化 initialValue(),每个线程会调用一次。
- get()方法是用来获取ThreadLocal在当前线程中保存的变量副本
- set(T value)用来设置当前线程中变量的副本
- remove()用来移除当前线程中变量的副本
- 工作原理
- Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个Map,他的key是ThreadLocal实例对象。
- 当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value,值时则类似。
- ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。
- 由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的,
- 存储结构的好处
- 1、线程死去的时候,线程共享变量ThreadLocalMap则销毁。
- 2、ThreadLocalMap<ThreadLocal,Object>键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map<Thread, Object>键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。
- 弱引用GC
- 1、使用完线程共享变量后,显示调用remove方法清除线程共享变量可以及时清除
- 2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。
- 3、对于ThreadLocal变量,我们可以手动的将其置为Null,比如tl =null。那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收。
- 解决Hash冲突方法
- 线性探测
- 应用场景: 用来解决 数据库连接、Session管理、AOP耗时统计等。
- 注意
- 不过有点遗憾的是只能放一个值,再次调用set设置值,会覆盖前一次set的值。如果要多个变量,新建多个ThreadLocal对象
- 是单个线程内函数和组件的共享变量,不是多线程的共享变量,线程隔离
- 覆盖initialValue()方法可以设置默认初始值,使得get()不会返回null
- 使用InheritableThreadLocal类可以让子线程从父线程中取得值,如果子线程在取得值得同时,主线程将InheritableThreadLocal中的值进行修改,那么子线程取到的值还是旧值
- 线程池技术
- 好处
- 1. 降低资源消耗
- 通过重复利用已经创建的线程消除了频繁创建和消亡线程的系统资源开销
- 2. 提高响应速度
- 当任务到达时,任务可以不需要等等待线程创建就能立即执行
- 3. 提高线程的可管理型
- 统一分配,调优和监控
- 4. 面对过量任务的提交能够平缓的劣化。
- 实现原理
- 处理流程
- 1. 提交任务
- 2. 判断核心线程池是否已满,否 创建线程执行任务,是下一步
- 3. 判断队列是否已满,否 将任务存储在队列里,是下一步
- 4. 判断线程池是否已满, 否 创建线程执行任务,是下一步
- 5. 按照饱和策略处理无法执行的任务
- 工作线程
- 线程池创建线程时,会将线程封装成工作线程worker,worker在执行完任务之后还会循环获取工作队列里的任务来执行
- 线程池的使用
- 见 ThreadPoolExecutor
- 线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断从工作队列上取出工作并执行。当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了一个任务后会通知任意一个工作者线程,随着大量任务被提交,更多的工作者线程会被唤醒。
- 线程池的数量不是越多越好,具体的数量需要评估每个任务的处理时间,以及当前计算机的处理器能力和数量。使用的线程过少,无法发挥处理器的性能;使用的线程过多,将会增加系统的无故开销,起到相反的作用。
- 应用
- 数据库连接池
- 服务器线程池
- 更多参加java并发框架-Executor框架
- Timer定时器
- 线程调度任务以供将来在后台线程中执行的功能。 任务可以安排一次执行,或定期重复执行。
- 定时计划任务
- 构造函数
- Timer():创建一个新的计时器
- Timer(boolean isDaemon):创建一个新的计时器,可以指定其关联的线程作为守护程序运行
- Timer(String name):创建一个新的计时器,其关联的线程具有指定的名称
- Timer(String name,boolean isDaemon):创建一个新的计时器,其关联的线程具有指定的名称,并且可以指定作为守护程序运行
- 这个类是线程安全的:多个线程可以共享一个单独的Timer对象,而不需要外部同步。内部使用多线程的方式进行处理
- API
- Timer.cancel()
- 终止此计时器,将任务队列里的全部任务清空
- 有时候不一定会停止执行计划任务,而是正常执行,因为有时候并没有抢到queue锁
- TimerTask.cancel()
- 将自身从任务队列里清除,其他任务不受影响
- purge()
- 从该计时器的任务队列中删除所有取消的任务。
- schedule(TimerTask task, Date time)
- 在指定的日期,执行一次某任务
- schedule(TimerTask task, Date firstTime, long period)
- 从指定的时间开始 ,按照执行的间隔周期性地无限循环地执行某一任务。
- 执行任务的时间晚于当前时间,在未来执行
- 执行任务的时间早于当前时间,立即执行
- schedule(TimerTask task, long delay)
- 以当前时间为参考时间,延迟指定的毫秒数后执行一次制定任务
- schedule(TimerTask task, long delay, long period)
- 以当前时间为参考时间,延迟指定的毫秒数后执行一次制定任务,再以某一个时间间隔无限次数地执行制定任务
- scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
- 从指定的时间 开始 ,对指定的任务执行重复的 固定周期执行 。
- scheduleAtFixedRate(TimerTask task, long delay, long period)
- 以当前时间为参考时间,在指定的延迟之后 开始 ,重新执行 固定周期的指定任务。
- schedule V.S. scheduleAtFixedRate
- 延时
- 下一次任务的执行时间参考的是上一次任务的“结束”时的时间计算
- 不延时
- schedule
- 下一次任务的执行时间参考的是上一次任务的“开始”时的时间计算
- scheduleAtFixedRate
- 下一次任务的执行时间参考的是上一次任务的“结束”时的时间计算
- 追赶性
- schedule
- 执行任务的时间早于当前时间,立即执行, 不填充
- scheduleAtFixedRate
- 执行任务的时间早于当前时间,立即执行, 不补充
- Timer中允许有多个TimerTask任务,以队列的形式被顺序执行,所以执行的时间和预期的时间不一致,因为前面的任务有可能消耗的时间太长,后面的任务运行的时间也会被延迟
- Java 5.0引入了java.util.concurrent软件包,其中一个java.util.concurrent程序是ScheduledThreadPoolExecutor ,它是用于以给定速率或延迟重复执行任务的线程池。 这实际上是对一个更灵活的替代Timer / TimerTask组合,因为它允许多个服务线程,接受各种时间单位,并且不需要子类TimerTask (只实现Runnable )。 使用一个线程配置ScheduledThreadPoolExecutor使其等同于Timer 。
- 单例模式
- 立即加载/饿汉模式
- 静态实例初始化
- 私有构造方法
- 静态getInstance
- 延迟加载/懒汉模式
- 静态实例声明
- 私有构造方法
- 静态getInstance{
- if(是null){
- 初始化
- }
- }
- 解决
- 声明synchronized关键字
- 尝试同步代码块
- 针对重要代码进行单独的同步
- 使用DCL双检查锁机制
- 使用静态内置类实现单例模式
- 序列化和反序列化的单例模式实现
- readResolve()
- 使用static代码块来实现单例模式
- 使用enum枚举数据类型实现单例模式
- SimpleDateFormat
- 负责日期的转换和格式化
- 非线程安全的
- 解决方法
- 每个线程一个SimpleDateFormat实例
- ThreadLocal类
10. 参考网址
- 参考来源:http://cmsblogs.com/wp-content/resources/img/sike-juc.png
- 《Java并发编程的艺术》_方腾飞PDF 提取码:o9vr
- http://ifeve.com/the-art-of-java-concurrency-program-1/
- Java并发学习系列-绪论
- Java并发编程实战
- 死磕 Java 并发精品合集
Java 并发系列之四:java 多线程的更多相关文章
- java并发系列(六)-----Java并发:volatile关键字解析
在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保证可见性 ...
- 【Java并发系列】--Java内存模型
Java内存模型 1 基本概念 程序:代码,完成某一个任务的代码序列(静态概念) 进程:程序在某些数据上的一次运行(动态) 线程:一个进程有一个或多个线程组成(占有资源的独立单元) 2 JVM与线程 ...
- java并发系列(八)-----java异步编程
同步计算与异步计算 从多个任务的角度来看,任务是可以串行执行的,也可以是并发执行的.从单个任务的角度来看,任务的执行方式可以是同步的,也可以是异步的. Runnable.Callable.Future ...
- Java并发系列[5]----ReentrantLock源码分析
在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...
- Java 并发系列之二:java 并发机制的底层实现原理
1. 处理器实现原子操作 2. volatile /** 补充: 主要作用:内存可见性,是变量在多个线程中可见,修饰变量,解决一写多读的问题. 轻量级的synchronized,不会造成阻塞.性能比s ...
- Java 并发系列之一:java 并发体系
1. java 并发机制的底层原理实现 1.1 volatile 1.2 synchronized 1.3 原子操作 2. java 内存模型(JMM) 3. java并发基础线程 4. java ...
- Java 并发系列之一
Java 并发系列之一 简单的总结了一些 Java 常用的集合之后,发现许多集合都针对多线程提供了支持,比如 ConcurrentHashMap 使用分段锁来提高多线程环境下的性能表现与安全表现.所以 ...
- Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析
学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, Cyc ...
- Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式
在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...
随机推荐
- centos安装sftp服务
一.创建sftp服务数据目录及相关测试用户 [root@localhost ~]# mkdir -pv /data/sftp/ #sftp数据目录 [root@localhost ~]# chown ...
- 【杂文】NOIP2018 蒟蒻自闭记
[杂文]NOIP2018 蒟蒻自闭记 都 \(9102\) 年了,谁还记得 \(2018\) 年的事啊 \(QAQ\) . 还有两个月就要去参加首届 \(CSP\) 了. 想着如果再不记下去年那些事儿 ...
- 上下文的哲学思考:上下文=环境 & 上下文=对象+行为+环境
事物的存在和运行所依赖的全部资源(能够看到和使用的一切)(环境). 上下文研究的是一个时段内,多个主体.对象在历次操作活动时,在空间的信息投射. 上下文是事物存在和生存活动的气泡,气泡消失,事物消失. ...
- js展示long型精度问题解决(server端解决)
问题:后端返回了个Long型的数据,在前端展示时最后2位变为00了 例如返回Long型的数据为75874464836881101,结果接口返回变为75874464836881100了 解决方法: 1. ...
- Kafka学习笔记1——Kafka的安装和启动
一.准备工作 1. 安装JDK 可以用命令 java -version 查看版本
- 阿里云开发工具包(SDK)
参考: 阿里云开发工具包(SDK)For Python Alibaba Cloud SDK for Go
- 开发--CentOS-7安装及配置
开发|CentOS-7安装及配置 本文主要进行详细讲解CentOS7.5系统的安装过程,以及CentOS系统初始化技术.我并不想将这篇文章变成一个教程,尽管我将详细的进行每一步的讲解,enjoy! 前 ...
- 网站怎么上传到服务器流程,从本地到服务器上线过程并通过域名(IP地址)进行访问
制作好的网页想要发布到互联网,该怎么发布呢?我们需要将保存在本地的站点上传站点到服务器,首先我们需要准备一个服务器(可通过服务器公网IP地址访问),也可以购买域名,域名购买可以通过阿里云.腾讯云.百度 ...
- FPM九:配置FPM Launchpad
1.事物代码LPD_CUST,点击新建输入角色和实例保存. 2.新建文件夹: 3.新建应用程序 这样一个菜单的LAUNCHPAD就好了. 4.FPM_WB运行FPM工作台,新建OVP应用程序. 保存本 ...
- Spring Cloud 微服务实战笔记
Spring Cloud 微服务实战笔记 微服务知识 传统开发所有业务逻辑都在一个应用中, 开发,测试,部署随着需求增加会不断为单个项目增加不同业务模块:前端展现也不局限于html视图模板的形式,后端 ...