什么是JUC
JUC就是java.util的这三个工具包
都是关于线程同步工具包
学习之前的回顾
回顾之前的基础知识:线程实现的三种方式
线程我们知道有三种实现方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
分别演示一下三种实现线程实现的方式
继承的方式实现创建线程
package jucTest2;
/**
* @author 雷雨
* @date 2020/12/3 19:51
* 使用继承Thread的方式实现一个线程
*/
public class MyThread1 extends Thread{
public static void main(String[] args) {
Thread date = new Data1();
date.setName("线程A");
date.start();
Thread data2 = new Data1();
data2.setName("线程B");
data2.start();
}
}
class Data1 extends Thread{
public void run(){
System.out.println("雷雨向您问好");
System.out.println(Thread.currentThread().getName());
}
}
实现Runnable的方式实现一个多线程
package jucTest2;
/**
* @author 雷雨
* @date 2020/12/3 20:00
*/
public class MyThread2 {
public static void main(String[] args) {
Data2 data2 = new Data2();
//该线程执行十次
new Thread(()->{
for (int i = 0; i < 10; i++) {
data2.run();
}
}).start();
}
}
class Data2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println("雷雨向您问好");
}
}
使用实现Callable的方式来实现线程
package jucTest2;
import java.util.concurrent.Callable;
/**
* @author 雷雨
* @date 2020/12/3 20:03
* 使用Callable的方式实现线程
* Callable是可以得到返回值的
*/
public class MyThread3 {
public static void main(String[] args) {
Data3 data3 = new Data3();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
try {
Integer call = data3.call();
System.out.println(call);
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
class Data3 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("当前线程的名字是"+Thread.currentThread().getName());
return 123;
}
}
可以发现这里出现了线程同步的问题,那么之后接着会介绍如何解决线程安全问题。
注意:在实际的开发过程中,我们要面向OOP的编程思想,也就是将有线程需求的资源类单独实现,而对于线程的操作放在其他类中,如上面三种方式演示的那样,资源类仅仅是实现了线程的功能的,而具体的操作逻辑放在了主操作类中。
三种线程的三种方式的区别:
- 继承Thread的方式实现多线程,那么要实现线程的类就不能继承其他类了(Java单继承的特性),其他两种方式还可以继承其他类。
- Callable的方式实现多线程比实现Runnable的方式多一个返回值,并且call()可以抛出异常
- 访问当前的线程的线程名,如果继承的是Thread类,那么可以直接使用this.getName,如果使用实现Runnable接口或者实现Callable的方式,那么就要使用Thread.getcurrentThread().getName()。
进程和线程
检测自己是否对于一个知识或者技术很熟悉,那么就看自己能不能一口气将这个技术讲清楚。
什么是进程,什么是线程?
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
举一个形象的栗子:
比如我们经常使用的QQ就是一个进程,但是QQ有很多功能,发送消息和打视频电话都是独立的线程。
进程和线程有什么区别和联系?
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
-
线程的划分尺度小于进程,使得多线程程序的并发性高。
-
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
-
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。**但是线程不能够独立执行,**必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
Java真的能开启线程吗?
Java其实不能自己开启一个线程,而是通过Java的一个本地方式start0()来开启线程。
并发和并行
并行:多个线程真正的同时运行。(比如CPU有多个核就可以处理多条线程)
并发:多个线程高速的轮询来执行线程,从外部看起来好像多个线程同时执行,其实是多个线程执行时间片的时间就让出CPU给别的线程执行机会。
public static void main(String[] args) { System.out.println(Runtime.getRuntime().availableProcessors());
}
wait和sleep的区别?
- wait是Object类的方法,而sleep是Thread的方法。
- wait()方法会释放锁资源,而sleep()方法不会释放锁资源。
- 使用的区域不同,wait()方法必须在同步代码块中,而sleep()可以在任何地方谁。
- wait()方法不需要捕获异常;sleep()必须捕获异常。
线程同步
实现线程同步的方式,常见的方式就是加锁。
实现线程同步方式一:synchronized
先说最常见的加synchronized锁的方式实现线程同步。
假想的题目要求:
两个线程
一个线程实现对数+1操作
另一个线程实现对数-1操作
保证加线程必须是num为0是执行+1操作
num不为0时执行-1操作
package jucTest2;
/**
* @author 雷雨
* @date 2020/12/3 20:42
*/
public class SynchronizedDemo1 {
public static void main(String[] args) {
Data4 data4 = new Data4();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data4.pulsOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程A执行的是加法").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data4.deOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程B执行的是减法").start();
}
}
class Data4{
//初始化资源num为0
private int num = 0;
//+1方法
public synchronized void pulsOne() throws InterruptedException {
if (num != 0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"当前num操作后为 "+num);
this.notifyAll();
}
//-1方法
public synchronized void deOne() throws InterruptedException {
if (num == 0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"当前num操作后为 "+num);
this.notifyAll();
}
}
执行结果如下:两个线程交替执行
这里演示的是两个线程,一个消费者线程,一个生产者线程,我们能够正确执行的操作。但是如果是多个生产者和多个消费者同时操作资源会不会出现问题呢?
package jucTest2;
/**
* @author 雷雨
* @date 2020/12/3 20:42
*/
public class SynchronizedDemo1 {
public static void main(String[] args) {
Data4 data4 = new Data4();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data4.pulsOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程A执行的是加法").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data4.pulsOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程C执行的是加法").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data4.deOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程B执行的是减法").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data4.deOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程D执行的是减法").start();
}
}
class Data4{
//初始化资源num为0
private int num = 0;
//+1方法
public synchronized void pulsOne() throws InterruptedException {
if (num != 0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"当前num操作后为 "+num);
this.notifyAll();
}
//-1方法
public synchronized void deOne() throws InterruptedException {
if (num == 0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"当前num操作后为 "+num);
this.notifyAll();
}
}
观察结果:发现出现了问题,出现了异常数2,发现在执行了一次了一次加操作后可能出现线程又执行了一次加操作,这是我们不想看到的情况的。
导致的原因就是发生了虚假唤醒
详细解释上述的虚假唤醒的问题的原因:wait方法导致的虚假唤醒
wait方法的执行其实可以分为三步:
- 释放锁,并阻塞
- 等待条件cond发生
- 获取通知后,竞争获取锁继续执行
我们上述代码中出现2的原因详解:
当加线程A要执行的时候发现了num!=0,然后会进入到wait方法进行等待(进入了阻塞队列),这时释放锁资源,然后加线程C也可能获取了锁,而加线程C也不满足条件,也进入了wait方法并释放锁资源,然后这时减线程B获取了锁资源,(因为num!=0),因此减线程正常执行,减线程执行之后,可能加线程A竞争之后获取了锁资源,并执行,而线程C在之后也获取了锁资源,而只是唤醒之后不会在判断条件num!=0是否满足,而是直接执行后续的代码。
这样的操作就导致了虚假唤醒。
怎么避免虚假唤醒?
使用while来判断cond条件就能避免虚假唤醒的问题。
原因是:while循环本身就是一个自旋的操作,会循环判断条件是否满足。
将上述代码改造一下:
package jucTest2;
/**
* @author 雷雨
* @date 2020/12/3 20:42
*/
public class SynchronizedDemo1 {
public static void main(String[] args) {
Data4 data4 = new Data4();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data4.pulsOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程A执行的是加法").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data4.pulsOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程C执行的是加法").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data4.deOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程B执行的是减法").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data4.deOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程D执行的是减法").start();
}
}
class Data4{
//初始化资源num为0
private int num = 0;
//+1方法
public synchronized void pulsOne() throws InterruptedException {
while (num != 0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"当前num操作后为 "+num);
this.notifyAll();
}
//-1方法
public synchronized void deOne() throws InterruptedException {
while (num == 0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"当前num操作后为 "+num);
this.notifyAll();
}
}
实现实现同步方式二:Lock锁
package jucTest2;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 雷雨
* @date 2020/12/3 20:42
* 使用Lock锁的方式实现线程同步
*/
public class LockDemo1 {
public static void main(String[] args) {
Data5 data5= new Data5();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data5.pulsOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程A执行的是加法").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data5.pulsOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程C执行的是加法").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data5.deOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程B执行的是减法").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data5.deOne();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程D执行的是减法").start();
}
}
class Data5{
//初始化资源num为0
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//+1方法
public void pulsOne() throws InterruptedException {
lock.lock();
while (num != 0){
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName()+"当前num操作后为 "+num);
condition.signalAll();
lock.unlock();
}
//-1方法
public void deOne() throws InterruptedException {
lock.lock();
while (num == 0){
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName()+"当前num操作后为 "+num);
condition.signalAll();
lock.unlock();
}
}
小结:
不论是传统的synchronized的方式实现线程的同步还是使用Lock锁的方式实现线程同步,都需要经历如下过程
- 加锁,判断等待(判断是否需要等待)
- 执行业务代码
- 唤醒其他线程,解锁
使用synchronized和Lock锁的区别?
- synchronized不能精确的判断锁的状态,而Lock锁是能够精确判断锁的状态的。synchronized不需要程序员自身对于锁状态进行改变,而Lock锁,需要手动的加锁和解锁。
- synchronized实现线程同步的话,直接使用Object类的wait()方法和notifyAll()方法,而Lock要实现线程同步时,必须借助condition监视器。