java之多线程与高并发
1、程序、进程、线程、协程/纤程
1)程序
- 软件/可执行文件
- 静态
- =算法+数据结构
2)进程:cpu资源调度和分配的基本单位;
- 一个进程由一个或多个线程组成
- 动态
- 进程控制块+程序段+数据段
3)线程:cpu独立运行和独立调度的基本单位;程序执行流的最小单位
- 线程的运行由调度器进行安排调度,调度器是与os紧密相关的
- 生命状态与周期
- 部分相关概念:
- 线程安全:多个线程同时运行某一段代码时,每次运行结果和单线程运行结果是一致的,且中间变量和预期结果也是一样。
- 竞争条件/竞态条件/竞争危害:一个进程的输出结果是无法预测时,取决于多个线程之前的交替执行次序。
- 并发:多核下,同时执行多件事。
- 并行:看似同时,实际上是交替运行。
- 另补充:
- 死亡后不可再执行start()
- stop()不建议用,而是让线程正常结束来关闭
- os中线程多于JVM中线程
- sleep、wait都可能被打断Interrupted(),catch异常(InterruptedException)抛出异常时进一步做处理,或者继续运行【底层jdk锁那块有用到,没有用interrupted控制业务逻辑的】
- 用户级别的线程或轻进程
- 一个线程可以拥有多个协程。
- 它不是由os管理,而是由程序所控制
- 是一个特殊函数
- 切换者是用户/程序而非os,由于切换没有涉及os,效率高->开销远远小于线程的开销
2、线程的创建
1)继承Thread
2)实现Runnable
3)实现Callable
-
有返回值,可在call()中抛出异常
-
可使用线程池newFixedThreadPool管理线程
-
另补充:
//让线程运行起来
1、new thread01().start();
2、new Thread(new thread02()).start();
3、new Thread(()->{
...
}).start();
4、...
//启动线程的方式
1、继承Thread
2、实现Runnable
3、lambad表达式
4、通过线程池Executors.newCachedThreadPool...
3、常见方法
1)Thread.sleep()与wait()
- Thread.sleep():
- 必须指定时间
- 可以在任何地方使用
- 释放cpu执行权,但不释放锁
- Thread类中方法
- 让当前线程睡眠,阻塞
- 到时间后如果锁没有被其他线程占用,则再次得到锁,执行后面的代码;如果锁被其他线程占用,则等待其他线程释放锁
-
wait():
- 可指定可不指定时间
- 只能在同步方法/同步中调用,不然会抛IllegalMonitorStateException
- 释放cpu执行权,释放锁
- Object类中方法
- 让对应线程进入等待队列,阻塞
- wait(),时间一到立刻争夺资源;wait(time),时间到了,若有其他线程正在使用cpu,会等待它挂起或结束才会争夺
2)start
- 调用start()可启动一个线程,线程处于就绪状态,一旦获取cpu执行权,就进入运行状态,执行run(),运行结束,线程随即终止
3)join
-
插队
-
example
在主线程中调用t1.join(),即等t1执行完毕后,主线程才继续执行
4)Thread.yield()
- 礼让,重新竞争,返回到就绪状态
- 使用场景少见,性能测试时压测使用
6)getState()
- 可获取线程状态
7)notify/notifyAll
- 与wait配合使用
- 必须在同步代码块中/同步方法中使用
public class thread04{
public static int cnt = 10;
public static void main(String[] args) {
thread04 t = new thread04();
new Thread(new produce(t)).start();
new Thread(new consume(t)).start();
}
}
class produce implements Runnable{
private thread04 t;
public produce(thread04 t){this.t = t;}
@Override
public void run() {
while(true) {
synchronized (t) {
try {
while (thread04.cnt > 5) t.wait();
++thread04.cnt;
System.out.println("生产后的库存有:::" + thread04.cnt);
t.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class consume implements Runnable {
private thread04 t;
public consume(thread04 t){this.t = t;}
@Override
public void run() {
while(true) {
synchronized(t) {
try {
while(thread04.cnt <= 0)t.wait();
thread04.cnt--;
System.out.println("消费后剩余:"+thread04.cnt);
t.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
8)stop\destory\interrupt
- 不推荐
9)setPriority
- 设置线程优先级
- 默认数值为5,设置的越高优先级越高,最高为10
- 优先级只是表示了执行的概率,也就是说优先级高的线程比低的线程被执行的概率更高,但不代表一定就会优先执行。
10)isAlive
- 是否存活
4、多线程带来的一些问题
1、线程不安全
- example
2、死锁
- 指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
- 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
- 产生死锁的必要条件
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
- 处理死锁的方法:
- 预防死锁:打破以上必要条件(除互斥外)
- 避免死锁
- 检测死锁
- 接触死锁
5、线程同步
- 同步:协同、协助、互相配合,按预定的先后次序执行【针对共享的资源】
- 当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作
- 锁
- 异步锁:同一个进程内,多个线程间有互斥关系,只有等一个线程结束才能运行另一个线程
- 同步锁:多个线程运行同一个方法,因为方法上加上了同步,一次只能有一个线程运行,其他线程进入竞争中
- 加锁的这段代码被称临界区或互斥区
- 存放在一个对象的markword【关于锁的信息记录在这里】中(8字节),java版本是64bit的(8byte),对齐是保证是8的倍数,这样cpu读起来更快,效率更高
- 存放在一个对象的markword【关于锁的信息记录在这里】中(8字节),java版本是64bit的(8byte),对齐是保证是8的倍数,这样cpu读起来更快,效率更高
1、synchronized关键字
- 对象锁、同步锁
- 可重入锁
- JVM层面上,通过对象内部的monitor锁实现的,本质是依赖os mutex lock(重量级锁)实现的
- 不能用String常量(可能和别人锁定同一个常量)、基本数据类型的包装类对象
- synchronized代码块
- synchronized方法
2、Lock锁
-
代码实现
-
基于可重入锁
-
自旋锁
-
example
- 最好在finally块中释放锁,避免死锁
- 最好在finally块中释放锁,避免死锁
3、wait()/notify()/notifyAll()
4、volatile变量
- 一种比 sychronized 关键字更轻量级的同步机制
- volatile变量规则:对volatile变量的写入操作必须在对该变量的读入操作之前进行
- 保持可见性+禁用重排序
- 可见性是指:一个线程修改后,其他线程跳过了本地内存,可直接从主存中获取最新的值
- 禁用重排序:在写操作后插入一个写屏障指令,读操作之前插入一个读操作指令(内存屏障/栅栏:一种cpu指令)
- 指令重排序:编译器和处理器对代码结构进行重排序,达到更好的执行效果;cpu允许多条指令不按照程序规定的顺序分开发送给各相应电路单元处理,但是能获取到正确结果
- 缺点:不具原子性,做运算时(如自增操作)会存在安全问题(内存屏障内有多步操作的缘故)(通过原子类中CAS操作【基于乐观锁,当写入时,若寄存器/内存旧值已经不等于现值/本地内存,就将现值换为内存中值,然后继续尝试;若等于,则将内存中值换为新值】解决,这里也有多部操作,但是具备原子性,是因为底层指令是lock cmpxchg 指令保障了原子性)
5、…