1. 什么是JUC
- java.util.concurrent包名的简写,是关于并发编程的API。
- 与JUC相关的有三个包:java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks。
- java.util表示工具包,包最开始是为了屏蔽同名,但是更多的是考虑分类。
重点讲解这四个包
- 解决一些业务的时候,会用普通的线程代码:Thread实现,但是这样效率并不高。
- Runnable:没有返回值,效率先比于Callable相对较低,功能也没有Callable强大。企业中用Runnable比较少。
- 我们还需了解一下Lock
2. 线程和进程
进程线程是什么?
进程:就是一个应用程序,如QQ.exe ,music.exe程序
线程:进程的一个执行单位,负责当前进程中程序的执行,一个进程至少有一个线程。
- 一个进程中可能包含多个线程,但是至少包含一个线程。
开了一个进程 :在Typora中,写字,自动保存等操作是线程负责的。
- 对于Java而言:通过Thread、Runnable、Callable、线程池实现。
在java中一个应用程序至少有几个线程?
2个, main线程(用户线程)、GC线程(守护线程)。
Java中Native关键字的作用
native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。
Java是跨平台的,既然是跨平台就,所以付出的代价是牺牲了对底层的控制,而Java要是先对底层的控制就要一些其他语言的帮助。
JNI是Java本机接口(Java Native Interface),是一个本机编程接口,它是Java软件开发工具箱(java Software Development Kit,SDK)的一部分。JNI允许Java代码使用以其他语言编写的代码和代码库。
Invocation API(JNI的一部分)可以用来将Java虚拟机(JVM)嵌入到本机应用程序中,从而允许程序员从本机代码内部调用Java代码。
Java真的可以开启线程吗?
答案是开不了的!
关键在最后一句代码native修饰的方法,调用了底层的C++,因为Java是运行在JVM之上的无法直接操作硬件。
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented.
*/
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack
*/
}
}
}
// 本地方法,底层的C++ ,因为Java 无法直接操作硬件
private native void start0();
线程有6个状态:
public enum State {
NEW, //尚未启动的线程,处于此状态。
RUNNABLE, // 在Java虚拟机中执行的线程,处于此状态。
BLOCKED, //阻塞:被阻塞等待监视器锁定的线程,处于此状态。
WAITING, // 等待: 正在等待 执行特定动作的线程,处于此状态。
TIMED_WAITING, // 延时等待:正在等待另一个线程执行动作达到指定等待时间的线程,处于此状态。
TERMINATED; //终止,结束:已退出的线程,处于此状态。
}
并发、并行
并发:
同一个CPU执行多个任务,按细分的时间片交替执行;多个线程操作一个资源类,快速交替的过程。
并发编程的主要目的,充分利用CPU的资源,提高性能
为什么这样做能提高效率?
这相当于2个小孩一共吃两碗菜,但只有一双筷子,必须两个孩子都吃完才能去睡觉,他们就轮流着用那双筷子吃,但是不论是轮流着吃还是孩子吃完后,另一个孩子再吃,同时吃完的时间都相同
并行:
在多个CPU上同时处理多个任务,也就是CPU多核,多个线程可以同时运行。例如:线程池。
总结:交替叫并发,同时叫并行。
举个例子:
你吃饭,吃到一半,电话来了,3种情况:
1.先吃完饭,再接电话(单线程)
2.先接电话再吃 (交替、并发)
3.边吃边接电话 (并行)
通过代码获取本机的核数
public class Test1 {
public static void main(String[] args) {
// 获取cpu的核数
// CPU 密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
wait/sleep()的区别
-
类
- wait属于Object类
- sleep属于Thread类
-
是否会释放锁
-
wait释放
调用wait()方法,会使当前线程放弃锁,线程暂停执行,该线程进入对象的等待池。
只有调用对象的notify()方法或者notifyall()方法才能唤醒等待池中的线程进入等锁池,而只有获取到了锁,才能进到就绪状态。
-
sleep不释放(可以想象成抱着睡觉哈哈哈哈哈哈嗝~)
将执行的机会让给其他线程,即把使用CPU的机会给其他线程,但会对该对象的锁仍然保持,sleep时间结束后该线程自动恢复,即进入就绪状态。
-
-
使用的范围
- wait、notify、notifyAll只能使用在同步方法或者同步代码中
- sleep可以使用到任意地方(可以在任何地方睡觉)
-
异常
- wait不需要捕获异常
- sleep必须捕获异常
扩展
-
sleep()、 suspend() 和 resume() 、yield() 阻塞时都不会释放占用的锁(如果占用了的话)。
wait(),notify()方法阻塞时要释放占用的锁。这是最核心的区别,这一核心区别导致了一系列细节上的区别(如下几条区别)。
-
wait()、notify()方法属于Object。也就是锁所有对象都拥有这一对方法,锁任何对象都有的。调用对象的wait()方法导致阻塞,当然也要有解除阻塞的方法,也就是notify()方法。notify()方法只是唤醒因调用该对象的 wait() 方法而阻塞的线程,且在这之中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
-
wait(),notify()方法必须在synchronized方法或块中调用。
原因(有锁才能释放锁):
只有在synchronized 方法或块中,当前线程才占有锁,才有锁可以释放。同样的道理,因为使对象调用方法,所以调用这方法的对象,该对象上的锁必须为当前线程所拥有,这样才有锁释放。对象的锁=当前线程的锁
所以,这一对方法调用必须放置在这样的 synchronized 方法或块中,这样才能使该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现illegalMonitorStateException异常。
-
为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用?
这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁。
-
wait() 和notify()与操作系统进程间的通信机制结合
注意:
关于wait()和notify():
-
notify方法只能唤醒由调用wait方法进入阻塞的线程,且唤醒哪个线程我们是无法预期的,所以要避免虚假唤醒的情况
-
notifyAll()方法也可以起到和notify()一样的作用,调用notifyAll()方法可以把因该对象调用wait()方法而进入阻塞的所有线程,一次性全部解除阻塞。当然只有获得锁才可以进如可运行状态。
说到阻塞,就不得不谈一谈suspend()方法和wait()方法的调用都可能产生死锁。遗憾的是,Java并不在语言级别上支持死锁的避免。
3. Lock
传统的Synchronized
public class SaleTicketTest {
public static void main(String[] args) {
//资源类
// 并发:多线程操作同一个资源类, 把资源类丢入线程
SaleTicket saleTicket = new SaleTicket();
//线程A
new Thread(() -> {
for (int i = 0; i < 40; i++) {
saleTicket.saleTicket();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
saleTicket.saleTicket();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
saleTicket.saleTicket();
}
}, "C").start();
}
}
//售票对象
class SaleTicket {
private int number = 50;
// synchronized 本质: 队列,锁
public synchronized void saleTicket() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第" + (number--) + "还剩" + number + "张票");
}
}
}
使用 juc.locks 包下的类操作 Lock 锁 + Lambda 表达式
Lock是一个接口,其实现类ReentranLock(可重入锁,后面章节会详细讲解)。
由上可知:
公平锁:十分公平,可以先来后到!
非公平锁:十分不公平:可以插队 (默认)
Lock的编程模型:
1、创建锁;
2、加锁:lock.lock();
3、在try/catch里面写业务代码;
4、在try/catch的finally里面解锁lock.unlock();
注意:
Lock的加锁解锁要配对出现,即有一个lock.lock()加锁,就必须在finally里面有一个lock.unlock(),出现两个lock.lock()加锁,在finally里面也必须有两个lock.unlock()。
完整的代码如下:
public class SaleTicketTest {
//此处代码省略
}
//售票对象
class SaleTicket {
private int number = 50;
//锁lock
private Lock lock=new ReentrantLock();
public void saleTicket() {
//加锁
lock.lock();
try{
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第" + (number--) + "还剩" + number + "张票");
}
}catch (Exception e){
e.printStackTrace();
}finally {
//解锁
lock.unlock();
}
}
}
synchronized和lock区别(自动挡、手动挡)
-
synchronized :Java内置的。
Lock :是一个Java 类;
-
synchronized:无法判断是否获取锁。
Lock:可以判断是否获得锁(通过isLocked()方法);
-
synchronized:锁会自动释放!
Lock:需要手动在 finally 释放锁,如果不释放锁,就会死锁,Lock的加锁解锁要配对出现;
-
synchronized:线程1阻塞,线程2永久等待下去。
Lock:可以 lock.tryLock(); // 尝试获取锁,如果尝试获取不到锁,可以结束等待;
-
synchronized:可重入,不可中断,非公平的。
Lock:可重入、可以判断是否获得锁(通过isLocked()方法)、非公平(也可设置公平)
注意:可重入就是说某个线程已经获得某个锁,可以多次获取相同的锁而不会出现死锁。
-
Synchronized:适合锁少量的代码同步问题,
Lock:适合锁大量的同步代码
锁是什么,如何判断锁的是谁??!!
4. 生产者和消费者的问题
面试的:单例模式、排序算法、生产者和消费者、死锁
生产者和消费者问题 Synchronized 版
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for(int i=0;i<10;i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for(int i=0;i<10;i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"B").start();
}
}
class Data{
//数字 资源类
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
if(number!=0){
//等待操作
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程 我+1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if(number==0){
//等待操作
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程 我-1完毕了
this.notifyAll();
}
}
问题存在,A线程B线程,现在如果我有四个线程A B C D!
问题如下:
简单说就是if判断只会判断一次
虚假唤醒:
当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功,比如说买货,如果商品本来没有货物,突然进了一件商品,这是所有的线程都被唤醒了 ,但是只能一个人买,所以其他人都是假唤醒,获取不到对象的锁
if改为while就可以了!!!
生产者和消费者-新版JUC写法
await、signal 替换 wait、notify
通过Lock 找到 Condition
代码实现:
通过Lock实例调用lock()和unlock()方法加锁
通过Lock实例.newConditional()创建Conditional实例
Conditional实例.await//等待
Conditional实例.signal//通知
package com.juc.study.lockdemo;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName:
* @PackageName: com.juc.study.lockdemo
* @author: youjp
* @create: 2020-04-14 15:25
* @description: TDOO 生产者消费模型:判断、干活、通知 //新版写法
* @Version: 1.0
*/
public class B {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.increment();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.decrement();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.increment();
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.decrement();
}
}, "D").start();
}
}
//属性、方法
class Data2 {
private int num = 0;
//定义锁
private Lock lock=new ReentrantLock();
private Condition condition=lock.newCondition();
//+1操作
public void increment() {
//加锁
lock.lock();
try {
//判断
while (num > 0) {
condition.await(); //等待
}
//干活
num++;
System.out.println(Thread.currentThread().getName() + "线程加操作\t" + num);
condition.signalAll();//通知
} catch (Exception e) {
e.printStackTrace();
} finally {
//解锁
lock.unlock();
}
}
//-1 操作
public void decrement() {
//加锁
lock.lock();
try {
//判断
while (num == 0) {
condition.await();//等待
}
//干活
num--;
System.out.println(Thread.currentThread().getName() + "线程减操作\t" + num);
condition.signalAll();//通知
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();// 解锁
}
}
}
任何一个新的技术,绝对不是仅仅只是覆盖了原来的技术,优势和补充!
以上4个进程,唤醒是随机的,即进程的执行时随机的,不是有序进行的,那怎么能让进程有序执行呢。接下来我们讨论进程间如何精确通知访问。
控制线程精确通知顺序访问 Condition
/**
* @ClassName:
* @PackageName: com.juc.study.lockdemo
* @author: youjp
* @create: 2020-04-15 14:19
* @description: TODO 精确线程调用A->B->C->A
* @Version: 1.0
*/
public class C {
public static void main(String[] args) {
Data3 da = new Data3();
new Thread(() -> {
for (