文章内容为本人对尚硅谷周阳老师JUC教学视频学习过程中的笔记整理及个人理解、思考与拓展。部分学术用语转自网络。
小白初探JUC,抱着热诚的学习心态,希望能一步一步提升,欢迎大神批评指正。
一些基本所需理论
这里主要是放一些在学习过程中所需要了解的基本术语和部分相关的概念。
JUC
JUC是java.util .concurrent工具包的简称,是一个处理线程的工具包。根据其API文档,其核心包包括:
java.util.concurrent 并发包
java.util.concurrent.atomic 并发原子包
java.util.concurrent.locks 并发lock包
进程/线程
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。是操作系统动态执行的基本单元。在传统操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:通常在一个进程中可以包含若干线程。当然一个进程中也至少包含一个线程,否则这个进程也是没有意义的。线程可以利用进程所拥有的资源,在引入线程的操作系统之中,通常就是将进程作为分配资源的基本单位,线程作为独立运行和独立调度的基本单位。由于线程比进程更小,所以基本上不拥有系统资源,所以对它的调度所付出的开销也就小得多,能够更加高效的提升系统多个程序间并发执行的程度。
可以通俗的这样理解:
进程就相当于用电脑写代码的时候,同时开着idea,word,网易云音乐,微信以及QQ。这一个一个的应用程序可以视为一个一个的进程。
而此时在idea上有语法检查功能,word有容灾备份等同时起着,这些依附与各个应用程序中的功能都是一个一个进程中的线程。
并发/并行
并发:多线程在同一时间点,争取同一个资源。 可通俗理解为12306的抢票。
并行:指在同一时刻,有多条指令在多个处理器上同时执行。可通俗理解为泡面的时候同时在做其他事情。
高内聚/低耦合
爬一下百科的说法:
高内聚低耦合,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高,耦合度是否低。
内聚通常是指从功能角度衡量模块内的联系,一个好的内聚模块应该只做一件事。
耦合则是度量模块间的相互联系,耦合程度取决于模块间的接口复杂性、调用方式等。
线程状态
- NEW 新建
- RUNNABLE 就绪
- BLOCKED 阻塞
- RUNNING 运行
- WAITING :一个正在无限期等待另一个线程执行一个特别的动作的线程处于这一状态。(不见不散)
- TIMED_WAITING :一个正在限时等待另一个线程执行一个动作的线程处于这一状态。(过时不候)
- TERMINATED:结束
wait和sleep的区别
wait和sleep都会导致线程阻塞,但是也是存在区别的。
通俗理解:
wait是放开了手去“睡”,放开了手中的锁。(放权)
sleep是握紧手去“睡”,醒了手里还有锁。
synchronized关键字
synchronized关键字可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
多线程编程练习(1)
多线程编程的一个总结出来的模板是:在高内聚低耦合的前提之下,多线程操纵资源类。
(1)先写一个资源类
(2)所谓操作就是方法,但该方法需要由资源类自身携带,以实现高内聚的初衷。
通俗理解:
A、B两个人使用空调的制冷、制热功能。
A、B和空调之间就是一种低耦合的状态,甚至可以说是没有耦合的。
而制冷、制热方法就是高内聚于空调内部的两种功能。
例:三个售票员,卖出30张票。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//先写一个资源类
class Ticket{
private int number = 30;
private Lock lock = new ReentrantLock(); //可重入锁
public void saleTicket()
{
lock.lock();
try
{
//try里面放置想要锁起来的代码行
if(number > 0){
System.out.println(Thread.currentThread().getName() + "\t卖出第:"+(number--)+"\t还剩下:"+number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class SaleTicket {
public static void main(String[] args) throws Exception //main 一切程序的入口
{
Ticket ticket = new Ticket();
//这里使用Lambda表达式来执行匿名内部类
new Thread(() -> {for (int i = 0; i < 40; i++) ticket.saleTicket(); },"A").start();
new Thread(() -> {for (int i = 0; i < 40; i++) ticket.saleTicket(); },"B").start();
new Thread(() -> {for (int i = 0; i < 40; i++) ticket.saleTicket(); },"C").start();
}
}
这里使用了juc包中的lock包,并使用了里面的可重入锁来ReentrantLock进行实现独占锁的功能。
取代了之前使用synchronized关键字执行方法加锁的操作。
接口也是对象,也可以new,new接口这种语法现象就叫:匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40 ; i++) {
ticket.saleTicket();
}
}
});
只不过上面的整体代码使用了Lambda表达式来执行匿名内部类。
多线程编程练习(2)
该章节的练习内容为Lambda表达式。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。
@FunctionalInterface
interface Fun{
public int mv(int a,int b);
//@FunctionalInterface注释声明下的接口为函数式接口,只有且仅有一个方法
// public int add(int a,int b);
//其余具体能够实现的方法需要加default关键字修饰或者是静态方法(可声明多个)
default int add(int a,int b){
return a + b;
}
default int add2(int a,int b){
return a + b + 3;
}
public static int div(int a,int b){
return a / b;
}
public static int div2(int a,int b){
return a / b + 1;
}
}
public class IOPPractice {
public static void main(String[] args) {
Fun fun = (a,b) -> {
System.out.println("Lambda Express Func");
return a * b;
};
System.out.println(fun.mv(10,2));
System.out.println(fun.add(2,3));
System.out.println(fun.add2(2,3));
//静态方法的引用需要使用大类引用
System.out.println(Fun.div(10,2));
System.out.println(Fun.div2(10,2));
}
}
注意事项:
(1)@FunctionalInterface 函数式声明,在该注解的约束下:该接口中有且仅有一个抽象方法。该注解常用于Lambda表达式和方法引用上,用以进行编译级错误检查,加上该注解,当所写接口不符合函数式接口定义的时候,编译器就会报错。
(2)口诀:拷贝小括号,写死右箭头,落地大括号。
落地实现具体抽象方法的内容写在大括号里面。
(3)Java8之后,可以在接口里具体实现default或者static修饰的方法。default修饰的方法只能在接口中使用,为接口中的普通方法,可直接书写方法体。详见代码实例。
实际上,在Java8之后,接口既能有定义,也能有实现。功能亦大大丰富,面向接口编程的思想在这里有进一步的体现。
因此总结看来,面向对象编程逐渐衍生为一种面向函数式接口的编程。
多线程编程练习(3)
首先,基本思想仍然是:
高内聚低耦合的前提下,线程操纵资源类。
其次,要谈一下多线程的横向调度通信机制:生产者消费者模式。
爬一下:在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
通俗说来也就是:判断/干活/通知。
题目:
现有两个线程,可以操作初始值为0的一个变量,
实现一个线程对变量+1,一个线程对该变量-1,
实现交替,来回10轮,变量初始值为0。
class AirConditioner{ //资源类
private int number = 0;
public synchronized void increment() throws InterruptedException{
//1. 判断
if(number != 0)
{
//wait是使得持有该对象的线程将该对象的控制权交出,然后处于等待状态。
// 即当调用wait的时候会释放锁并处于等待状态
this.wait();
}
//2. 干活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3. 通知
//通知消费者可以消费了
//notify()是通知某个等待该对象控制权的线程继续运行
//notifyAll()是通知所有等待这个对象控制权的线程可以继续运行了(唤醒所有等待的线程)
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException{
//1. 判断
if(number == 0)
{
this.wait();
}
//2. 干活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3. 通知
//通知消费者可以消费了
this.notifyAll();
}
}
public class ThreadWaitNotifyDemo {
public static void main(String[] args) {
AirConditioner airConditioner = new AirConditioner();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
//可以视为生产者,一开始为0,先生产
airConditioner.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
//可以视为消费者
airConditioner.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
wait()是使得持有该对象的线程将该对象的控制权交出,然后处于等待状态。
notify()是通知某个等待该对象控制权的线程继续运行。
notifyAll()是通知所有等待这个对象控制权的线程可以继续运行了(唤醒所有等待的线程)。
现在将题目修改下:
四个线程,两线程为++,两线程为–
一开始的想法,就是将上述的代码复制一遍,增加两个线程叫“C”,“D”即可:
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
//可以视为生产者,一开始为0,先生产
airConditioner.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
//可以视为消费者
airConditioner.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
//可以视为生产者,一开始为0,先生产
airConditioner.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
//可以视为消费者
airConditioner.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
但是运行结果却出现如下情况:
A 1
B 0
A 1
B 0
A 1
B 0
A 1
C 2
A 3
B 2
B 1
B 0
A 1
C 2
A 3
B 2
B 1
B 0
A 1
C 2
A 3
D 2
D 1
D 0
B -1
D -2
D -3
D -4
D -5
D -6
D -7
D -8
A -7
C -6
这里就是要注意:
在多线程的交互中,必须要防止多线程的虚假唤醒(spurious wakeups)。
在上面卖票那个题目中,线程之间不存在交互,先到先得。
在当前例题程序中,有消费需求了再生产,这样的情况便是交互。也即存在wait(),notify(),notifyAll()方法的线程是交互的。
问题便出在:
多线程的判断中,不能用if,只能用while。
之前的情况是两个线程,不是加就是减,因此不会出现问题。
但当出现四个线程的时候,就会出现虚假唤醒的情况。多个生产者线程堵在下述代码行的this.wait()这里。光凭if判断可能导致长时间堵塞在这里的两个++线程获得对象控制权之后直接向下运行,造成虚假唤醒的情况。但是使用while进行判断,则不会导致如此情况。
public synchronized void increment() throws InterruptedException{
while (number != 0)
{
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
this.notifyAll();
}