错误(Error): 不能捕获处理的严重问题. 绝症
必须将程序停下来, 修改代码才能解决
错误的类名都是 "XxxError" 方式
异常(Exception): 可以捕获处理的问题. 发烧感冒吃个药就好了
程序执行起来后, 如果有合适的处理方式, 即使发生异常, 程序也能处理该异常并继续运行
异常的类名都是 "XxxException" 方式
1. 编译时异常:
编译时期就会发生的异常, 必须在编译时期处理 Unhandled exception XxxException
2. 运行时异常:
编译时正常, 运行时才会发生的异常
throw关键字作用:
在方法中抛出指定的异常对象
格式:
throw new 异常类名("异常原因字符串");
例:throw new ArrayIndexOutOfBoundsException("");
注意:
1. throw 必须写在方法的内部
2. throw 后面new的异常对象, 必须是 "Exception" 或 "Excetion的子类" 的对象
3. 一个方法内部 throw 了一个异常对象, 则该方法可以分为2种情况来处理该异常:
如果 throw 的是"运行时异常"(RuntimeException及其子类)对象, 那么可以不处理 该异常最终会交给JVM处理, 结果就是: 打印异常信息到控制台, 并立刻结束程序
如果 throw 的是"编译时异常"(Exception及其子类), 则必须处理:
处理方式1: throws 抛出
处理方式2: try...catch 捕获补充:
如果我们没有额外创建线程, 那么我们的程序就只有一个线程, 即主线程, 此时程序是"单线程"的
单线程的执行特点:
同一个线程内的代码, 从上往下依次执行
实现多线程的第一种方式:
1. 定义类, 继承 Thread 类
2. 重写 run() 方法, run方法内部是线程要执行的任务
3. 创建Thread子类的对象, 调用 start() 方法启动线程
java.lang.Thread类: 表示线程. 实现了Runnable接口
void start(): 启动线程, 即让线程开始执行run()方法中的代码
注意:
必须调用 start() 方法来开启线程, 不能直接调用 run() 方法, 调用 run() 会变成单线程
同一个线程对象, 不能多次调用 start() 方法
Java是抢占式调度, 不同线程的代码, 执行顺序是随机的
l 多线程的结果的随机性打印结果。
抢占式,本质是多个线程在抢夺cpu的资源,
l 获取当前线程的名称
1. String get.name()
2. currentThread静态方法直接掉用,获取名称
l 设置线程名字:
1. set.Name
2. 创建一个带参数的构造方法,参数传递线程名称,
修改添加线程创建对象以后 对象名.setname
修改main线程的名字直接在main方法中Thread.setname
l Thread底下的sleep方法:
表示钟表内部的时间的,让它放慢1秒后执行
l 创建多线成的第二种方法Runnable
创建多线程程序的第二种方式:实现Runnable接口
java.lang.Runnable
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。
java.lang.Thread类的构造方法
(1)Thread(Runnable target) 分配新的 Thread 对象。
(2)Thread(Runnable target, String name) 分配新的 Thread 对象。
实现步骤:
树
1.创建一个Runnable接口的实现类
2.在实现类中重写Runnable接口的run方法,设置线程任务
3.创建一个Runnable接口的实现类对象
4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
5.调用Thread类中的start方法,开启新的线程执行run方法
Thread和实现Runnable接口创建多线程程序的好处:
1.避免了单继承的局限性
一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类
实现了Runnable接口,还可以继承其他的类,实现其他的接口
2.增强了程序的扩展性,降低了程序的耦合性(解耦)
耦合性: 相互之间的关系的紧密程度
耦合性高: 相互之间的关系非常紧密
耦合性低: 相互之间的关系不太紧密
我们追求 "低耦合"
实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
实现类中,重写了run方法:用来设置线程任务
创建Thread类对象,调用start方法:用来开启新线程
匿名内部类创建接口
new 父接口/父类() {
重写方法
};
继承Thread类
实现Runnable接口
l 线程安全的问题图解:模拟电影院卖票:线程安全问题
问题发生场景:
多个线程操作共享资源
问题发生原因:
JVM是抢占式调度, CPU在每个线程之间切换是随机的, 代码执行到什么位置是不确定的
在操作共享资源时, 由于一个线程还没有执行完, 另一个线程就来操作, 就会出现问题
如何解决:
在操作共享资源时, 让线程一个一个来执行, 不要并发操作共享变量, 就可以解决问题
l 解决线程安全的解决问题方式1:同步代码块
1. 同步技术三种
(1) 同步代码块
解决线程安全问题的一种方案:使用同步代码块
格式:
树
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
1.通过代码块中的锁对象,可以使用任意的对象
2.但是必须保证多个线程使用的锁对象是同一个
3.锁对象作用:
把同步代码块锁住,只让一个线程在同步代码块中执行
同步代码块就类似于房间和房门
房间
+------+
| | 线程1
| \ 门锁 线程2
| | 线程3
+------+
线程进入房间后, 锁上房间, 其他线程就进不来
+------+
| |
| 线程1| 线程2
| | 线程3
+------+
线程1做完事情, 打开锁出了门, 其他线程就可以抢着进来
+------+
| |
| \线程2
| | 线程3
+------+
线程1
同步代码块图解
l 同步技术的原理
树
锁对象, 也称为"同步锁", "对象锁", "对象监视器"
l 同步的原理:
线程进入同步代码块前, 会"争夺锁对象", "只有一个线程"会抢到锁对象
进入同步代码块的线程, 会"持有锁对象", 并执行同步代码块中的代码
此时同步代码块外的线程, 处于"阻塞"状态, 只能等待
当同步代码块内的线程执行完代码块, 会离开同步代码块, 并"归还锁对象"给同步代码块
等在同步代码块外的其他线程就可以继续争夺锁对象
l 解决线程安全问题的二种方案:使用同步方法
使用步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中
2.在方法上添加synchronized修饰符
格式:定义方法的格式
树
修饰符 synchronized 返回值类型 方法名(参数列表){ 可能会出现线程安全问题的代码(访问了共享数据的代码)
非静态同步方法的锁对象: this
静态同步方法:
public static synchronized void method(){
// 可能会产生线程安全问题的代码
}
静态同步方法的锁对象: 当前类的字节码对象 Class对象
RunnableImpl.class -> Class对象
l 获取一个类的字节码对象的3种方式:
1. 对象名.getClass() new RunnableImpl().getClass()
2. 类名.class RunnableImpl.class
3.Class.forName("类的全名"); Class.forName("com.itheima.test05.RunnableImpl");
l 字节码对象的特点:
同一个类, 他的字节码对象只有"唯一的一个"
Person
new Person() this
new Person() this
..
Person.class 只有一个
锁对象必须是多个线程共享的同一个对象
}
l 第三种 方式
解决线程安全问题的三种方案:使用Lock锁
java.util.concurrent.locks.Lock接口
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
树
void lock()获取锁。
void unlock() 释放锁。
java.util.concurrent.locks.ReentrantLock implements Lock接口
使用步骤:
1.在成员位置创建一个ReentrantLock对象
2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
使用修改代码, 使用Lock锁解决卖票问题
public class RunnableImpl implements Runnable {
// 定义多线程共享的票数
private int ticket = 100;
// 定义Lock锁
Lock l = new ReentrantLock();
@Override
public void run() {
// 这个run方法会被3个窗口执行, 所以每个窗口不知道循环多少次才能卖完, 所以使用死循环
while (true) {
// 加锁(获取锁)
l.lock();
try {
// 判断, 当有票时, 再卖票
if (ticket > 0) {
// 先打印当前窗口卖出的票
System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
// 然后让票数-1
ticket--;
} else {// 在这里也可以写个else, 里面break
break; // 没票了, 结束循环
}
} finally {
// 释放锁
l.unlock();
}
}
}
}
public class Test {
public static void main(String[] args) {
// 先创建一个卖票任务
RunnableImpl runnable = new RunnableImpl();
// 创建3个线程模拟3个售票窗口
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
// 为了看的明显, 也可以给线程设置名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
// 启动线程, 开始卖票
t1.start();
t2.start();
t3.start();
}
}
l 线程的状态
锁对象, 也称为"同步锁", "对象锁", "对象监视器"
Object类中关于线程的方法:
java.lang.Object类:
// 成员方法 (<<注意>>: 只能通过"锁对象"调用)
树
void notify(): 随机唤醒在同一个锁对象上的某一个处于等待状态的线程
void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
void wait(): 让当前线程处于"无限等待"状态
void wait(long timeout): 让当前线程处于"计时等待"状态, 时间到或被唤醒后结束此状态
void wait(long timeout, int nanos): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
"注意!! 以上方法只能通过锁对象调用"
l 线程的生命周期中, 可以出现有6种状态:
1. "NEW 新建"
线程被创建, 但没有调用 start() 启动
2. "RUNNABLE 可运行"
调用 start()方法后已启动, 但可能正在执行 run() 方法的代码, 也可能正在等待CPU的调度
3. "BLOCKED (锁)阻塞"
线程试图获取锁, 但此时锁被其他线程持有
4. "WAITING 无限等待"
通过锁对象调用无参的 wait() 进入此状态.
等待其他线程通过锁对象执行 notify() 或 notifyAll() 才能结束这个状态
5. "TIMED_WAITING 计时等待"
如通过锁对象调用有参的 wait(long millis) 或 sleep(long millis), 则进入此状态.
直到时间结束之前被其他线程通过锁对象执行 notify()或 notifyAll()唤醒, 或时间结束自动唤醒
6. "TERMINATED 终止"
run()方法结束(执行结束, 或内部出现异常), 则进入此状态
线程间通信
卖包子吃包子
*/
public class Test {
public static void main(String[] args) {
// 创建一个对象作为锁对象
Object lock = new Object();
// 使用匿名内部类方式创建一个线程, 作为顾客线程
new Thread(){
@Override
public void run() {
// 死循环要包子吃包子
while (true) {
// 使用同步代码块, 同步代码块中有锁对象, 可以用来调用wait()方法
synchronized (lock) {
// 先向老板要包子
System.out.println("[顾客说:] 老板! 来2斤包子!");
// 顾客先等着老板做
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 如果能执行到这里, 说明被老板唤醒了, 开始吃
System.out.println("[顾客说:] 嗯嗯!真香~");
System.out.println("-------------------");
}
}
}
}.start();
// 使用匿名内部类方式创建一个线程, 作为老板线程
new Thread(){
@Override
public void run() {
// 死循环做包子
while (true) {
// 先睡5秒模拟做包子的时间(实际上是为了让老板线程抢锁不要太快)
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 使用同步代码块, 同步代码块中有锁对象, 可以用来调用notify()方法
synchronized (lock) {
// 打印做好了包子
System.out.println("[老板说:] 包子做好了, 来吃吧");
// 老板要通知顾客, 使用锁对象调用notify()
lock.notify();
} }} }.start();
}}
l Object类中wait(long timeout)和notifyAll()方法
// 成员方法 (只能通过"锁对象"调用)
树
void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
void wait(long timeout): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
l wait() 和 sleep() 的区别:
1. wait会释放锁, 恢复时需要重新获取锁; sleep不会释放锁
2. wait可以被notify/notifyAll唤醒; sleep不会
3. wait要用锁对象调用; sleep要用Thread类名调用
l 进入到TimeWaiting(计时等待)有两种方式
1.使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
2.使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
唤醒的方法:
void notify() 唤醒在此对象监视器上等待的单个线程。
void notifyAll() 唤醒在此对象监视器上等待的所有线程。
java.lang.Thread类: 表示线程. 实现了Runnable接口
// 构造方法
树
Thread(): 创建Thead对象
Thread(String threadName): 创建Thead对象并指定线程名
Thread(Runnable target): 通过Runnable对象创建Thread对象
Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线名
// 成员方法
void run(): 用于让子类重写, 表示该线程要执行的任务.不能直接调用
void start(): 启动线程, 即让线程开始执行run()方法中的代码
String getName(): 获取线程的名称
void setName(String name): 设置线程名称
// 静态方法
static Thread currentThread(): 返回对当前正在执行的线程对象的引用
static void sleep(long millis): 让所在线程睡眠指定的毫秒
u 今日API
Thread Thread(): 创建Thead对象
Thread Thread(String threadName): 创建Thead对象并指定线程名
Thread Thread(Runnable target): 通过Runnable对象创建Thread对象
Thread Thread(Runnable target, String threadName): 通过Runnable对象创建对象并指定线程名
// 成员方法
void run(): 用于让子类重写, 表示该线程要执行的任务.不能直接调用
void start(): 启动线程, 即让线程开始执行run()方法中的代码
String getName(): 获取线程的名称
void setName(String name): 设置线程名称
// 静态方法
static Thread currentThread(): 返回对当前正在执行的线程对象的引用
static void sleep(long millis): 让所在线程睡眠指定的毫秒
java.lang.Object类:
// 成员方法 (只能通过"锁对象"调用)
void notify(): 随机唤醒在同一个锁对象上的某一个处于等待状态的线程
void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
void wait(): 让当前线程处于无限等待状态
void wait(long timeout): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态
void wait(long timeout, int nanos): 让当前线程处于计时等待状态, 时间到或被唤醒后结束此状态