Java是一门广泛流行的需求量大,入门门槛低的语言,非常适合开发人员学习。

Java入门

Java的特点

  • 面向对象编程
  • 支持分布式计算
  • 可跨平台(由JVM负责翻译成可在不同操作系统上执行的程序)

JDK和JRE的区别
JRE是Java程序的运行时环境,要在计算机上运行使用Java语言编写的程序,JRE是必不可少的。JRE包含了Java虚拟机。
JDK是Java开发工具包,包含了JRE和开发Java程序所必需的基础类库。对于Java开发人员来说,是必不可少的

Java环境变量配置

1
2
3
JAVA_HOME = <JDK安装路径>
CLASSPATH = .;%JAVA_HOME%\lib
PATH = ;%JAVA_HOME%\bin

Java基础命令

1
2
javac HelloWorld.java   //将java源文件编译成字节码文件(.class)
java HelloWorld //使用JVM运行字节码文件

编译Java程序时,文件名必须和文件中唯一的public类名一致

Java中需要注意的几个关键字

关键字 描述
synchronized 实现线程同步
this 表示当前实例
super 表示当前类的父类
throw 异常
throws 在方法中主动抛出一个异常

在掌握Java的基础“拳法”后,应该注意修炼“内功”。同样一套拳法,在不同人身上的效果也是不同的

栈内存和堆内存

栈内存:存放变量和基本数据类型值(栈内存中只能存储数值,字符可以转换为数值)

堆内存:存放引用数据类型值

20230111162000

Java多线程

进程和线程

进程和线程的概念

进程一般来讲就是计算机正在运行的一个独立的应用程序,比如QQ进程

进程是动态的,只有在应用程序运行时才会产生!

线程是组成进程的基本单位,一个进程是由一个或多个线程组成的,一个线程通常用来完成应用程序的一个特定的功能

进程与线程概念之间的界限不是确定的,有时一个进程可以有多个子进程,比如我们在使用IDEA工具开发SpringBoot应用时,启动的SpringBoot应用就可以看做IDEA的一个子进程,它里面又包含了读取配置文件、连接数据库、启动Tomcat等多个子线程

20230108171134

进程和线程的区别

进程拥有独立的内存空间,每个进程(QQ,微信)所占用的内存空间都是独立的,互不干扰

属于同一个进程的多个线程共享同一块内存空间(每个线程的执行是独立的),且线程不能脱离进程独自执行

并发和并行

并发:(单核)多个线程在同一时间段内交替访问同一个共享资源,因为CPU速度太快,看起来像是多个线程在同时运行

并行:(多核)多个线程在同一时间点执行,它们占用不同的CPU,相互之间没有影响,也不会争夺资源

线程的状态

线程共有5种状态,在特定情况下,可以相互切换

  1. 创建状态:实例化一个Thread对象,还未启动时
  2. 就绪状态:调用start()方法完成启动,进入线程池等待CPU调度
  3. 运行状态:线程对象在一段时间内获得CPU资源,开始执行任务
  4. 阻塞状态:正在运行的线程暂停执行任务,并释放其所占用的CPU资源。解除阻塞状态的线程不能直接进入运行状态,而是重新回到就绪状态
  5. 终止状态:线程执行完毕或因异常终止

20230108203351

20230108205425

什么是多线程

多线程是指在一个进程中,多个线程同时并发执行。

注意这里所说的多个线程同时并发执行并不是真正地在同一时刻去执行多个线程,而是在一段时间内,多个线程交替执行,占用CPU资源。

在一个类的普通main()方法中,无论它有多少行代码,调用了多少个方法,它都只是一个主线程。如下图所示:

20230108175508

多线程一定是“交替”的,而不是“顺序”的

多线程可以充分利用CPU资源,是从“软件”层面提升程序性能的一种主要方式

20230108165737

Java中的多线程

在Java中使用线程的两种方式:
方式一 继承Thread类

  1. 创建自定义类继承Thread类,该类就成了一个线程类
  2. 重写run()方法,定义该线程需要完成的功能
  3. 在主线程中实例化一个线程对象,调用其start()方法启动一个子线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyThread extends Thread {
@Override
public void run(){
//====在此处定义任务逻辑====
for(int i=0;i<1000;i++){
System.out.println("-----MyThread-----");
}
}
}

public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); //开启一个线程

}
}

只有通过线程的start()方法来开启一个线程,该线程才能真正地成为一个线程去执行,而不能直接调用线程的run()方法,这会使线程执行变成普通的方法调用,不具备抢占CPU的能力。
当某个线程获得了CPU的调度,会自动执行其run()方法

上面的程序有两个线程哦,一个main()主线程,一个myThread子线程

方式二 实现Runnable接口(建议)

  1. 创建自定义类并实现Runnable接口
  2. 实现接口中的run()方法,在其中编写该线程要执行的任务代码
  3. 创建一个Thread实例,传入任务的具体实现,启动线程即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyRunnable implements Runnable {
@Override
public void run(){
//====在此处定义任务逻辑====
for(int i=0;i<1000;i++){
System.out.println("-----MyThread-----")
}
}
}

public class TestThread {
public static void main(String[] args) {
MyThread myThread = new Thread(new MyRunnable());
myThread.start(); //开启一个线程

}
}

在实际开发中,推荐使用第二种方式,由于Java不支持多继承,因此实现一个接口比继承一个类能使类间的耦合更低,进而提高代码重用率

线程和任务的关系
线程是任务的载体,只有线程可以去抢占CPU资源,启动(start)线程后会自动执行(run)定义在其中的任务,这好比是学生和作业的关系。任务不能脱离线程独自去执行。

线程调度(状态转换)

线程休眠
通过Thread类的sleep()静态方法可以让一个线程暂缓执行。该方法接收一个毫秒值(ms)表示线程休眠的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyThread extends Thread {
@Override
public void run(){
for(int i=0;i<10;i++){
if(i==5){
try {
this.sleep(3000); //线程休眠3秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(i+"-----MyThread-----");
}
}
}


public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}

如何让主线程main()方法休眠?

1
2
3
4
5
public class TestSleep {
public static void main(String[] args){
Thread.sleep(3000); //休眠3秒
}
}

Thread类的sleep()是一个静态方法,既可以通过类的实例调用,也可以通过类名.sleep()调用

线程合并
线程合并是指将某个线程加入到当前线程中,合并为一个线程。线程合并后这两个子线程将会变为顺序执行。

线程甲和线程乙,如果甲在一个时间点调用线程乙的join()方法(把线程乙合并到线程甲中),那么当前时间点之后CPU资源将由线程乙来独占,线程甲暂时进入阻塞状态,直到线程乙执行完毕(空参)或到达join()方法指定时间(ms)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MyRunnable implements Runnable{
@Override
public void run(){
for(int i=0;i<10;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("-----MyThread-----"+i);
}
}
}

public class TestThread {

public static void main(String[] args) {
Thread myThread = new Thread(new MyRunnable());
myThread.start(); //开启一个线程

for(int i=0;i<20;i++){
if(i == 10){
try {
myThread.join(3000); //加入一个子线程,在3秒内完全占用CPU资源
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("main"+i);
}

}
}

看一下输出结果,在三秒内,主线程让加入进来的MyThread子线程完全独占CPU资源

1
2
3
4
5
main9
-----MyThread-----0
-----MyThread-----1
-----MyThread-----2
main10

线程礼让
线程礼让是指在某个特定的时间点,让线程暂停抢占CPU资源,暂时进入阻塞状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MyThread extends Thread {

@Override
public void run() {
for(int i=0;i<100;i++){
if(i == 90){
yield(); //进行一次礼让
}
System.out.println(Thread.currentThread().getName()+"======"+i);
}
}
}

public class MyThread2 extends Thread {

@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"+++++"+i);
}
}
}

public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("子线程1");

MyThread2 myThread2 = new MyThread2();
myThread2.setName("子线程2");

myThread.start();
myThread2.start();

}
}

看一下执行结果,子线程1在i=90这个时间节点,做了一次礼让行为,把CPU资源让给子线程占用

1
2
3
4
5
6
子线程1======89
子线程2+++++97
子线程2+++++98
子线程2+++++99
子线程1======90
子线程1======91

Thread.currentThread().getName()用来获取当前线程的名称,如果没有自行设置,系统会为每个线程分配Thread-0、Thread-1这样的默认名称。

线程中断

可以造成线程停止运行(不是暂停)的场景有

  • 线程执行完毕
  • 线程在执行过程中出现了异常
  • 线程在执行过程中发生了线程中断

在Java中实现线程中断的方法

方法 描述
void stop() 强制停止当前线程。(不建议使用)
void interrupt() 中断当前线程
boolean isInterrupted() 判断当前线程是否已经中断执行(当一个线程还没有启动时返回false)

当一个线程对象处于不同的状态时,相应的中断机制也是不同的

1
2
3
4
5
6
7
8
9
public class TestThread {
public static void main(String[] args) {
Thread thread = new Thread();
System.out.println(thread.getState()); //NEW
thread.interrupt();
System.out.println(thread.isInterrupted()); //false

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestThread2 {
public static void main(String[] args) {

Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("MyThread====="+i);
}
}
});
thread.start();
System.out.println(thread.getState()); //Runnable
thread.interrupt(); //中断线程
System.out.println(thread.isInterrupted()); //判断一个运行的线程是否已中断
System.out.println(thread.getState()); //BLOCKED


}
}

线程同步

Java中允许多个线程在同一时间段内并行访问同一数据,为了应对可能导致的数据不一致问题,需要用到线程同步

先看一个统计网站访问人数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Account implements Runnable{
private static int num;

@Override
public void run() {

num++;

try {
//线程休眠时,其他线程再访问同一数据可能带来数据错误
Thread.currentThread().sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访问");
}
}

public class TestAccount {
public static void main(String[] args) {

Account account = new Account();

Thread t1 = new Thread(account,"张三");
Thread t2 = new Thread(account,"李四");
t1.start();
t2.start();
}
}

// 执行结果
// 张三是当前的第2位访问
// 李四是当前的第2位访问

20230111134252

执行逻辑:张三和李四两个线程并发访问,张三执行num++后开始休眠,期间李四执行了num++也开始休眠,然后张三李四依次打印结果num都是2,而不是想象中的1和2,这就是多线程共享数据时可能带来的错误

表示访问人数的num必须是一个static修饰的类变量,以实现不同Account实例之间的数据共享

使用线程同步解决数据冲突

每个Java对象都有一个内置锁,这个内置锁会保护对象中使用synchronized关键字修饰的方法。当一个线程正在执行这个方法时,其他的线程不能调用这个方法,必须等待前一个线程结束调用。

synchronized修饰的方法好比一个带锁的厕所门,每次只允许一个线程访问,调用过程中会把门上锁,直到调用完毕。synchronized牛逼👏

我们再来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class SynchronizedTest {

public static void main(String[] args) {

for (int i=0;i<3;i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest.test();
}
});
thread.start();
}
}

public static void test(){
System.out.println("start...");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end...");

}

}

// 执行结果
// start...
// start...
// start...
// end...
// end...
// end...

为了解决这个问题,需要给目标方法test(可能多个线程同时访问)加上synchronized关键字。执行结果将变为:

1
2
3
4
5
6
// start...
// end...
// start...
// end...
// start...
// end...

synchronized修饰的实例方法和类方法的区别

类方法由所有类的实例共享,实例方法每个实例都有一个。使用synchronized修饰的类方法是一把锁+一个厕所;synchronized修饰的实例方法是多把锁+多个厕所,多个实例之间互不影响

20230111144010

20230111143915

给实例方法添加synchronized关键字,并不能实现线程同步。把上面的test()方法修改为实例方法并通过实例调用,会发现synchronized“失效”!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class SynchronizedTest2 {

public static void main(String[] args) {

for (int i=0;i<3;i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest2 synchronizedTest2 = new SynchronizedTest2();
synchronizedTest2.test();
}
});
thread.start();
}
}

public synchronized void test(){
System.out.println("start...");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end...");
}
}

// 执行结果仍为:
// start...
// start...
// start...
// end...
// end...
// end...

test()实例方法的synchronized是独立的,不会对其他实例造成影响。

🤔在统计网站访问量的例子中,我们是给实例方法run()上的锁,为何也实现了线程同步呢?这岂不是矛盾了吗?

其实,线程同步的本质是锁定多个线程所共享的资源。如果该资源在类中唯一,不会改变,就可以实现线程同步。

在统计网站访问量的例子中,run()方法操作的数据num是用static修饰的类变量,synchronized修饰的其实是num类变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 统计网站访问量
public class Account implements Runnable{
private static int num;

@Override
public synchronized void run() {
num++;

try {
Thread.currentThread().sleep(1); //线程休眠,可能带来数据错误
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访问");
}
}

// 执行结果
// 张三是当前的第1位访问
// 李四是当前的第2位访问

如何判断线程是否同步?

找到关键点,锁定的资源(可以是类、对象、方法、变量)在内存中是一份还是多份。如果是唯一的,就能够实现线程的同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class SynchronizedTest2 {

public static void main(String[] args) {

for (int i=0;i<3;i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest2 synchronizedTest2 = new SynchronizedTest2();
synchronizedTest2.test();
}
});
thread.start();
}
}

public void test(){

// synchronized可以锁定整个类
synchronized (SynchronizedTest2.class) {
System.out.println("start...");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end...");
}

}
}

// 执行结果
// start...
// end...
// start...
// end...
// start...
// end...

线程安全的单例模式

单例模式:一个类最多只有一个实例对象(无论new多少次)

如何实现单例模式:

  1. 将类的构造器私有(不能随便new)
  2. 定义一个public类方法getInstanse(),返回实例对象(供外部调用获取)
  3. 定义类对应的static成员变量,在getInstanse()方法内部定义处理逻辑,只在类的static成员变量为null时创建实例对象
  4. 考虑到多线程情况下的线程安全问题,在getInstanse()方法上添加synchronized关键字

单线程下的单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SingletonDemo {

private static SingletonDemo singletonDemo;

private SingletonDemo() {
System.out.println("创建了SingletonDemo对象...");
}

public static SingletonDemo getInstance(){
if (singletonDemo == null){
singletonDemo = new SingletonDemo();
}
return singletonDemo;
}

}

public class TestSingleton {
public static void main(String[] args) {

SingletonDemo s = SingletonDemo.getInstance();
SingletonDemo s2 = SingletonDemo.getInstance();
System.out.println(s == s2); //true,两个变量的内存地址指向同一实例对象

}
}

多线程下的单例模式(线程安全的单例模式)

其实只要在获取类的实例对象的getInstance()方法上添加synchronized关键字(上锁)就可以了,保证同一时刻只有一个线程能够调用getInstance()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class SingletonDemo2 {

private static SingletonDemo2 singletonDemo;

private SingletonDemo2() {
System.out.println("创建了SingletonDemo对象...");
}

// synchronized保证线程同步
public synchronized static SingletonDemo2 getInstance(){
if (singletonDemo == null){
singletonDemo = new SingletonDemo2();
}
return singletonDemo;
}

}

public class TestSynchronized2 {
public static void main(String[] args) {

// 线程1
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo2 s = SingletonDemo2.getInstance();
}
}).start();

// 线程2
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo2 s2 = SingletonDemo2.getInstance();
}
}).start();

}
}

优化后的线程安全的单例模式

对上面的代码进一步优化,如果直接在方法上应用synchronized关键字,那么线程调用时会把整个方法锁起来,当方法中还有其他业务处理逻辑时,其他线程必须等待该线程运行完毕再去调用方法,然后才能执行方法中的其他业务,效率不高。

如果synchronized锁定的是多个线程共享的数据(同一个对象),那么线程就是安全的!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static SingletonDemo2 getInstance(){
if (singletonDemo == null) {
synchronized (SingletonDemo2.class) {
if (singletonDemo == null) {
singletonDemo = new SingletonDemo2();
}
}
}

//其他业务逻辑,不受锁的影响,多个线程可以并发执行
//... ...

return singletonDemo;
}

使用 volatile 保证单例模式的绝对线程安全

工作内存中的内容是主内存中内容的副本,主内存中的数据对线程不可见,线程只对工作内存操作(这是为了保证主内存中数据的正确性,在中间加了一层工作内存)

20230111173434

这也带来了一个问题:可能导致线程不安全,SingletonDemo2 实例化两次。(在线程1创建一个对象并释放锁后,工作内存还未向主内存输出数据时,线程2又从主内存中复制了一份SingletonDemo2的副本,而此时SingletonDemo2还为null,线程2又重新创建了一个SingletonDemo2实例对象)

20230111173941

完美解决线程安全:

1
private volatile static SingletonDemo2 singletonDemo;

volatile:使主内存中的数据对线程可见。当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

使用lambda表达式实现Runnable接口简化开发(JDK1.8以上)

因为Runnable是一个函数式接口,我们可以直接使用lambda表达式去实现它,简化开发

1
2
3
4
5
new Thread(()->{
for (int i=0;i<10;i++){
System.out.println("===MyThread===");
}
}).start();

甚至,我们可以直接写作

1
new Thread(()->{ for(int i=0;i<10;i++) System.out.println("===MyThread==="); }).start();

死锁

线程同步(不能争抢synchronized资源)可能带来死锁的问题。两个线程都需要两个同样资源,但都只拿到一个,等待对方执行完成,但是它们都无法继续执行。

20230111183628

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 1.资源类
public class Resources {
}

//2.模拟两个线程
public class MyRunnable implements Runnable{

//线程号
protected int num;

//static修饰保证资源在MyRunnable中唯一,锁是有效的
private static Resources r1 = new Resources();
private static Resources r2 = new Resources();


@Override
public void run() {
if (num == 1){
System.out.println(Thread.currentThread().getName()+"拿到了一个资源,正在等待另一个资源");
synchronized (r1){
try {
//这里是为了保证另一个线程可以获得其中一个资源
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (r2){
System.out.println(Thread.currentThread().getName()+"拿到了所有资源,可以继续执行");
}
}
}

if (num == 2){
System.out.println(Thread.currentThread().getName()+"拿到了一个资源,正在等待另一个资源");
synchronized (r2){
try {
//这里是为了保证另一个线程可以获得其中一个资源
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (r1){
System.out.println(Thread.currentThread().getName()+"拿到了所有资源,可以继续执行");
}
}
}

}
}

// 3. 启动这两个线程
public class ReadLockTest {

public static void main(String[] args) {

MyRunnable myRunnable1 = new MyRunnable();
myRunnable1.num = 1;

MyRunnable myRunnable2 = new MyRunnable();
myRunnable2.num = 2;

new Thread(myRunnable1,"张三").start();
new Thread(myRunnable2,"李四").start();


}
}

// 执行结果
// 张三拿到了一个资源,正在等待另一个资源
// 李四拿到了一个资源,正在等待另一个资源
// =====发生死锁,程序仍会一直运行下去=====

想要解除死锁,可以不让多个线程并发访问同一资源。一个线程执行完毕,另一个线程才可以运行

1
2
3
4
5
6
7
8
9
10
new Thread(myRunnable1,"张三").start();

try {
//确保线程1有足够的时间执行完毕,释放两个资源,解除死锁
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

new Thread(myRunnable2,"李四").start();

生产者消费者模式

Object类提供了wait()方法和notify()方法

方法 描述
wait() 访问当前资源的线程暂停执行
notify() 唤醒一个线程

多线程并发买票

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class TestSale {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.sale();
}
},"窗口A").start();

new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.sale();
}
},"窗口B").start();

}
}

class Ticket {
private Integer saleNum = 0; //卖出的票数
private Integer lastNum = 30; //剩余票数、总票数

public synchronized void sale(){
if (lastNum > 0) {
lastNum--;
saleNum++;

try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

if (lastNum > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + saleNum + "张票,剩余票数" + lastNum);
} else {
System.out.println(Thread.currentThread().getName() + "卖出了第" + saleNum + "张票,所有票都卖完了!");
}
}
}
}

// 窗口A卖出了第27张票,剩余票数3
// 窗口A卖出了第28张票,剩余票数2
// 窗口B卖出了第29张票,剩余票数1
// 窗口B卖出了第30张票,所有票都卖完了!

注意点

Java程序默认的线程数有两个:main()主线程GC垃圾回收器

Java本身是无法开启线程的,start()方法需要调用Java本地方法start0()(使用C++编写的动态函数库)去操作计算机底层开启一个线程

20230114133103

Java并发编程

并发是为了在硬件一定的条件下,充分利用计算机资源

什么是高并发

高并发是指我们设计的程序,可以支持海量的任务在同一时间段内交替执行。或者说在同一时间段内处理很多的任务

高并发的标准:

  1. QPS:每秒响应的HTTP请求数量
  2. 吞吐量:单位时间内处理的请求数,由QPS和并发数来决定
  3. 平均响应时间:系统对一个请求做出响应的平均时间
  4. 并发用户数:最多同时承载正常使用系统的用户数

QPS = 并发数/平均响应时间

如何实现高并发

互联网分布式系统架构设计,提高系统并发能力的两种方式:垂直扩展与水平扩展

垂直扩展:提升单机处理能力(提升计算机的硬件设备,增加CPU核数,升级网卡,升级内存等);提升单机的架构性能(使用Cache缓存,使用异步请求增加单服务吞吐量,使用NoSQL提升数据库访问能力)

水平扩展:通过增加机器数量来提升效率

20230114131813

集群和分布式(水平扩展)
集群:多个人做同一件事情。(一个厨师搞不定,多雇几个厨师一起炒菜)
分布式:一件事情拆分成多个步骤,由不同的人去完成。(一个厨师搞不定,给厨师雇几个助手,厨师只管炒菜,把洗菜和切菜交给助手去完成)

Nginx反向代理(代理服务器)(集群方式)

20230112210530

JUC

JUC(java.util.concurrent)是JDK中专门用来处理Java并发编程的工具包

Callable+FutureTask实现多线程

在java.util.concurrent中提供了Callable接口和FutureTask类(Runnable的实现类),用于实现多线程的并发执行

20230114134835

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ExecutionException;

public class TestCallable {

public static void main(String[] args) {

FutureTask<String> futureTask = new FutureTask<String>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();

try {
String s = futureTask.get(); //获取返回值
System.out.println(s);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}

class MyCallable implements Callable<String> {

@Override
public String call() throws Exception {
System.out.println("callable is running...");
return "返回了一个字符串";
}
}

//class MyRunnable implements Runnable {
// @Override
// public void run() {
//
// }
//}

Callable接口与Runnable接口的区别

  1. call()方法有返回值,run()方法没有
  2. call()方法抛出一个异常,run()方法没有
  3. Callable实现类不能直接放到Thread中,需要借助Runnable的实现类FutureTask

sleep()和wait()的区别
sleep()是让当前线程休眠;wait()是让访问当前对象或资源的那个线程休眠,同时释放锁

TimeUnit实现线程休眠

1
2
3
4
5
6
try {
//休眠2秒
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

ReentrantLock实现线程同步

使用synchronized关键字可以实现线程同步,还有一种方式也可以实现线程同步。

Lock是一个接口,也可以实现线程的同步。ReentrantLock(可重用锁)是Lock最常用的一个实现类

回顾统计网站访问量的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestAccount {
public static void main(String[] args) {
Account account = new Account();
new Thread(account,"线程A").start();
new Thread(account,"线程B").start();
}
}

class Account implements Runnable {
private int num;
@Override
public synchronized void run() {
num++;
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
}
}

使用Lock接口代替synchronized关键字实现线程同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestAccount2 {
public static void main(String[] args) {
Account account = new Account();
new Thread(account,"线程A").start();
new Thread(account,"线程B").start();
}
}

class Account2 implements Runnable {
private int num;
private Lock mylock = new ReentrantLock(); //声明一个可重用锁
@Override
public void run() {
mylock.lock(); //上锁
num++;
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
mylock.unlock(); //上锁后一定记得要释放锁
}
}

// 执行结果
// 线程A是当前的第1位访客
// 线程B是当前的第2位访客

ReentrantLock的高级用法

方法 描述
lock() 用于上锁
lockInterruptibly() 可中断的锁
unlock() 用于释放锁,Lock不会自动释放锁
tryLock(long time,TimeUnit unit) 让当前线程在指定时间内尝试去获得锁,返回是否能获得锁
isHeldByCurrentThread() 判断锁是否被当前线程占用
  1. tryLock()让当前线程尝试获取锁:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class TestTryLock {
public static void main(String[] args) {
TryLock tryLock = new TryLock();
new Thread(()->{
tryLock.getLock();
},"线程A").start();

new Thread(()->{
tryLock.getLock();
},"线程B").start();
}
}
class TryLock {
private ReentrantLock mylock = new ReentrantLock();
public void getLock(){
try {
if (mylock.tryLock(3, TimeUnit.SECONDS)){ //让当前线程在3秒钟内尝试获得锁,拿到锁后休眠5秒(仍在占用锁)
System.out.println(Thread.currentThread().getName()+"拿到了锁");
TimeUnit.SECONDS.sleep(5);
}else{
System.out.println(Thread.currentThread().getName()+"在指定时间内拿不到锁");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
if (mylock.isHeldByCurrentThread()){ //如果锁被当前线程占有,就释放锁
mylock.unlock();
}
}
}
}
  1. ReentrantLock(可重用锁)可以多次调用lock()方法重复上锁,但是相对应的必须重复调用unlock()方法释放所有锁。ReentrantLock(可重用锁)是可中断的,等待线程在等待获取锁的过程中可以主动终止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TestAccount3 {

public static void main(String[] args) {
Account3 account3 = new Account3();
// new Thread(account3,"线程A").start();
// new Thread(account3,"线程B").start();

new Thread(()->{ //run()方法的实现
account3.count();
},"线程A").start();
new Thread(()->{ //run()方法的实现
account3.count();
},"线程B").start();

}
}

class Account3 {
private int num;
private Lock lock = new ReentrantLock();
public void count() {
lock.lock();
lock.lock();
num++;
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
lock.unlock();
lock.unlock();
}
}

在上面的代码中,我们把资源类Account3和任务Runnable进行了解耦合,Account3不是Runnable接口的实现类,而是一个单纯的类

synchronized和ReentrantLock的区别

  1. synchronized是一个关键字,而ReentrantLock是一个实现类
  2. synchronized由JVM实现线程同步,ReentrantLock由JDK实现
  3. synchronized在线程结束自动释放锁,ReentrantLock需要开发人员手动释放
  4. ReentrantLocksynchronized更高级,更灵活
  5. synchronized拿不到锁会一直等待,ReentrantLock可以设置等待时间超时放弃等待

公平锁与非公平锁

公平锁(FairSync):线程同步时,多个线程依次排队等待获取锁
非公平锁(NonfairSync):线程同步时,可以插队抢占没有被占用的锁

查看ReentrantLock类的源码发现,默认创建的是非公平锁(实际上synchronized创建的也是非公平锁)

1
2
3
4
5
6
7
public ReentrantLock() {
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}