多线程——JUC
1. 线程
线程简介
程序:指令和数据的有序集合
进程:执行程序的一次过程,系统资源分配的单位
线程:CPU调度和执行的单位
一个进程可以有多个线程
- 线程及时独立的执行路径
- 程序运行时,后台存在多个线程(主线程、GC线程)
- 对同一份资源操作,会存在资源抢夺问题,需要加入并发控制
- 线程会带来额外开销(cpu调度时间,并发控制开销)
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致(独立概念)
线程创建
继承Thread类
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
避免OOP单继承局限性
实现Runnable接口
- 实现接口Runnable具有多线程能力
- 启动线程:传入目标对象,Thread对象.start()
避免单继承局限性,方便同一个对象被多个线程使用
实现Callable接口
- 实现Callable接口,需要返回值类型
- 重写call()方法,抛出异常
- 创建目标对象Tread
- 创建执行服务:
ExextorService ser = Exectors.newFixedThreadPool(1);
- 提交执行:
Future<Boolean> result1 = ser.submit(t1)
- 获取结果:
Boolean r1 = result1.get();
- 关闭服务:
ser.shutdownNow();
线程状态
线程停止
- 推荐线程自己停下来,而不是调用stop(),destroy()
- 建议使用一个标志位进行终止变量,当flag = false,线程终止
线程休眠
- sleep指当前线程堵塞的毫秒数,会抛出异常InterruptedExcetion
- sleep时间到达后进入就绪状态
- 每个对象都会有锁,但是sleep不会释放锁
线程礼让
-
线程从CPU从运行转为就绪,让正在执行的线程暂停但不是阻塞
-
让CPU重新调度,不一定会成功
join
Join合并线程,强制执行插入线程,执行完毕后,在执行其他线程
观测线程的状态
Tread.State
线程状态
new->尚未启动
runnable->执行状态
blocked->被阻塞等待监视器锁定
waiting->等待另一个线程
time_waiting->等待另一个线程到达动作指定的等待时间
terminated->一退出的线程
线程优先级
java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度那个线程来执行
线程优先级数字表示:
Tread.MIN_POIRITY=1
Tread.MAX_POIRITY=10
Tread.NORM_POIRITY=5
getPriority().setPriority
获取线程的优先级
守护线程
虚拟机必须确保用户线程执行完毕,不用等待守护线程执行完毕
线程同步
线程同步
由于同一个进程共享一块存储空间,为了保证数据在方法中被访问的正确性,加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待
使用后释放锁遇到的问题
- 一个线程持有锁会导致其他所有需要锁的线程挂起
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置
synchronized方法
synchronized默认锁的this
synchronized方法必须获得调用该对象的锁才能执行,不然线程会阻塞,方法一旦执行,就独占锁直到方法返回,后面的线程才能继续执行
synchronized块-> synchronized(obj) {}
obj是同步监视器
- obj可以是任何对象,推荐共享资源
- 同步方法中无序指定同步监视器,因为同步方法就是对象本身
执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁然后锁定访问
死锁
两个线程或多个线程都在等待释放资源,就会停止执行,形成死锁
产生死锁的条件
- 互斥条件:一个资源每次只能被一次进程调用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程一获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成头尾相接的循环等待资源的关系
public class current {
public static void main(String[] args) {
get get1 = new get(0,"bai");
get get2 = new get(1,"hui");
get1.start();
get2.start();
}
}
class misk{ }
class link{ }
class get extends Thread{
static misk m = new misk();
static link l = new link();
int choice;
String name;
public get(int choice, String name1) {
this.choice = choice;
this.name = name1;
}
@Override
public void run() {
if (choice == 0) {
synchronized (m) {
System.out.println(Thread.currentThread().getName() + name);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (l) {
System.out.println(Thread.currentThread().getName() + name);
}
}
} else {
synchronized (l) {
System.out.println(Thread.currentThread().getName() + name);
synchronized (m) {
System.out.println(Thread.currentThread().getName() + name);
}
}
}
}
}
Lock锁
JDK1.5之后,通过显式定义同步锁对象实现同步
每次只能有一个线程对Lock对象加锁,线程访问资源之前现货得Lock对象
ReentrantLock(可重入锁)实现Lock接口,与synchronized有相同的并发性和内存定义
privte final ReentrantLock lock = new ReenTRantLock();
public void run() {
try () {
lock.lock();
---需要加锁的内容--
}finally {
lock.unlock();
}
}
synchronized与lock的区别
- Lock是显式锁,synchronized是隐式锁
- Lock只有代码块锁
- Lock性能更好,有很好扩展
线程协作
生产者和消费者问题 -> 两个线程之间需要线程通信
管程法
信号灯法
使用一个标志表示生产与消费的关系
线程池
提前创建好线程直接放在池里,使用时获取,使用完放回
- 提高响应速度
- 降低资源消耗
- 便于线程管理
ExecutorService接口
viod executor(Runnable)执行Runnable
Future submit(Callbale task)执行Callable
void shutdown()关闭连接池
ExecutorService service = Executor.newFixedThreadPool(int);
service.executor(线程);
service.shutdown;
2. JUC
java.util.concurrent
Lock锁
synchronied与lock的区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c7BFJgOc-1631605059960)(
)]
- synchronied是内置关键字,lock是java类
- synchronied无法按判断锁的状态,lock可以判断
- synchronied会自动释放锁,lock必须手动释放
- synchronied如果阻塞会一直等待;lock不一定
- synchronied是非公平锁,lock可以自己设置公平
- synchronied适合锁少量代码,lock适合大量代码
虚假唤醒
不要使用if判断,要使用while循环
在if中判断,线程被唤醒后,不会再执行if里面的判断,而是继续执行唤醒之后的代码,如果是while会一直执行括号中的
synchronized和lock性能区别
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。
在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。
但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
2种机制的具体区别:
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
synchronized的底层原理
从上面可以看出,同步代码块是使用monitorenter和monitorexit指令实现的,同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现
同步代码块:monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁
同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象
使用Condition精准等待和唤醒线程
Condition将Object监视器方法( wait 、 notify和notifyAll )分解为不同的对象,通过将它们与任意Lock实现的使用相结合,产生每个对象具有多个等待集的效果。
Lock代替了synchronized方法和语句的使用,而Condition代替了对象监视器方法的使用。
条件(也称为条件队列或条件变量)为一个线程提供了一种挂起执行(“等待”)的方法,直到另一个线程通知某个状态条件现在可能为真。 因为对这个共享状态信息的访问发生在不同的线程中,它必须受到保护,所以某种形式的锁与条件相关联。 等待条件提供的关键属性是它以原子方式释放关联的锁并挂起当前线程,就像Object.wait一样。
public class lockTest {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.incream();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.decream();
}
},"B").start();
}
}
class Data {
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
// private Condition condition2 = lock.newCondition();
private int number;
public void incream() {
lock.lock();
try {
while (number != 0) {
condition1.await();
}
number++;
System.out.println(Thread.currentThread().getName()+number);
condition1.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decream() {
lock.lock();
try {
while (number == 0) {
condition1.await();
}
number--;
System.out.println(Thread.currentThread().getName()+number);
condition1.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
八锁现象
锁是锁住对象即方法的调用者或者Class对象(静态方法)
普通方法并不受锁的干扰
List不安全
解决方案
1. List<> list = new Vector<>();
2. List<> list = Collections.synchroniedList(new ArrayList<>());(装潢器模式)
3. List<> list = new CopyOnWriteArrayList<>();
写入时复制
Callable
new THread(new Runnable()).start();->
new Thread(new FutureTask<>()).start();->
new Thread(new FutureTask<>(Callable)).start();
CountDownLatch
类似减法计数器,数量不断减一,知道归零的时候await()被唤醒
CyclicBarrier
Semaphore
acquire()等待线程
release()释放信号量
作用:多个共享资源的互斥,并发限流,控制最大线程数
读写锁
独占锁(写锁)共享锁(读锁)
队列
阻塞队列 blockingQueue
方式 | 抛出异常 | 不抛出异常 | 阻塞等待 | 超前等待 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer()需要加上时间 |
移除 | remove() | poll() | take() | pop() |
判断队首元素 | element() | peek() | - | - |
同步队列 synchronizedQueue
不存储元素,存一个元素必须取出来才能存储下一个
使用put()、take()
线程池
三大方法
只是创建单个线程
Executors.newSingleThreadExecutor()
创建固定的线程池大小
Executors.newFixedThreadPool(5)
创建可伸缩的线程池
Executors.newCacheThreadPool()
七大参数
线程池创建不要使用Executors进行创建,而是使用ThreadPoolExecutor规避资源耗尽(OOM)
四种拒绝策略
new ThreadPoolExecutor.AbortPolicy()
不处理进来的线程,抛出异常
new ThreadPoolExecutor.CallerRunsPOlicy()
哪里的线程会哪里去
new THreadPoolExecutor.DiscardPolicy()
丢掉下城,不会抛出异常
new ThreadExecutor.DiscardOldestPolicy()
尝试竞争,不会抛出异常
JMM
JMM(java内存模型)规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量
-
线程解锁之前必须将共享内存理科刷回主存
-
线程加锁之前必须读取主存内最新值到工作内存中
-
加锁解锁是同一个锁
关于主内存与工作内存之间的具体交互协议
即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
Java内存交互的规则:
- 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
volatile-轻量级同步机制
- 保证可见性
- 不保证原子性
- 防止指令重排
可见性
解决内存可见性问题可以使用:
- Java中的volatile关键字:volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每个线程在每次使用volatile变量前都立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
- Java中的synchronized关键字:同步快的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。
- Java中的final关键字:final关键字的可见性是指,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程就能看见final字段的值(无须同步)
原子性
要想保证原子性操作,使用原子类解决或者使用lock锁
指令重排
源代码 ——> 编译器优化的重排——> 指令并行也会重排——>内存系统重排——>执行
数据之间具有依赖性不会重排
volatile防止指令重排的方法就是内存屏障
内存屏障
- 保证特定操作的执行顺序
- 可以保证某些变量的内存可见性
在普通读写与volatile写之间加入内存屏障
单例模式
饿汉式单例
public class Singleton {
private satic Singleton instance = new Singleton();
public Singleton getInstance() {
return instance;
}
}
懒汉式单例
public class Singleton {
private static Singleton instance;
public synchronized Singeton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
DCL懒汉式 (双重检测机制)
public class Singleton {
private static Singleton instance = null;
public Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
intance = new Singleton();
}
}
}
return instance;
}
}
每当判断如果有实例的话,只有在线程创建的时候抢夺锁,之后其他线程仅仅只有一个读取操作都需要等待锁,会造成效率低下的问题,需要锁很小范围
这种单例模式很容易被反射所破解,当你使用反射进行创建对象的时候,就会造成单例不安全,原子性失效,这时应该使用枚举类来保证原子性
Constructor<> declaredConstructor = EnumSingleton.class.getDeclareConstructor(String.class,int.class);//枚举类的反编译源码,创建的构造器并不是无参,而是带有参数的构造器
declareConstructor.setAccessible(true);
EnumSingleton instance = declareConstructor.newInstance();
CAS模式
主要依靠Unsafe类去操作内存
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的就执行操作,如果不是就一直循环
缺点:
- 循环耗时
- 一次性还能保证一个共享变量的原子性
- ABA问题
ABA问题
当线程A去引用主内存中的变量的时候,线程B也引用了这个变量,但其实线程A已经将这个变量修改了,线程B拿到的就是改变了的变量
解决ABA问题:引用原子引用,使用乐观锁(加入版本号控制)
==所有相同类型的包装类对象之间的比较全部使用equals比较,例如Integer在-128~127之间的赋值时在IntegerCache。cache()中产生的,会复用已有对象,这个区间的值可以使用 比较,超过这个区间产生的对象会在堆上产生
AtomicStampedReference<Integer> atomic = new AtomicStampedReferce<>(1,1);
----------main()----------------
new Thread(() -> {
//当见到期望值的时候,获取当前线程的版本号,然后改变期望值,变换版本号 atomicStampedReferce.compareAndSet(1,2,atomicStampedReferce.getStamp(),atomicStampedReferce.getStamp() + 1);
}
).start();
锁
公平锁和不公平锁
公平锁:不可以插队
不公平锁(默认):可以插队
可重入锁
拿到外面的锁就相当于拿到了里面的锁,只有当所有的锁解开,其他线程才能继续
自旋锁
就是底层操控CAS