一、多线程(重点和难点)
1.1 什么是多线程?
1、程序(program)、进程(process)、线程(thread)
程序是未运行状态。进程是程序的运行时的状态。线程是进程中的其中1条执行路径。
2、串行(serial)、并行(parallel)、并发(concurrent)
1.2 Java中如何创建和启动多线程
Java中一共提供了4种方式来创建和启动多线程。JavaSE阶段只先介绍2种,在高级部分再介绍另外2种。
如果我们没有单独再开启其他线程,Java程序也有一个线程,是main线程,即主线程。现在希望创建和开启main线程之外的线程,与main线程是“同时”执行的关系,并发的关系。
1.2.1 继承Thread类
步骤:
-
编写一个类,可以是有名字的类,也可以是匿名的类,继承Thread类
- 如果这个线程类的代码比较简洁,而且这个线程类的对象只有1个,那么通常使用匿名的类就可以
- 如果这个线程类的代码比较复杂,而且这个线程类的对象需要创建多个,那么请用有名的类
-
必须重写public void run()方法- 在run方法中编写,你这个线程需要完成的任务代码
-
创建这个线程类的对象
-
启动线程,调用线程类对象的
start方法
1.2.2 实现Runnable接口
Java中类有单继承的限制。有时候,需要使用实现接口的方式来创建多线程。
步骤:
- 让线程类实现java.lang.Runnable接口
必须重写public void run()方法- 在run方法中编写,你这个线程需要完成的任务代码
- 创建接口的实现类的对象
- 创建一个Thread类的对象,让它帮我们代理一下这个接口的实现类的对象
- 调用Thread类的start方法
package com.mytest.thread;
public class MyRunnable implements Runnable{
@Override
public void run() {
//例如:打印1-10的偶数
for(int i=2; i<=10; i+=2){
System.out.println("偶数:" + i);
}
}
}
package com.mytest.thread;
public class TestMyRunnable {
public static void main(String[] args) {
MyRunnable m = new MyRunnable();
//m.start();//MyRunnable的父类没有start方法,它自己也没有start方法
Thread t = new Thread(m);//m是被代理的,t是代理
t.start();//当t线程被启动后,CPU会调用t的run方法,然后t的run再调用m的run方法
/*
下面是Thread类的run方法源码:
public void run() {
if (target != null) {
target.run();
}
}
这里的target就是我们创建t对象时,传入的m对象。
*/
//打印1-10的奇数
for(int i=1; i<=10; i+=2){
System.out.println("奇数:" + i);
}
}
}
1.2.3 问题答疑
1、使用JUnit和main方法来测试多线程的区别
- JUnit的test方法自己的代码执行完,就退出JVM了,不会管其他线程是否执行完
- main方法如果自己的代码执行完了,也不会立刻退出JVM,会等其他线程执行完
2、如果需要多个线程,是不是每一个线程都要弄一个独立的类?
- 如果多个线程的事情是一样的,那么类只需要1个,但是对象可以创建多个。
- 如果多个线程的事情不一样,那么需要每个任务单独写一个类。
1.3 Thread类的方法
1.3.1 方法系列1
- String getName():获取线程名称
- 默认的线程名称,Thread-下标
- void setName(新名称):修改线程名字
- int getPriority():获取优先级
- void setPriority(优先级):设置优先级
- 优先级高的线程的几率更大,但是不代表优先级低完全没机会。
- 如果要设置优先级,必须在 MIN_PRIORITY 到 MAX_PRIORITY 范围内,否则会发生IllegalArgumentException非法参数异常。
- MIN_PRIORITY:1
- MAX_PRIORITY :10
- NORM_PRIORITY:5
- static Thread currentThread() :可以在任意方法中通过Thread.currentThread()调用这个方法,至于获取的是哪个线程对象,就要看是哪个线程在执行这句代码。
1.3.2 方法系列2
1、sleep
static void sleep(long millis):用于让当前线程(执行这句语句的线程)进入休眠状态。时间单位是毫秒。
2、yield
static void yield():用于让当前线程暂停一下,让出CPU,重新加入抢夺CPU的队伍。
3、join
public final void join():让调用这个方法的线程“挤到”当前线程(执行这句代码的线程)之前,阻塞当前线程。当前线程只能等待,等待“挤”进来的线程执行完才能继续。
public final void join(long millis):让调用这个方法的线程“挤到”当前线程(执行这句代码的线程)之前,阻塞当前线程。当前线程只能等待,等待一段时间,时间到了才能继续。
4、stop
stop:早期有这个方法,但是后面发现有问题,现在废弃了,不推荐使用了。
那么如何优雅地停止线程?
可以使用变量来控制线程的执行,通常用boolean 类型的flag变量。
5、interrupt
interrupt:中断线程。但是它只对线程的休眠sleep、等待wait、加塞join等方法有效果。某个线程被中断会发生InterruptedException线程中断异常,它是编译时异常,必须try-catch处理。或者当前方法不处理,又能throws的话,也可以抛给调用者处理。大部分都是try-catch。
package com.mytest.thread;
public class TestThreadMethod {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
try {
for (int i = 1; i <= 1000; i++) {
Thread.sleep(10);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果try-catch在循环里面,那么中断只影响单次的循环
//如果try-catch在循环外面,那么中断会影响整个循环
}
};
t.start();//开启
//下面是主线程代码
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();//中断t线程
}
}
1.3.3 方法系列3
void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
守护线程是指为其他线程服务的后台线程。例如:JVM中的GC线程等。这种线程不会独立存在。当非守护线程结束后,守护线程会自动结束。
1.4 线程安全
1.4.1 什么是线程安全问题?
1.4.3 解决线程不安全问题
方向:加锁
JavaSE阶段:同步锁 synchronized
1、如何使用同步锁
同步锁的使用方式有2种:
【修饰符】 class 类名{
【其他修饰符】 synchronized 返回值类型 方法名(形参列表){ //同步方法
需要加锁的语句代码;
}
}
【修饰符】 class 类名{
【其他修饰符】 返回值类型 方法名(形参列表){
//语句
synchronized(监视器对象){//同步代码块
需要加锁的语句代码;
}
//语句
}
}
2、同步锁的原理
Java中每一个对象都可以作为线程的监视器对象。比喻:每一个人都可以当厕所所长。
Java的对象结构分为3个部分:
- 对象头:包含对象所有的类指针,锁标记,GC标记等等
- 其中一个锁标记,就可以用于表示当前对象是不是被某个线程给“占用”
- 比喻:谁要进入厕所,那么先要征得所长的同意,让所长拿着你的名字牌,表示厕所中有人了,其他人只能等待,等到你用完厕所。用完厕所的人,要从所长那里把名字牌拿走。
- 对应Java的话,就是某个线程要进入synchronized标记的方法或代码块执行之前,先要占用这个监视器对象的锁标记位。其他线程只能等待,等待这个线程执行完这段代码为止。才继续抢这个监视器对象。执行完同步方法或同步代码块的线程,要清除标记位。
- 实例变量数据区:对象的属性
- 填充空白:可选,不是所有对象都有。当整个对象占用的内存不是字节的整数倍,那么会填充一些空白凑够字节的整数倍,例如:boolean类型的值占1位
3、如何选择同步锁对象
- 具有竞争关系的多个线程,选择**
“同一个”**同步锁对象即可。对象的类型不重要。 - 必须是一个对象。
- 特别是this对象,以及非静态方法要小心,同步锁对象是不是同一个,要判断清楚
对于同步方法来说,同步锁对象是默认的,非静态方法是this对象,静态方法是当前类的Class对象。
4、同步范围的选择问题
- 同步范围太大,会导致其他线程没有机会
- 同步范围太小,线程安全问题不彻底
静态方法去掉static为啥就不安全了?
- 方法是非静态的,那么它的默认锁对象/监视器对象是this
- 方法是静态的,那么它的默认锁对象/监视器对象是当类的Class对象,即
TicketThreadOne.class
5、如何判断自己的线程代码有没有安全问题?
- 多个线程
- 共享资源:可以是同一个变量,同一个对象,同一个文件,同一个打印机等
- 多个线程对这些共享数据有写/修改操作
只要上面的3个条件同时满足了,就一定有安全问题。就一定要加synchronized或 juc中的锁。
2.5 单例设计模式
1、什么是设计模式?
大量程序员在编写代码解决问题的过程中,总结出来的一套一套的解决方案。
针对不同的问题,通常会有固定的解决方案的模板。
常见的设计模式有23种。设计模式在不同的语言之间有通用性。
在Java中,具体的设计模式的实现代码与其他语言有区别。因为不同的语言的语法毕竟是不同的。但是理论是相同的。
2、什么是单例设计模式?
为了保证某个类的对象在整个程序运行期间,只有唯一的1个。不允许出现第2个。
要解决这个问题,咱们在设计这个类的时候,必须有一套模板,规范,要求。这些模板就是单例设计模式。
3、单例设计模式的分类
写法有很多种,大致可以分为2大类:
(1)饿汉式单例设计模式:饥不择食,着急。这个类的对象在类初始化时,就提前创建好了。不管你现在用不用这个对象。
(2)懒汉式单例设计模式:拖延。这个类的对象必须等到别人第一次来明确获取这个对象了,才new。
饿汉式的写法1:
public enum SingleOne {
INSTANCE //这个对象名自己取就可以,不一定非得是INSTANCE
}
饿汉式的写法2:
public class SingleTwo {
//这里final可选,但是public和static必选的,static表示共享同一个,public外面可以获取到
public static final SingleTwo INSTANCE = new SingleTwo();
private SingleTwo(){//构造器私有化
}
}
饿汉式的写法3:
public class SingleThree {
private static final SingleThree INSTANCE = new SingleThree();
private SingleThree(){//构造器私有化
}
public static SingleThree getInstance(){
return INSTANCE;
}
}
懒汉式的写法1:
package com.mytest.single;
public class SingleFour {
private SingleFour(){//构造器私有化
}
public static SingleFour getInstance(){
return Inner.INSTANCE;//INSTANCE的类型是SingleFour
}
private static class Inner{//静态内部类
private static SingleFour INSTANCE = new SingleFour();
}
}
懒汉式的写法2:
package com.mytest.single;
public class SingleFive {
private static SingleFive instance;
private SingleFive(){//构造器私有化
}
public static synchronized SingleFive getInstance(){
if(instance == null){
instance = new SingleFive();
}
return instance;
}
/* public static SingleFive getInstance(){
//SingleFive.class代表当前类的Class对象,它是共享的
synchronized (SingleFive.class) {
if (instance == null) {
instance = new SingleFive();
}
return instance;
}
}*/
/*public static SingleFive getInstance(){
//SingleFive.class代表当前类的Class对象,它是共享的
if(instance == null) {
synchronized (SingleFive.class) {
if (instance == null) {
instance = new SingleFive();
}
}
}
return instance;
}*/
}
1.6 生产者与消费者问题
1.6.1 什么是生产者与消费者问题?
当一个或一些线程负责往“共享数据区”填充/增加数据,这个/些线程被称为生产者线程,
另一个或一些线程负责从“共享数据区”消耗/减少数据,这个/些线程被称为消费者线程。
当它俩/它们“一起”工作时可能出现:
- 线程安全问题:解决办法是加synchronized
- 线程的协作问题:
- 当“共享数据区”空的时候,消费者线程应该停下来“等待”,生产者可以工作。消费者线程需要等待别人“唤醒”它或指定等待时间。
- 当“共享数据区”满的时候,生产者线程应该停下来“等待”,消费者可以工作。生产者线程需要等待别人“唤醒”它或指定等待时间。
- 所以解决办法就是 等待与唤醒机制。JavaSE阶段用的是 wait() 和notify() /notifyAll()。这些方法定义在了Object类中。
- 而且它们都必须由 同步锁对象/监视器对象来调用,不能由别人调用。