JUC基础知识(一)
一:线程、进程,并行、并发
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程是资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个
单元执行流。线程是程序执行的最小单位。
串行模式
串行表示所有任务都按先后顺序进行。串行意味着必须先装完一车柴才能
运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步
骤,才能进行下一个步骤。
串行是一次只能取得一个任务,并执行这个任务。
并行模式
并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模
式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列
的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上
则依赖于多核 CPU。
并发:同一时刻多个线程在访问同一个资源,多个线程对一个点
例子:春运抢票 电商秒杀…
并行:多项工作一起执行,之后再汇总
例子:泡方便面,电水壶烧水,一边撕调料倒入桶中
二:创建线程的两种方式
1、继承Thread类
//匿名内部类
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("继承Thread类创建线程");
}
};
thread.start();
2、实现Runnable接⼝
public static void main(String[] args) {
//匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("实现Runnable接口创建线程");
}
}).start();
new Thread(()->{
System.out.println("实现Runnable接口创建线程 : 使用Lambda表达式");
}).start();
}
三:为什么是 start 方法?
为什么是 start 方法,而不是直接调用 run 方法,这里结合源码来分析一下:
首先来看一下 Thread 类中的 run 方法:
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
/* What will be run. */
private Runnable target;
翻译一下上面的注释:
如果此线程是使用单独的,Runnable接口 run 对象,然后Runnable接口调用对象的 run 方法;否则,此方法不执行任何操作并返回。Thread 的子类应重写此方法。
Person类实现 run 方法来实现自己的业务逻辑,如果在 main 线程中调用 run 方法,相当于并没有开辟一个线程,run 方法只是一个普通的方法,那么就会把 run 方法执行完毕,才向下执行。
如果是调用 start 方法那么就会启动线程,Runnable接口就会调用执行对象的 run 方法。主线程不会阻塞, 会继续执行,主线程和子线程是交替执行。
来看一下 start 方法
我们发现其实 start 中真正调用的是 start0 方法;
看到这里就很清楚了,run 方法中只是实现了业务代码,真正要实现线程还是要靠 start -> start0
四:什么是JUC?
JUC就是 java.util .concurrent 工具包的简称。这是一个处理线程的工具包,JDK
1.5 开始出现的。
五:线程的状态
线程状态枚举类 Thread.State
六:管程
七:用户线程和守护线程
用户线程:也叫工作线程,一般就是我们用户自定的线程。
守护线程:指在程序运行的时候在后台提供一种通用服务的线程,守护线程是为用户线程服务的。
用户线程和守护线程的区别
二者其实基本上是一样的。唯一的区别在于JVM何时离开。
用户线程:当存在任何一个用户线程未离开时,JVM是不会离开的。
守护线程:如果只剩下守护线程未离开,JVM是可以离开的。
八:Synchronized
synchronized 是 Java 中的关键字,是一种同步锁。独占锁、悲观锁
它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}
括起来的代码,作用的对象是调用这个代码块的对象; - 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用
的对象是调用这个方法的对象;
▲虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定
义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方
法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这
个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上
synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方
法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,
子类的方法也就相当于同步了。
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的
所有对象; - 修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主
的对象是这个类的所有对象。
synchronized实现同步的基础:
Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
(1)对于普通同步方法,锁是当前实例对象。
(2)对于静态同步方法,锁是当前类的class对象。
(3)对于同步方法块,锁是synchonized括号里配置的对象。
一个典型的卖票案例:
class Ticket {
private int num = 50;
public synchronized void saleTicket(){
if (num > 0){
num--;
System.out.println(Thread.currentThread().getName() + ":卖出了一张票," + "还剩下" + num + "张票");
}
}
}
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0;i < 50;i++)
ticket.saleTicket();
},"A售票员").start();
new Thread(()->{
for (int i = 0;i < 50;i++)
ticket.saleTicket();
},"B售票员").start();
new Thread(()->{
for (int i = 0;i < 50;i++)
ticket.saleTicket();
},"C售票员").start();
}
}
九:Lock接口
Lock 和 synchronized 有以下几点不同:
- Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内
置的语言实现; - synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现
象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很
可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁; - Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用
synchronized 时,等待的线程会一直等待下去,不能够响应中断; - 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- Lock 可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源
非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于
synchronized。
采用 Lock 接口实现卖票案例:
十:虚假唤醒问题
场景:有两个线程。
一个线程:当前数值为0时,对当前数值加 1。
另一个线程:当前数值为1时,对当前数值减 1。
要求用线程间通信
class Share {
private int number = 0;
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
if (number != 0) {
condition.await();
}
//干活
number++;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知
condition.signalAll();
}finally {
//解锁
lock.unlock();
}
}
//-1
public void decr() throws InterruptedException {
lock.lock();
try {
if(number != 1) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+" :: "+number);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
显示结果:
根据题目要求,最终显示的结果应该都是 0 或 1 。很明显结果出错了,那么为什么会出错呢?
这就是发生了虚假唤醒的问题。
因为 wait 方法有一个特点是:在哪里睡就会在哪里醒
如果,某一个线程正好在 if 判断语句中等待,那么一旦他被唤醒,它就会跳过 if 判断语句,会直接执行下面的代码。
所以解决办法是把判断语句换成 while 语句,这样即使它在判断语句中被唤醒,它也会继续在 while 语句中进行判断。