框架是个好东西,可早晚有一天会过时,这世界上就没有亘古不变的东西,来学下Java基础吧
JUC并发编程
并发与并行的区别
- 并发:同一时间段,多个任务都在执行(单位时间段内不一定同时执行)
- 并行:单位时间内,多个任务同时执行
为什么要使用多线程
- 计算机底层:
多核CPU时代意味着多个线程可以同时运行,减少了以前提出的线程切换的开销
单核时代:在单核时代使用多线程主要是为了提高CPU和IO设备的综合利用率,如果是单线程程序,会出现当CPU进行计算时,IO设备空闲,当IO设备进行读写的时候,CPU空闲,综合利用率在50%左右。但如果是双线程程序,当一个线程执行CPU计算时,另一个线程可以进行IO操作,这样理想的利用率可以达到100%
多核时代:多线程存在的目的是为了提高CPU的利用率,CPU可以同时运行多个线程,让CPU的每个核心都跑满线程,可以提高CPU的利用率
- 互联网发展趋势:
并发量高,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力
线程的状态
在Java基础(二)中有详细的描述
多线程可能带来的问题
比如:内存泄漏,上下文切换,死锁和受制于硬件和软件的资源闲置问题
上下文切换
一般线程数都大于CPU的核心数,而CPU的每个核心单位时间只能运行唯一的线程,所以CPU采取的策略就是为每个线程都分配相同的运行时间,并在运行时间结束的时候切换到下一个线程。上下文切换具体说的就是:当CPU的一个核心要切换到下一个线程时,上一个线程会保存自己的状态,而一个线程从保存自己的状态到再被加载的过程就是一次上下文切换
上下文切换可能是消耗操作系统中时间最多的操作
Linux相比于其他操作系统有很多的优点,其中就包括:上下文切换和模式切换消耗的时间非常少
死锁
多个线程被无期限的阻塞,这就是死锁
模拟场景:A线程持有资源2,等待资源1,B线程持有资源1,等待资源2
//资源
private static Object resources1 = new Object();
private static Object resources2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (resources1) {
System.out.println(Thread.currentThread().getName() + " get resources1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting resources2");
synchronized (resources2) {
System.out.println(Thread.currentThread().getName() + "get resources2");
}
}
}, "A").start();
new Thread(() -> {
synchronized (resources2) {
System.out.println(Thread.currentThread().getName() + " get resources2");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting resources1");
synchronized (resources1) {
System.out.println(Thread.currentThread().getName() + "get resources1");
}
}
}, "B").start();
}
<<<A get resources1
<<<B get resources2
<<<A waiting resources2
<<<B waiting resources1
程序直接卡死,因为A在拥有了资源1的情况下去要资源2,B在拥有资源2的情况下去要资源1,谁都不让步,直接卡死
上面例子的代码风格:尽量别去锁一个类或者对象,去锁你要操作的共有资源就行了,在方法体里面创建的对象默认是私有的
学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件: 线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。
如何避免死锁呢
破坏以上一个条件即可
互斥条件无法破坏,你不可能去改变底层JVM的synchronized的实现原理
请求与保持条件:一次性锁上所有需要的资源
代码实现:
public static void main(String[] args) {
new Thread(() -> {
synchronized (resources1) {
synchronized (resources2) {
System.out.println(Thread.currentThread().getName() + " get resources1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting resources2");
System.out.println(Thread.currentThread().getName() + "get resources2");
}
}
}, "A").start();
new Thread(() -> {
synchronized (resources2) {
synchronized (resources1) {
System.out.println(Thread.currentThread().getName() + " get resources2");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting resources1");
System.out.println(Thread.currentThread().getName() + "get resources1");
}
}
}, "B").start();
}
之间最开始就锁上所有资源
不剥夺条件:如果申请不到想要的资源,就释放当前资源(估计要用lock锁来做)
循环等待条件:让所有线程都按照统一路线去申请资源
public static void main(String[] args) {
new Thread(() -> {
synchronized (resources1) {
System.out.println(Thread.currentThread().getName() + " get resources1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting resources2");
synchronized (resources2) {
System.out.println(Thread.currentThread().getName() + "get resources2");
}
}
}, "A").start();
new Thread(() -> {
synchronized (resources1) {
System.out.println(Thread.currentThread().getName() + " get resources1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting resources2");
synchronized (resources2) {
System.out.println(Thread.currentThread().getName() + "get resources2");
}
}
}, "B").start();
}
Sleep方法和wait方法的异同
- 主要区别:sleep没有释放锁,而wait释放了锁
- 都暂停了线程
- wait 通常被用于线程间交互 / 通信,sleep 通常被用于暂停执行
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。
既然调用start方法会执行run方法,为什么不直接调用run方法
新建一个Thread对象,线程进入新建状态,直到调用start方法,才会启动线程并且进入就绪状态,当线程被分配到了时间片的时候就可以执行run方法了。start会执行线程的相应准备工作,再去执行run方法,是真正的多线程。而直接调用run方法,相当于只是在主线程里面执行run方法里面的代码,并没有去新建一个线程。