1、JUC概述
1.1、进程与线程的概念
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
总结来说:
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程是资源分配的最小单位
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程时程序执行的最小单位
1.2、线程的六种状态
1))NEW:初始状态,线程被构建,但是还没有调用 start 方法;
2)RUNNABLED:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统一称为“运行中” ;
3)BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况 :
-
等待阻塞:运行的线程执行了 Thread.sleep 、wait()、 join() 等方法JVM 会把当前线程设置为等待状态,当 sleep 结束、join 线程终止或者线程被唤醒后,该线程从等待状态进入到阻塞状态,重新抢占锁后进行线程恢复;
-
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中
-
其他阻塞:发出了 I/O请求时,JVM 会把当前线程设置为阻塞状态,当 I/O处理完毕则线程恢复;
4)WAITING:等待状态,没有超时时间,要被其他线程或者有其它的中断操作;执行wait()、join()、LockSupport.park();
5)TIME_WAITING:超时等待状态,超时以后自动返回;
执行 Thread.sleep(long)、wait(long)、join(long)、LockSupport.park(long)、LockSupport.parkNanos(long)、LockSupport.parkUntil(long)
6)TERMINATED:终止状态,表示当前线程执行完毕 。
注意:
1)sleep、join、yield时并不释放对象锁资源,在wait操作时会释放对象资源,wait在被notify/notifyAll唤醒时,重新去抢夺获取对象锁资源。
2)sleep可以在任何地方使用,而wait,notify,notifyAll只能在同步控制方法或者同步控制块中使用。
3)调用obj.wait()会立即释放锁,以便其他线程可以执行notify(),但是notify()不会立刻立刻释放sycronized(obj)中的对象锁,必须要等notify()所在线程执行完sycronized(obj)同步块中的所有代码才会释放这把锁。然后供等待的线程来抢夺对象锁
1.3、sleep和wait方法的区别
- 来源:sleep()方法是Thread类的静态方法,而 wait()方法是object类的实例方法。这意味着所有Java对象都可以调用wait()方法,而只有 Thread类及其子类可以调用sleep()方法。
- 锁释放:当线程调用sleep()方法时,它不会释放已经持有的任何对象锁。因此,如果线程在调用sleep()之前获取了锁,其他线程将无法访问受该锁保护的资源,直到睡眠时间结束。而当线程调用wait()方法时,它会释放持有的对象锁,允许其他线程访问受锁保护的资源。
- 唤醒机制:sleep()方法在指定的时间(毫秒)后自动唤醒线程。而wait()方法需要依赖其他线程调用相同对象的
notify()或notifyAll()方法来唤醒等待的线程。如果没有其他线程调用这些方法,调用wait()的线程将一直等待下去。 - 使用场景:sleep()方法通常用于让线程暂停执行一段时间,以便其他线程执行或等待某些条件成熟。例如,在轮询某一资源时,可以让线程每隔一段时间检查一次资源状态。而wait()方法通常用于线程间的协作,一个线程在等待某个条件满足时调用wait()进入等待状态,而另一个线程在条件满足时调用notify()或notifyAll()来唤醒等待的线程。
举个例子,假设有两个线程A和B。线程A负责生产数据,线程B负责消费数据。当数据队列为空时,线程B需要等待线程A生产数据。这时,线程B可以调用wait()方法进入等待状态,并释放锁,以便线程A可以生产数据。当线程A生产完数据后,调用notify()或notifyAl1()方法唤醒线程B,线程B可以继续消费数据。
1.4、并发和并行
1、并行
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行,所以无论从微观还是从宏观来看,二者都是一起执行的。
2、并发
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
1.5、用户线程和守护线程
在Java中,线程分为两种类型:用户线程和守护线程。
守护线程是一种特殊的线程,它在后台默默地完成一些系统性的服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。这些线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,Java虚拟机也就退出了。守护线程并不会阻止Java虚拟机退出。
设置守护线程的方法是调用Thread对象的setDaemon(true)方法。需要注意的是,一定要在调用线程的starti)方法之前设置。这是一个简单的守护线程的例子:
public class MyThread {
public static void main(String[] args) {
//使用Lambda 表达式实现这个接口,创建 线程t1
Thread t1 = new Thread(() -> {
//判断是否是守护线程,(后台运行的)
System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon());
while (true) {
//主线程结束,程序还在运行,jvm 没停止
}
}, "t1");
// 如果把他设置为守护线程 ,主线程结束这个程序没有用户线程了,结束了
t1.setDaemon(false);
//启动线程
t1.start();
System.out.println(Thread.currentThread().getName() +"结束");
}
}
2、Lock接口
2.1、Synchronized
在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。
复习可以参考【Java并发编程之深入理解】Synchronized的使用_synchronize三种用法
2.2、Lock接口
Lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象 Condition 。
当在不同范围内发生锁定和解锁时,必须注意确保在锁定时执行的所有代码由 try-finally 或 try-catch 保护,以确保在必要时释放锁定。
Lock 实现提供了使用 synchronized 方法和语句的附加功能,通过提供非阻塞尝试来获取锁 tryLock(),尝试获取可被中断的锁 lockInterruptibly() ,以及尝试获取可以超时 tryLock(long, TimeUnit)
public class Ticket implements Runnable {
//票的数量
private int ticket = 100;
private Object obj = new Object();
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//synchronized (obj){//多个线程必须使用同一把锁.
try {
lock.lock();
if (ticket <= 0) {
//卖完了
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
// }
}
}
}
public class Demo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
2.3、lock和synchronized的差异
-
synchronized是java关键字,内置,而lock不是内置,是一个类,可以实现同步访问且比 synchronized中的方法更加丰富。
-
synchronized不会手动释放锁,而lock需手动释放锁(不解锁会出现死锁,需要在 finally 块中释放锁)
-
lock等待锁的线程会相应中断,而synchronized不会相应,只会一直等待
-
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
-
Lock 可以提高多个线程进行读操作的效率(当多个线程竞争的时候)锁会出现死锁,需要在 finally 块中释放锁)
-
lock等待锁的线程会相应中断,而synchronized不会相应,只会一直等待。
-
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
-
Lock 可以提高多个线程进行读操作的效率(当多个线程竞争的时候)
3、线程间通信
线程间通信有两种实现方法:
关键字 synchronized
与 wait()/notify()
这两个方法一起使用可以实现等待/通知模式
Lock
接口中的 newContition()
方法返回 Condition 对象,Condition 类也可以实现等待/通知模式
3.1、synchronized 实现案例
// 创建一个资源类
class Share{
// 设置临界资源
private int number = 0;
// 实现+1操作
public synchronized void incr() throws InterruptedException {
// 操作:判断、干活、通知
if (number != 0) {
// number不为0,等待
// wait 有一个特点,在哪里睡,就在哪里醒
this.wait();
}
number++;
System.out.print(Thread.currentThread().getName()+"::"+number);
// 唤醒其他线程
// 注意这里的通知是随机的,就是只能通知全部
this.notifyAll();
}
// 实现-1操作
public synchronized void decr() throws InterruptedException {
// 操作:判断、干活、通知
if (number != 1) {
// number不为0,等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
this.notifyAll();
}
}
public class InterThreadCommunication {
public static void main(String[] args) {
Share share = new Share();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
3.2 虚假唤醒问题
虚假唤醒主要出现在多线程中出现。
同样使用上述案例,现在有四个线程,分别为A,B,C,D,其中A,C线程做+1操作,B,D线程做-1操作,想要的结尾应该是A,C线程输出值为1,B,D线程输出值为0 。修改上述代码如下:
// 创建一个资源类
class Share{
// 设置临界资源
private int number = 0;
// 实现+1操作
public synchronized void incr() throws InterruptedException {
// 操作:判断、干活、通知
if (number != 0) {
// number不为0,等待
this.wait();
}
number++;
System.out.print(Thread.currentThread().getName()+"::"+number+"--->");
// 唤醒其他线程
this.notifyAll();
}
// 实现-1操作
public synchronized void decr() throws InterruptedException {
// 操作:判断、干活、通知
if (number != 1) {
// number不为0,等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
this.notifyAll();
}
}
public class InterThreadCommunication {
public static void main(String[] args) {
Share share = new Share();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
share.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
结果如下
所谓虚假唤醒,就是 wait()方法的一个特点,总结来说 wait() 方法使线程在哪里睡就在哪里醒。 这是什么意思呢?那就以上述代码为例。
当 A 进入临界区,BCD三个线程在 if 判断后进入 wait() 等待,当A线程完成操作,此时 number 值为1,notifyAll() 会随机唤醒一个线程。
现在C被唤醒,由于 wait() 方法使线程在哪里睡就在哪里醒,所以接下来C在执行时不会再通过 if 判断而是直接+1,此时 number 就是2了。从而导致最后输出的结果和我们预想的不一致。
按照 JDK1.8 文档的提示,将资源类的 incr() 方法和 decr() 方法中的if语句改为循环语句,修改代码如下:
// 创建一个资源类
class Share{
// 设置临界资源
private int number = 0;
// 实现+1操作
public synchronized void incr() throws InterruptedException {
// 操作:判断、干活、通知
while (number != 0) {
// number不为0,等待
// 哪里睡哪里起
this.wait();
}
number++;
System.out.print(Thread.currentThread().getName()+"::"+number+"--->");
// 唤醒其他线程
this.notifyAll();
}
// 实现-1操作
public synchronized void decr() throws InterruptedException {
// 操作:判断、干活、通知
while (number != 1) {
// number不为0,等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"::"+number);
this.notifyAll();
}
}
3.3、Lock实现
在 Lock 接口中,有一个 newCondition() 方法,该方法返回一个新 Condition 绑定到该实例 Lock 实例。
Condition 类中有 await() 和 signalAll() 等方法,和 synchronized 实现案例中的 wait() 和 notifyAll() 方法相同。所以通过 Lock 接口创建一个 Condition 对象,由该对象的方法进行等待和唤醒操作
class Share {
// 设置临界资源
private int number = 0;
// 创建一个Com
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 实现+1操作
public void incr() {
// 上锁
lock.lock();
try {
// 判断
while (number != 0) {
condition.await();
}
// 干活
number++;
System.out.print(Thread.currentThread().getName() + "::" + number + "--->");
// 通知
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 实现-1操作
public void decr() throws InterruptedException {
// 上锁
lock.lock();
try {
// 判断
while (number != 1) {
condition.await();
}
// 干活
number--;
System.out.println(Thread.currentThread().getName() + "::" + number);
// 通知
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
3.4、线程定制化通信
案例实现
案列:启动三个线程,按照如下要求:
AA打印5次,BB打印10次,CC打印15次,一共进行10轮
具体思路:
每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志 位的线程
创建一个可重入锁 private Lock lock = new ReentrantLock();
分别创建三个开锁通知 private Condition c1 = lock.newCondition();(他们能实现指定唤醒)
(注意)具体资源类中的A线程代码操作
上锁,(执行具体操作(判断、操作、通知),解锁)放于try、finally,具体代码如下
class Share{
private int flag = 1;
private Lock lock = new ReentrantLock();
// 创建三个Comdition对象,为了定向唤醒相乘
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
public void Aprint(int loop) {
//上锁
lock.lock();
try{
// 判断
while(flag!=1) {
c1.await();
}
// 干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
}
flag = 2; //修改标志位,定向唤醒 线程b
// 唤醒
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
public void Bprint(int loop) {
//上锁
lock.lock();
try{
// 判断
while(flag!=2) {
c2.await();
}
// 干活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
}
flag = 3; //修改标志位,定向唤醒 线程b
// 唤醒
c3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
public void Cprint(int loop) {
//上锁
lock.lock();
try{
// 判断
while(flag!=3) {
c3.await();
}
// 干活
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
}
flag = 1; //修改标志位,定向唤醒 线程b
// 唤醒
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
}
public class CustomInterThreadCommunication {
public static void main(String[] args) {
Share share = new Share();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
share.Aprint(i);
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
share.Bprint(i);
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
share.Cprint(i);
}
}
},"C").start();
}
}
注意点:
进程/线程同步有四个原则,都是为了禁止两个进程同时进入临界区。同步机制应该遵循以下原则
-
空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区
-
忙则等待:当已经有进程进入临界区的时候,其他试图进入临界区的进程必须等待
-
有限等待:对请求访问的进程,应保证能在有限时间内进入临界区
-
让权等待:当进程不能进入临界区的时候,应立即释放处理机,防止进程忙等待
很显然,该案例被称为单标志法。因为该案例设置一个公用整型变量flag,用于指示被允许进入临界区的进程编号。
若 flag =1,则允许 P1进程进入临界区;若 flag =2,则允许 P2 进程进入临界区;若 flag =3,则允许 P3 进程进入临界区
该算法可确保每次只允许一个进程进入临界区。但两个进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也无法进入临界区比如,若 P3 顺利进入临界区并从临界区离开,则此时临界区是空闲的,但 P1并没有进入临界区的打算,flag = 1 一直成立,P3 就无法再次进入临界区。违背了"空闲让进"原则,让资源利用不充分·
比如,将上述代码中的 main() 方法的A、B线程都只循环十次而C线程循环20次 ,但其实C线程在循环10次以后就不能访问 Share 资源了,因为 A 线程已经不再访问同时 flag 值不再改变了。