前言:
本笔记仅做学习与复习使用,不存在刻意抄袭。
------------------------------------------------------------------------------------------------------------
给各位学友强烈推荐《遇见狂神说》他的整套Java学习路线使我获益匪浅!!!
如果你也是狂神的小迷弟,可以加我好友一起探讨学习。
JUC简述
JUC是什么?
JUC就是java.util.concurrent包,俗称java并发包,是Java开发工程师学习并发的时候需要掌握的内容。
为什么要学习JUC?
我认为起码有以下几点:
- 一个初级java程序员进阶学习的必经之路;
- 让我们深刻了解并发的思想,为以后的微服务,分布式打好基础;
- 能在脑海中构建一个计算机底层处理问题的大概模型,这对研究算法是很有帮助的;
- 最后一点,也是最现实的一点,这是面试高频考点,学习JUC并发编程,除了能丰富知识,还能提高今后面对面试的底气。
接下来就是《狂神说Java》JUC并发编程的课程笔记和我自己的总结了,或许,没有看视频的学友看着会觉得有些混乱或者逻辑错误。这里还是强调一点,各位最好是配合视频学习,笔记只当作初学或者今后复习就好。
一、学习方式
源码+官方文档(面试高频)
这里简述一下juc在jdk帮助文档的位置(jdk帮助文档请自行百度下载):
我们打开jdk帮助文档
点击左侧目录:
二、回忆多线程的学习内容
2.1、实现多线程的三种方式
- 继承Thread类(其实Thread类根本上也实现了Runnable接口)
我们按住ctrl加鼠标左键,点击Thread :
跳转到:
可以看到其实现了Runnable接口。
这里简单写个小例子回忆一下继承Thread类如何使用。
package com.example.demo1.demo02;
/**
* @author liar
*/
public class TestThread {
public static void main(String[] args) {
Eat eat = new Eat();
Out out = new Out();
eat.start();
out.start();
}
}
class Eat extends Thread{
@Override
public void run() {
for (int i = 0; i < 500; i++) {
System.out.println( "我在吃饭呢");
}
}
}
class Out extends Thread{
@Override
public void run() {
for (int i = 0; i < 500; i++) {
System.out.println("我在拉屎呢");
}
}
}
其核心是重写run方法;其运行结果是:
- 实现Runnable接口
package com.example.demo1.demo02;
/**
* @author liar
*/
public class TestRunnable {
public static void main(String[] args) {
Who who = new Who();
FBI fbi = new FBI();
Thread who1 = new Thread(who,"who");
Thread fbi1 = new Thread(fbi,"FBI");
who1.start();
fbi1.start();
}
}
class Who implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("Who are you ?");
}
}
}
class FBI implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("Open the door!!!FBI!!!");
}
}
}
运行结果也如预期。
- 实现Callable接口
package com.example.demo1.demo02;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TestCallable {
public static void main(String[] args) {
Hit hit = new Hit();
Connection connection = new Connection();
FutureTask futureTask1 = new FutureTask(hit);
FutureTask futureTask2 = new FutureTask(connection);
Thread t1 = new Thread(futureTask1);
Thread t2 = new Thread(futureTask2);
t1.start();
t2.start();
try {
//get()方法的返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Object o1 = futureTask1.get();
Object o2 = futureTask2.get();
System.out.println(o1);
System.out.println(o2);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class Hit implements Callable{
@Override
public Object call() throws Exception {
for (int i = 0; i < 500; i++) {
System.out.println("怎么样!!你打我啊!!!");
}
return "hit";
}
}
class Connection implements Callable{
@Override
public Object call() throws Exception {
for (int i = 0; i < 500; i++) {
System.out.println("小伙子,出来混,是要讲人脉的!!!");
}
return "connection";
}
}
其运行结果:
其与前两者的区别是,可以通过FutureTask获取返回值的。
2.2、总结
普通的线程代码:继承Thread类
Runnable接口:没有返回值,效率相比于Callable接口较低
2.3、线程和进程
关于线程和进程,如果不能用一句话说出来,说明你掌握的还不够扎实!
进程:
- 一个正在执行的程序
- 例如:QQ.exe;是运行程序的集合
- 一个进程可以包含多个线程且至少包含一个线程
Java真的能开启线程吗?
- 不能开启线程
- 分析Thread类的原码了解,最后该类还是调用了本地方法(native)
- Java的底层是c++
2.4、并发和并行
并发:(多线程操作同一资源)
- CPU一核,模拟出来多线程;天下武功唯快不破,快速交替
并行:(多个人同时走路)
- CPU多核,多个线程同时执行
并发编程的本质:充分利用CPU的资源
2.5、线程有几个状态
public enum State {
NEW,//新生
RUNNABLE,//运行
BLOCKED,//阻塞
WAITING,//等待,一直等待
TIMED_WAITING,//超时等待,过期不候
TERMINATED;//终止
}
2.6、wait和sleep的区别
来自不同的类:
- wait==>Object
- sleep==>Thread
关于锁的释放
- wait:会释放锁
- sleep:“抱着锁睡觉,不会释放锁”
使用的范围是不同的
- wait:必须在同步代码块中使用
- sleep:任何地方
是否需要捕获异常
- wait:不需要捕获异常(什么叫中断异常?)
- sleep:需要捕获异常(存在超时等待的情况)
三、Lock锁(重点)
3.1、回忆"synchronized"
作用:
保证线程安全,例如当多个线程访问统一资源时,会发生数据紊乱问题;
怎么理解:
队列+锁
实例:没有synchronized修饰的方法
package com.example.demo1.demo02;
/**
* @author liar
*/
public class UnsafeTicket {
public static void main(String[] args) {
BuyTicket ticket = new BuyTicket();
new Thread(ticket,"倒霉的我").start();
new Thread(ticket,"幸运的你们").start();
new Thread(ticket,"可恶的黄牛党").start();
}
}
class BuyTicket implements Runnable{
private int tickets = 10;
private boolean flag = true;
@Override
public void run() {
while (flag){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
buy();
}
}
//在方法名前加上"synchronized",那么该方法就变成了同步方法了
//!!!锁的是"this"!!!
//'synchronized'默认锁的是this
private synchronized void buy(){
if (tickets<=0){
flag=false;
//这个return放在这里,如果符合if条件,那么返回出去,不执行下面的sout语句
return;
}
System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
}
}
我们看一下运行效果:
实例:有synchronized修饰的方法
package com.example.demo1.demo02;
/**
* @author liar
*/
public class UnsafeTicket {
public static void main(String[] args) {
BuyTicket ticket = new BuyTicket();
new Thread(ticket,"倒霉的我").start();
new Thread(ticket,"幸运的你们").start();
new Thread(ticket,"可恶的黄牛党").start();
}
}
class BuyTicket implements Runnable{
private int tickets = 10;
private boolean flag = true;
@Override
public void run() {
while (flag){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
buy();
}
}
//在方法名前加上"synchronized",那么该方法就变成了同步方法了
//!!!锁的是"this"!!!
//'synchronized'默认锁的是this
private synchronized void buy(){
if (tickets<=0){
flag=false;
//这个return放在这里,如果符合if条件,那么返回出去,不执行下面的sout语句
return;
}
System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
}
}
这下,就是线程安全的了。
3.2、Lock接口(Lock锁)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//基本的卖票例子
/*
真正的多线程开发,公司中的开发,降低耦合性
线程就是一个单独的资源类,没有任何附属操作
1、属性、方法
*/
public class SaleTicketDemo2 {
public static void main(String[] args) {
//并发:多线程操作同一个类,把资源类丢入线程
Ticket2 ticket = new Ticket2();
//@FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{代码}
new Thread( () ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"A").start();
new Thread( () ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"B").start();
new Thread( () ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"C").start();
}
}
//Lock三部曲
//1.new ReentrantLock();
//2. lock.lock(); 加锁
//3.finally --> lock.unlock(); 解锁
class Ticket2{
//属性方法
private int number = 50;
Lock lock = new ReentrantLock();
public void sale(){
lock.lock(); //加锁
try{
//业务代码
if(number > 0){
System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余" + number);
}
}catch (Exception e){
e.printStackTrace();
}finally {
//解锁
lock.unlock();
}
}
}
3.3、Synchronized和Lock的区别
1、Synchronized内置的Java关键字,Lock是一个Java类
2、Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
3、Synchronized是全自动的,会自动释放锁,Lock必须要手动释放锁,如果不释放锁,死锁
4、Synchronized 线程1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去
5、Synchronized可重入锁,不可以中断,非公平;Lock可重入锁,可以判断锁,非公平(可以自己设置)
6、Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码
3.5、思考
锁是什么?如何判断锁的是谁?
四、生产者和消费者问题
面试:单例模式、排序算法、生产者和消费者问题、死锁
4.1、synchronized版
package com.my.pc;
/*
* 线程之间的通信问题:生产者和消费者问题(等待唤醒,通知唤醒)
*
* 线程交替执行:
* 生产者和消费者操作同一个变量
* 生产者:+1
* 消费者:-1
* */
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"消费者").start();
}
}
//资源类
/*
* 编写资源类的思路:
* 判断等待、业务、通知
* */
class Data{
private int number = 0;
public synchronized void increment() throws InterruptedException {
if (number!=0){
//生产者等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//唤醒消费者进程进行“消费”
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (number==0){
//消费者等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//唤醒生产者进程进行“生产”
this.notifyAll();
}
}
这样会出现问题!!–虚假唤醒
场景:当出现多个生产者和消费者时,会出现数据紊乱?
原因:就是这个if语句只判断一次:当消费者线程阻塞,来唤醒生产者线程时,多个生产者线程都会被唤醒,但是只有第二个生产者线程执行了if判断,然后第二个生产者线程阻塞(基于if语句的机理),因为第一个生产者线程能够正常执行,而剩余的其他生产者线程,则会执行if语句后面的方法(“意思就是后面的生产者线程在if判断前面生产者线程时,利用了if语句的bug捡了漏”)
(1)if判断流水线状态为空时,线程被阻塞,这时if判断就完成了,线程被唤醒后直接执行线程剩余操作
(2)while判断流水线状态为空时,线程被阻塞,这时的while循环没有完成,线程被唤醒后会先进行while判断
解决方法:
把if语句改为while语句
package com.my.pc;
/*
* 线程之间的通信问题:生产者和消费者问题(等待唤醒,通知唤醒)
*
* 线程交替执行:
* 生产者和消费者操作同一个变量
* 生产者:+1
* 消费者:-1
* */
public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.increment();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"消费者").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"C").start();
new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
data.decrement();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"D").start();
}
}
//资源类
/*
* 编写资源类的思路:
* 判断等待、业务、通知
* */
class Data{
private int number = 0;
public synchronized void increment() throws Interrupted