Java SE 复习
并发/多线程
基本概念
程序(program): 是为了完成特定任务, 用某种语言编写的一组指令的集合, 是一段静态的代码, 静态对象
进程(process): 程序都一次执行过程, 正在运行中的程序, 有生命周期–
线程(thread): 进程可细化为线程, 是程序内部的执行路径. 一个进程可以拥有多个线程
线程创建与使用
java.lang.Thread
多线程的创建, 方法一: 继承Thread类
1 | /** |
测试编写两个线程 以及创建Thread匿名类
1 | package com.atguigu.exer; |
方式二:实现 Runnable 接口
定义子类 ,实现 Runnable 接口。
子类中重写 Runnable 接口中的 run 方法。
通过 Thread 类含参构造器创建线程对象。
将 Runnable 接口的子类对象作为实际参数传递给 Thread 类 的构造器中 。
调用 Thread 类的 start 方法:开启线程 调用 Runnable 子类接口的 run 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* 创建多线程的方法2: 实现Runnable接口
*
*/
class MThread implements Runnable{
//实现run()方法
public void run() {
for (int i = 0; i < 100; i++) {
if (i/2 == 0) {
System.out.println(i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
MThread mThread = new MThread();
//通过 Thread 类含参构造器创建线程对象
Thread t1 = new Thread(mThread);
t1.start();
}
}
Thread类的有关方法
void start(): 启动线程,并执行对象的 run() 方法
run(): 线程在被调度时执行的 操作
String getName (): 返回线程的名称
void setName (String 设置该线程名称
- ```java
//也可以用构造器命名
HelloThread helloThread = new HelloThread(“Thread: 1”);
//setName()重命名
helloThread.setName(“线程一”);
//也可以给主线程命名
Thread.currentThread().setName(“主线程”);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
- static Thread currentThread (): 返回 当前 线程 。在 Thread 子类中就是 this ,通常用于主线程和 Runnable 实现类
- static void yield() 线程让步
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
- ```java
class HelloThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
System.out.println(Thread.currentThread().getName()+"::"+i);
}
if(i%20==0){
// Thread.currentThread().yield();
this.yield();
}
}
}
public HelloThread(String name){
super(name);
}
}
- ```java
join() 当某个程序执行流中调用其他线程的 join() 方法时调用线程将
被阻塞,直到 join() 方法加入的 join 线程执行完为止低优先级的线程也可以获得执行
```java
if(i==20){helloThread.join();
}
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
- static void sleep(long millis) 指定时间 毫秒
- 令当前活动线程在指定时间段内放弃对 CPU 控制 使其他线程有机会被执行 时间到后
重排队。
- 抛出 InterruptedException 异常
- stop(): 强制线程生命期结束,已经过时, 不推荐使用
- boolean isAlive(): 返回 boolean,判断线程是否还活着
### 线程的调度
- 调度策略
- 时间片
- 抢占式 高优先级的线程抢占 CPU
- Java 的调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
- 线程的优先级等级---默认是5
- MAX_PRIORITY: 10
- MIN PRIORITY: 1
- NORM_PRIORITY: 5
- 涉及的方法
- getPriority () 返回 线程优先值
- setPriority ( int newPriority ) 改变 线程的优先级
- ```java
//重命名
helloThread.setName("线程一");
helloThread.setPriority(Thread.MAX_PRIORITY);
helloThread.start();
//也可以给主线程命名
Thread.currentThread().setName("主线程");
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
说明
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
案例1, 使用继承Thread类的方式创建线程
1 | package com.atguigu.java; |
案例2: 实现同样的案例, 但是使用实现Runnable接口的方式创建线程
1 | package com.atguigu.java; |
继承方式和实现方式的联系与区别
实际开发中, 实现Runnable接口的方式更好一些.
1 | public class Thread extends Object implements Runnable |
区别
继承 Thread :线程 代码存放 Thread 子类 run 方法中。
实现 Runnable :线程代码存在接口的子类的 run 方法。
实现方式的好处
- 避免 了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
补充:线程的分类
Java中的线程分为两类:一种是守护线程 ,一种是 用户线程 。
- 它们在几乎每个方面都是相同的,唯一的区别是判断 JVM 何时离开。
- 守护线程是用来服务用户线程的,通过在 start() 方法前调用 thread.setDaemon (true) 可以把一个用户线程变成一个守护线程。
- Java 垃圾回收就是一个典型的守护线程。
- 若 JVM 中都是守护线程,当前 JVM 将 退出 。
- 形象理解: 兔死狗烹,鸟尽弓藏
生命周期
JDK 中用 Thread State 类定义了线程的几种 状态
要想实现多线程必须在主线程中创建新的线程对象 。 Java 语言使用 Thread 类及其子类的对象来表示线程在它的一个完整的生命周期中通常要经历如下的五种状态- 新建:当 一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建
状态 - 就绪:处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已
具备了运行的条件,只是没分配到 CPU 资源 - 运行:当就绪的线程被调度 并 获得 CPU 资源时便进入运行状态,run() 方法定义了线
程的操作和功能 - 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中
止自己的执行,进入阻塞状态 - 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
- 新建:当 一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建
线程的同步
主要解决线程安全问题
问题的提出:
- 多个线程执行的不确定性引起执行结果的不稳定
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
案例: 模拟火车站售票程序,开启三个窗口售票。
所以就出现了重票和错票问题
问题的原因:
- 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误 。
解决办法:
- 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行
Synchronized的使用方法
Java 对于多线程的安全问题提供了专业的解决 方式 同步 机制
同步代码块:
1
2
3synchronized(对象){
//需要被同步的代码
}括号里放的是同步监视器, 俗称锁, 任何一个对象都可以充当锁
- 要求多个线程必须要共用一把锁
买票案例改成线程安全版本:
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/**
* 创建三个窗口买票, 100张, 使用实现Runnable接口的方式实现
* 依然存在线程安全问题, 待解决
* 但是这种方式不需要给ticket加静态变量
*/
class Window1 implements Runnable{
private int ticket = 100;
Object obj = new Object();
public void run() {
while (true){
synchronized (obj){
if(ticket>0){
// try {
// Thread.sleep(300);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName()+"卖票, 票号为: "+ ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("线程1");
t2.setName("线程2");
t3.setName("线程3");
t1.start();
t2.start();
t3.start();
}
}synchronized 还可以放在方法声明中,表示整个方法为同步方法 。
1
2
3public synchronized void show(String name){
}继承实现的时候需要用静态方法
1
2
3public static synchronized void show(String name){
}因为同步方法的锁, 默认使用的是this对象, 而Extend实现, this对象是不固定的
在继承Thread类的实现方法里, 锁需要使用static
1 | private static Object obj = new Object(); |
锁也可以使用 this 当前对象, 但是只能在实现Runnable类的时候使用, 因为此时this是唯一
1 | synchronized (this){ |
继承Thread可以使用.class, 类也是对象(反射相关), 这也只会加载一次:
1 | synchronized (Window.class){ |
同步的好处:
操作同步代码时, 只有一个线程参与, 其他线程等待, 相当于是一个单线程的过程, 效率低
使用同步机制将单例模式中的懒汉式改成线程安全的
1 | public class BankTest { |
线程的死锁问题
死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
死锁演示
- ```java
/**演示线程死锁问题
线程1 拿着s1 等线程2放开s2, 而线程2 拿着s2 等线程1放开s1, 双方都在等待对方放开
资源, 死锁就出现了, 代码就执行不下去了
/
public class ThreadTest {public static void main(String[] args) {
StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); new Thread(){ @Override public void run() { synchronized (s1){ s1.append("a"); s2.append("1"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start();
}new Thread(new Runnable() { @Override public void run() { synchronized (s2){ s1.append("c"); s2.append("3"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); }
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
- 解决方法:
- 专门的算法、原则
- 尽量减少同步资源 的 定义
- 尽量避免嵌套同步
### Lock( 锁 ) -- JDK5新增的同步方法
- 从 JDK 5.0 开始 Java 提供了更强大的线程同步机制 通过显式定义同
步锁对象来实现同步。同步锁使用 Lock 对象充当 。
- java.util.concurrent.locks.Lock 接口是 控制多个线程对共享资源进行访问的
工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象
加锁,线程开始访问共享资源之前应先获得 Lock 对象 。
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和
内存语义, 在 实现线程安全的控制中,比较常用的是 ReentrantLock 可以
显式加锁、释放锁 。
```java
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决线程安全问题的方式三: lock锁 --- JDK5.0新增
* 面试题: synchronized 与 Lock的异同
* 同: 解决线程安全问题
* 不同: lock需要手动解锁: 调用unlock(), synchronize机制在执行完代码逻辑之后自动释放
*/
class Window implements Runnable{
private int ticket = 100;
//实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
//调用lock锁定方法
lock.lock();
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "ticketNo: "+ ticket);
ticket--;
}else {
break;
}
} finally {
//调用解锁方法
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
- ```java
面试题: synchronized 与 Lock的异同
同: 解决线程安全问题
不同: lock需要手动解锁: 调用unlock(), synchronize机制在执行完代码逻辑之后自动释放
练习
1 | package com.atguigu.exer; |
线程通信
1 | //使得调用wait()方法的线程进入阻塞状态 |
然后notify唤醒
1 | notify(); |
1 | package com.atguigu.java2; |
面试题:
sleep()和wait() 的异同:
相同点: 都可以使得当前线程进入阻塞状态
不同点:
声明位置不同, 一个在Thread类中, wait在Object类中
wait()只能在同步代码块中, sleep()可以在任意地方调用
wait()会释放同步监视器, 而sleep()不会释放
经典例题:生产者/消费者问题
- 生产者 Productor 将产品交给店员 ( Clerk),而消费者 ( 从店员处取走产品,店员一次只能持有固定数量的产品 比如 :20 ),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
- 这里可能出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者会取相同的数据。
实现:
1 | package com.atguigu.java2; |
JDK5.0新增的线程创建方式
新增方式一:实现Callable 接口
与使用 Runnable 相比, Callable 功能更强大些
- 相比 run() 方法,可以有返回值
- 方法 可以抛出异常
- 支持泛型的返回值
- 需要借助 FutureTask 类,比如获取返回结果
Future 接口
- 可以 对具体 Runnable 、 Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask 是 Futrue 接口的唯一的实现类
- FutureTask 同时实现了 Runnable, Future 接口。它既可以作为Runnable 被线程执行,又可以 作为 Future 得到 Callable 的返回值
如何理解Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
call()有返回值
call()可抛出异常, 被外面的操作捕获获取
Callable支持泛型
实现代码:
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
49package com.atguigu.java2;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
/**
* 创建线程方式三: 实现Callable借口 -- JDK5.0新增
*/
//1. 创建一个实现Callable的实现类
class NumThread implements Callable{
//2. 实现call 方法, 将此线需要执行的操作声明在call()中
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i%2 == 0) {
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3. 创建Callable接口都实现类对象
NumThread numThread = new NumThread();
//4. 讲此Callable接口实现类的对象作为传递到FutureTask构造器中, 创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5. 将FutureTask的对象作为参数传递到Thread类的构造器中, 创建Thread对象, 并start()
new Thread(futureTask).start();
try {
//6. 获取callable中的call()方法都返回值
//返回值为FutureTask构造器参数Callable实现重写的call()的返回值
Object sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
新增方式二:使用线程池
- 背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大 。
- 思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:
- 提高响应速度 (减少了创建新线程的时间)
- 降低资源消耗 (重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize :核心池的大小
- maximumPoolSize :最大线程数
- keepAliveTime :线程没有任务时最多保持多长时间后会 终止
线程池相关API
JDK 5.0 起提供了线程池相关 API: ExecutorService 和 Executors
ExecutorService :真正的线程池 接口。常见子类 ThreadPoolExecutor
void execute(Runnable command) :执行任务 命令,没有返回值,一般用来执行
RunnableFuture submit(Callable task):执行任务,有返回值,一般又来执行Callable void shutdown():关闭连接池
Executors :工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool (): 创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool (n): 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor (): 创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool (n): 创建一个线程池,它可安排在给定延迟后运
行命令或者定期地执行。
使用实现:
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
48package com.atguigu.java2;
import javax.xml.ws.Service;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 创建线程的方式4: 使用线程池
*/
class NumberThread implements Runnable{
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
System.out.println(Thread.currentThread().getName()+"::"+i);
}
}
}
}
class NumberThread1 implements Runnable{
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 != 0) {
System.out.println(Thread.currentThread().getName()+"::"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
service1.setCorePoolSize(15);
service.execute(new NumberThread()); //适合Runnable
service.execute(new NumberThread1());
//关闭线程池
service.shutdown();
// service.submit(); //适合Callable
}
}
反射
反射提供的功能
- 可以在运行时判断任意对象所属的类
- 可以在运行时构造任意一个类的对象
- 可以在运行时判断类里的成员变量和方法
- 可以在运行时获取泛型信息
- 可以在运行时调用
- 可以在运行时处理注解
- 生成动态代理
相关API
- java.lang.Class 代表一个类
- java.lang.reflect.Method 代表类的方法
- java.lang.reflect.Field 代表类的成员变量
- java.lang.reflect.Constructor 代表类的构造器
反射实现:
1 | public void test2(){ |
通过反射还可以调用私有的构造器, 方法, 属性
关于java.Lang.Class
- 程序经过javac.exe命令后, 会生成一个或者多个字节码文件(.class), 接着使用java.exe命令对某个字节码文件进行解释运行. 相当于讲字节码文件加载到内存中. 此过程就称为类的加载. 加载到内存中的类, 我们就称为运行时类, 此运行时类, 就是Class的一个实例
- 换句话说, Class的实例就对应着一个运行时类.
1 | public void test3(){ |
- 加载到内存中的运行时类, 会缓存一定的时间. 在此时间之内, 我们可以通过不同的方式来获取此运行时类.
加载器:
- 自定义加载器
- 系统类加载器
- 扩展类加载器
- 引导类加载器( 无法获取) —- String.class.getClassLoader
代理/动态代理
泛型
JDK1.5 的新特性
泛型是什么:
泛型就是标签, 表示容器里放的内容类型
把元素的类型设计成一个参数, 这个类型参数叫做泛型
静态方法中不能使用类的泛型
异常类不能够使用泛型
为什么要有泛型
所谓泛型, 就是允许在定义类, 接口都的时候, 通过表示标识类中某个属性的类型或者某个方法的返回值及参数类型. 这个类型参数将在使用的时候确定
集合中用泛型
- 集合接口或集合类在jdk5.0时都修改为带泛型的结构。
- 在实例化集合类时,可以指明具体的泛型类型
- 指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(比如:方法、构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型。
- 比如:add(E e) —>实例化以后:add(Integer e)
- 注意点:泛型的类型必须是类,不能是基本数据类型。需要用到基本数据类型的位置,拿包装类替换
- 如果实例化时,没有指明泛型的类型。默认类型为java.lang.Object类型。
自定义泛型结构
实例化如果不指明类型, 默认泛型类型为Object类型
例子:
1 | public class Order<T> { |
泛型方法
- 泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。
- 换句话说,泛型方法所属的类是不是泛型类都没有关系。
- 泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在实例化类时确定。
1 |
|
继承上实现泛型
通配符的使用
有限制条件的通配符使用
1 | /* |