Java多线程基础
实现多线程的方法:
- 继承Thread类覆写run()方法,创建对象并调用start()
- 实现Runnable方法,覆写run()方法,创建对象并调用start()
两种方法的区别
Thread不能用于多个线程之间的资源共享,Runnable可以用于多个线程的资源共享
实例
/**
* 创建线程方式一
* 1. 创建: 继承Thread+重写run
* 2. 启动: 创建子类对象+start
*/
public class StartThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
System.out.println("一边听歌");
}
}
public static void main(String[] args) {
//创建子类对象
StartThread st = new StartThread();
//启动
st.start();//不保证立即运行, 由CPU调用
// st.run();//普通方法
for (int i = 0; i < 2000; i++) {
System.out.println("一边coding");
}
}
}
线程的状态
创建状态
调用Thread thread = new Thread()方法后则进入创建状态,有相应内存空间和其他资源,但不可运行
就绪状态
创建线程后调用start()方法,进入就绪状态,进入线程队列,等待CPU运行
运行状态
就绪状态线程获得CPU资源,进入运行状态,自动调用该线程对象的run()方法
阻塞状态
调用sleep(),suspend(),wait(),进行耗时IO操作,都将进入阻塞状态,阻塞状态线程不能进入排队队列,只有引起阻塞的原因消除后,才可以转入就绪状态
死亡状态
调用stop()方法或run()方法执行结束后,处于死亡状态,死亡状态线程不具有继续运行的能力
问题:Java程序每次运行至少启动几个线程?
2个,每次使用java命令执行一个类时,都会启动一个JVM(线程),其中至少有两个线程,main线程,垃圾回收线程
线程的常用方法
thread.join() 让一个线程强制运行,期间其他线程无法运行
Thread.sleep() 一个线程暂时休眠(静态方法)
thread.yield() 线程让出CPU使用权限
synchronized关键字
Java提供的一种原子性内置锁,Java中每个对象都可以把它当做同步锁使用
- Java内置的,使用者看不到的锁称为内部锁,也叫做监视器锁
线程执行代码在进入synchronized代码块前会自动获取内部锁,其他线程访问该同步代码块就会被阻塞挂起
拿到内部锁的线程在
- 正常退出同步代码块,
- 抛出异常,
- 或在同步块内调用改内置锁资源的wait()系列方法时
释放该内置锁
synchronized的三种用法
- 修饰实例方法,进入同步代码前要获得当前对象实例的锁
- 修饰类的静态方法,进入同步代码前要获得当前类对象的锁
- 修饰代码块,进入同步代码前要获得给定对象的锁
synchronized和ReenTrantLock的区别
- 两者都是可重入锁
- synchronized依赖于JVM,ReenTrantLock依赖于API
- ReenTrantLock增加了一些高级功能
-
等待可中断
通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -
可实现公平锁
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。
所谓的公平锁就是先等待的线程先获得锁。
ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair) 构造方法来制定是否是公平的 -
可实现选择性通知(锁可以绑定多个条件)
在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,
用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。
synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。
Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。
在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,
用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。
而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。
如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
-
- 性能已不是选择标准
加锁和释放锁的语义
- 当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用共享变量时从主内存中加载,
- 释放锁时将本地内存中修改的共享变量刷新到主内存
volatile关键字
- 弱形式的同步
- 确保一个变量的更新对其他线程马上可见
- 当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存
- 其他线程读取该变量时,从主内存中获取最新值,而不是使用当前线程的工作内存中的值
synchronized和volatile的区别
-
synchronized是独占锁
- 同时只能有一个线程调用synchronized修饰的方法或者代码块,其他调用线程会被阻塞
- 同时会存在线程上下文切换和线程重新调度的开销
-
volatile是非阻塞算法,不会造成线程上下文切换的开销
-
volatile保证变量变化的可见性,但不保证操作的原子性
- 在写入变量值不依赖变量的当前值,(如果依赖当前值,是 获取-计算-写入 三步操作,这三步操作不是原子性的)
- 读写变量值时没有加锁(如果加锁就已经保证内存可见性了,不需要再使用volatile)
的情况下使用volatile
-
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。
-
volatile关键字只能用于变量
-
synchronized关键字可以修饰方法以及代码块
-
实际开发中使用 synchronized关键字的场景还是更多一些。
-
多线程访问volatile关键字不会发生阻塞,
-
synchronized关键字可能会发生阻塞
-
volatile关键字能保证数据的可见性,但不能保证数据的原子性。
-
synchronized关键字两者都能保证。
-
volatile关键字主要用于解决变量在多个线程之间的可见性,
-
synchronized关键字解决的是多个线程之间访问资源的同步性。
CAS compare and swap
JDK提供的非阻塞性原子操作,通过硬件保证比较比较-更新操作的一致性
乐观锁和悲观锁
悲观锁:
对数据被外界修改持保守态度,认为数据很容易会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。
乐观锁:
认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。
乐观锁并不会使用数据库提供的锁机制,一般在表中添加version字段或者使用业务状态来实现。乐观锁直到提交时才锁定,所以不会产生任何死锁。
其他锁
公平锁与非共平锁
独占锁与共享锁
可重入锁
自旋锁
ReentrantLock独占锁
ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的AQS阻塞队列里面。
方法:
获取锁
-
void lock(){
线程希望获取该锁。
如果锁当前没有被其他线程占用并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置AQS的状态值为1,然后直接返回。
如果当前线程之前已经获取过该锁,则这次只是简单地把AQS的状态值加1后返回。
如果该锁已经被其他线程持有,则调用该方法的线程会被放入AQS队列后阻塞挂起。 -
void lockInterruptibly() {
该方法与lock)方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出InterruptedException异常,然后返回。
} -
boolean tryLock()
尝试获取锁,
如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。
注意,该方法不会引起当前线程阻塞。 -
boolean tryLock(long timeout, TimeUnit unit)
尝试获取锁,与tryLock()不同的是,设置了超时时间,如果到了时间没有获取到该锁就返回false -
释放锁
void unlock(){
尝试释放锁,
如果当前线程持有该锁,则调用该方法会让该线程对该线程持有的AQS状态值减1,
如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。
如果当前线程没有持有该锁而调用了该方法则会抛出llegalMonitorStateException异常,
线程池ThreadPoolExecutor
- 介绍
线程池主要解决两个问题:
一是当执行大量异步任务时线程池能够提供较好的性能。
在不使用线程池时,每当需要执行异步任务时直接new一个线程来运行,而线程的创建和销毁是需要开销的。线程池里面的线程是可复用的,不需要每次执行异步任务时都重新创建和销毁线程。
二是线程池提供了一种资源限制和管理的手段,
比如可以限制线程的个数,动态新增线程等。每个ThreadPoolExecutor也保留了一些基本的统计数据,比如当前线程池完成的任务数目等。
另外,线程池也提供了许多可调参数和可扩展性接口,以满足不同情境的需要,程序员可以使用更方便的Executors的工厂方法,比如newCachedThreadPool(线程池线程个数最多可达Integer.MAX_VALUE,线程自动回收)、newFixedThreadPool(固定大小的线程池)和newSingle ThreadExecutor(单个线程)等来创建线程池,当然用户还可以自定义。 - 使用线程池的好处
降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控 - 如何创建线程池
-
通过构造方法实现,ThreadPoolExecutor()
-
通过Executor工具类的Executors来实现
FixedThreadPool :
该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor:
方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
CachedThreadPool:
该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 Executors 返回线程池对象的弊端如下: FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。 CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
-
Atomic
-
简介
Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
并发包 java.util.concurrent 的原子类都存放在 java.util.concurrent.atomic 下。
} -
JUC包中的原子类是哪4类? {
基本类型
使用原子的方式更新基本类型
AtomicInteger:整形原子类
AtomicLong:长整型原子类
AtomicBoolean :布尔型原子类
数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray:整形数组原子类
AtomicLongArray:长整形数组原子类
AtomicReferenceArray :引用类型数组原子类
引用类型
AtomicReference:引用类型原子类
AtomicStampedRerence:原子更新引用类型里的字段原子类
AtomicMarkableReference :原子更新带有标记位的引用类型
对象的属性修改类型
AtomicIntegerFieldUpdater:原子更新整形字段的更新器AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
}AtomicInteger类常用方法 1 2 3 AtomicInteger源码分析 1 2 3
AQS AbstractQueuedSynchronizer 抽象同步队列
这个类在在java.util.concurrent.lock包下
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,
比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。 private volatile int state;//共享变量,使用volatile修饰保证线程可见性 AQS使用CAS对该同步状态进行原子操作实现对其值的修改。 状态信息通过procted类型的getState,setState,compareAndSetState进行操作