1. JUC是什么?
在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类。此包包括了几个小的、已标准化的可扩展框架,并提供一些功能实用的类,没有这些类,一些功能会很难实现或实现起来冗长乏味。
参照JDK文档:
2. 进程和线程
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
线程多了好还是少好?
合理最好: 一般和CPU核心数差不错最好
1、如果程序执行的是耗时操作 线程多一些好
2、如果程序执行的是cpu计算型操作,线程越少越好 cpu调度线程时 切换上下文也需要时间
生活实例:
- 使用QQ,查看进程一定有一个QQ.exe的进程,我可以用qq和A文字聊天,和B视频聊天,给C传文件,给D发一段语言,QQ支持录入信息的搜索。
- 大四的时候写论文,用word写论文,同时用QQ音乐放音乐,同时用QQ聊天,多个进程。
- word如没有保存,停电关机,再通电后打开word可以恢复之前未保存的文档,word也会检查你的拼写,两个线程:容灾备份,语法检查
3. 并行和并发
串行:
资源:服务器中的一个文件 或者是某个接口
并发:同一时刻多个线程在访问同一个资源,多个线程对一个点
例子:小米13pro今天上午10点,限量抢购
春运抢票
电商秒杀...
并行:多项工作一起执行,之后再汇总
例子:泡方便面,电水壶烧水,一边撕调料倒入桶中
4. wait/sleep的区别
功能都是当前线程暂停,有什么区别?
wait:放开手去睡,放开手里的锁
sleep:握紧手去睡,醒了手里还有锁
wait是Object的方法,sleep是thread的方法
wait从等待时所在的行继续向后执行
5. 创建线程回顾
创建线程常用两种方式:
- 继承Thread:java是单继承,资源宝贵,要用接口方式
- 实现Runnable接口
5.1 继承Thread抽象类
public class MyThread extends Thread
new MyThread().start();
5.2 实现Runnable接口的方式
① 新建类实现runnable接口。这种方法会新增类,有更好的方法
class MyRunnable implements Runnable//新建类实现runnable接口
new Thread(new MyRunnable(), name).start() // 使用Rannable实现类创建进程,name是线程名
② 匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
// 调用资源方法,完成业务逻辑
}
}, "your thread name").start();
6. lambda表达式
之前说了Runnable接口的两种实现方式,其实还有第三种:
1. 创建类实现Runnable接口
2. 编写匿名内部类实现Runnable接口
3. lambda表达式:这种方法代码更简洁精炼
new Thread(() -> {
}, "your thread name").start();
6.1 什么是lambda
Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
Lambda 表达式在Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或剪头操作符。它将 Lambda 分为两个部分:
- 左侧:指定了 Lambda 表达式需要的所有参数
- 右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能
6.2案例
在一个方法中调用接口中的方法:传统写法
interface Foo {
public int add(int x, int y);
}
public class LambdaDemo {
public static void main(String[] args) {
Foo foo = new Foo() {
@Override
public int add(int x, int y) {
return x + y;
}
};
System.out.println(foo.add(10, 20));
}
}
接下来,要用lambda表达式改造。其实是改造main方法
public static void main(String[] args) {
Foo foo = (int x, int y)->{
return x + y;
};
System.out.println(foo.add(10, 20));
}
改造口诀:拷贝小括号(),写死右箭头->,落地大括号{...}
思考:如果Foo接口有多个方法,还能使用lambda表达式吗?
6.4 函数式接口
lambda表达式,必须是函数式接口,必须只有一个方法,如果接口只有一个方法java默认它为函数式接口。为了正确使用Lambda表达式,需要给接口加个注解:@FunctionalInterface。如有两个方法,立刻报错。
Runnable接口为什么可以用lambda表达式?
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
发现Runnable接口上有一个注解:@FunctionalInterface
并且该接口只有一个方法:run()方法
其实,函数式接口必须只有一个方法,这个描述并不准确,它还允许有default方法和静态方法。
例如,在Foo接口中,又添加了sub方法和mul方法:
interface Foo {
public int add(int x, int y); // 抽象方法
default int sub(int x, int y){ // default方法
return x - y;
}
public static int mul(int x, int y){ // 静态方法
return x * y;
}
}
public class LambdaDemo {
public static void main(String[] args) {
Foo foo = (int x, int y)->{ // lambda表达式实现抽象方法
return x + y;
};
System.out.println(foo.add(10, 20)); // 调用抽象方法
System.out.println(foo.sub(30, 15)); // 调用default方法
System.out.println(Foo.mul(10, 50)); // 通过Foo调用静态方法
}
}
6.4 小结
lambda表达式实现接口的前提是:有且只有一个抽象方法,可以选择@FunctionalInterface注解增强函数式接口定义
改造口诀:拷贝小括号(形参列表),写死右箭头 ->,落地大括号 {方法实现}
7. synchronized回顾
(1)多线程编程模板上
1. 线程 操作 资源类
2. 高内聚低耦合
(2)实现步骤
1. 创建资源类
2. 资源类里创建同步方法、同步代码块
3. 多线程调用
(4)例子:卖票程序
class Ticket {
private Integer number = 20;
public synchronized void sale(){
if (number <= 0) {
System.out.println("票已售罄!!!");
return;
}
try {
System.out.println(Thread.currentThread().getName() + "开始买票,当前票数:" + number);
Thread.sleep(200);
System.out.println(Thread.currentThread().getName() + "买票结束,剩余票数:" + --number);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 在main方法中创建多线程方法,测试卖票业务
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}, "AAA").start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}, "BBB").start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}, "CCC").start();
}
}
8. synchronized的8锁问题
看下面这段儿代码,回答后面的8个问题:
class Phone {
public synchronized void sendSMS() throws Exception {
//TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
public class Lock_8 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "AA").start();
Thread.sleep(100);
new Thread(() -> {
try {
phone.sendEmail();
//phone.getHello();
//phone2.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}, "BB").start();
}
}
多线程的8个问题:
1. 标准访问,先打印短信还是邮件
2. 停4秒在短信方法内,先打印短信还是邮件
3. 普通的hello方法,是先打短信还是hello
4. 现在有两部手机,先打印短信还是邮件
5. 两个静态同步方法,1部手机,先打印短信还是邮件
6. 两个静态同步方法,2部手机,先打印短信还是邮件
7. 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
8. 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
答案:
1. 标准访问,先打印短信还是邮件: 先短信
2. 停4秒在短信方法内,先打印短信还是邮件:先短信(两个方法锁的对象都是调用方法的phone)
3. 普通的hello方法,是先打短信还是hello:先hello(hello方法未加锁)
4. 现在有两部手机,先打印短信还是邮件:先邮件(两个线程各自用各自的锁)
5. 两个静态同步方法,1部手机,先打印短信还是邮件:先短信
6. 两个静态同步方法,2部手机,先打印短信还是邮件:先短信(静态方法调用锁定的对象和谁调用无关,锁的一定是类对象)
7. 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件:先邮件
8. 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件:先邮件
一个线程会等待另一个线程释放锁以后执行,那么它俩争抢的锁一定是同一个
总结:
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
1. 对于普通同步方法,锁是当前实例对象。
2. 对于静态同步方法,锁是当前类的Class对象。
3. 对于同步方法块,锁是Synchonized括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
也就是说:
- 如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁;可是不同实例对象的非静态同步方法因为用的是不同对象的锁,所以毋须等待其他实例对象的非静态同步方法释放锁,就可以获取自己的锁。
- 所有的静态同步方法用的是同一把锁——类对象本身。不管是不是同一个实例对象,只要是一个类的对象,一旦一个静态同步方法获取锁之后,其他对象的静态同步方法,都必须等待该方法释放锁之后,才能获取锁。
- 而静态同步方法(Class对象锁)与非静态同步方法(实例对象锁)之间是不会有竞态条件的。