JavaSE-多线程
多线程
概念
程序:未完成任务,用某种语言编写的一组指令的集合。
进程:程序的一次执行过程,或是正在内存中运行的应用程序。
线程:进程可以进一步细化为线程,是程序内部的一条执行路径。线程是CPU调度和执行的最小单位。
-
不同进程之间是不共享内存的。
-
进程之间的数据交换和通信的成本很高。
线程调度策略
分时调度:所有线程轮流使用CPU的使用权。并且平均分配每个线程占用CPU的时间
抢占式调度:让优先级高的线程以较大的概率优先使用CPU。如果线程的优先级相同,那么就会随机选一个。Java使用的是抢占式调度。
了解并行和并发
多线程的优点
提供应用程序的响应。
提高计算机CPU的利用率。
改善程序结构。将既长又复杂的进程拆为多个线程,独立运行,便于理解和修改。
多线程创建方式
方式一:继承Thread类的方式
-
创建一个继承于Thread类的子类
-
重写Thread类的run()
-
创建当前Thread的子类对象
-
通过对象调用start()
创建线程代码:
1 | public class ThreadTest { |
方式二:实现Runnable接口
- 创建一个Runnable接口的实现类
- 实现接口中的run()
- 创建当前实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
- 实例调用start()
1 | public class Way2test { |
对比一下两种方式:
共同点:①启动线程都是Thread类的start()方法②创建的线程对象都是Thread类或其子类的实例
不同点:前者是类的继承,后者是接口的实现
建议使用runnable接口的方式。
好处:①实现的方式,避免了单继承的局限性②更适合处理有共享数据的问题③实现了代码和数据的分离
联系:代理模式
方式三:实现Callable方式
方式四:使用线程池
方式三、四在此省略
Thread类
常用方法
线程中的构造器(记得子类不会继承父类的构造器)
Thread()
Thread(String name)
Thread(Runnable target)
Thread(Runnable target,String name)
线程中的常用方法
start():①启动线程②调用线程的run()
run():声明线程要执行的代码
currentThread():获取当前的线程
getName():获取线程名
setName():设置线程名
sleep(long mills):静态方法,调用可以是当前线程睡眠指定的毫秒数
yield():静态方法一旦执行此方法,就释放CPU的执行权
join():在线程a中通过线程b调用join方法,意味着线程a进入阻塞状态,直到线程b结束,线程a才会继续执行
isAlive():判断当前线程是否还存活
线程优先级
getPriority():获取线程的优先级
setPriority():设置线程的优先级(1~10)
线程的生命周期
jdk5之前
jdk5之后对阻塞进行了细分,在此不做赘述,在JUC中会详细学习
线程安全问题
什么叫线程安全问题?
如何解决?
方式一:同步代码块解决安全问题
1 | synchronized(同步监视器){ |
说明:
- 需要被同步的代码,即操作共享数据的代码。
- 共享数据:即多个线程都需要操作的数据。
- 需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。
- 同步监视器,俗称锁,哪个线程获取了锁,哪个线程就能执行需要被同步的代码。
- 同步监视器,可以用任何一个类的对象充当。但是,多个线程必须公用一个同步监视器。
在实现Runnable接口的方式中,同步监视器可以考虑使用this
在继承Thread类的方式中,同步监视器慎用this
方式二:同步方法解决安全问题
如果操作共享数据的代码完整声明在了一个方法中,那么我们就可以将此方法声明为同步方法。
非静态的同步方法,默认监视器是this;静态的同步方法,默认监视器是当前类本身(.class)。
synchronized的好处:解决了线程安全问题。
弊端:在操共享数据时,多线程其实是串行执行的,意味着性能低。
例子:
1 | public class ThreadTest { |
懒汉式单例模式
publicpublic class LazyOne {
private static LazyOne instance;
private LazyOne(){}
//方式1:线程不安全
public static synchronized LazyOne getInstance1(){
if(instance == null){
instance = new LazyOne();
}
return instance;
}
//方式2:线程安全
public static LazyOne getInstance2(){
synchronized(LazyOne.class) {
if (instance == null) {
instance = new LazyOne();
}
return instance;
}
}
//方式3:相比2效率提高
public static LazyOne getInstance3(){
if(instance == null){
synchronized (LazyOne.class) {
try {
Thread.sleep(10);//加这个代码,暴露问题
} catch (InterruptedException e) {
e.printStackTrace();
}
if(instance == null){
instance = new LazyOne();
}
}
}
return instance;
}
/*
从JDK2开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要
volatile关键字,避免指令重排。
*/
形式二:使用内部类
1 | public class LazySingle { |
内部类只有在外部类被调用才加载,产生INSTANCE实例;又不用加锁。
此模式具有之前两个模式的优点,同时屏蔽了它们的缺点,是最好的单例模式。
此时的内部类,使用enum进行定义,也是可以的。
死锁
不同线程分别占用着对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
诱发死锁的原因:
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待
以上4个条件,同时出现就会触发死锁。
解决死锁:
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
方式三:Lock锁的方式解决线程安全问题
Lock的使用
- 创建Lock实例,需要确保多个线程共用一个Lock实例。需要考虑将此对象声明为static final
- 执行lock()方法吗,锁定对共享资源的调用
- 执行unlock(),释放对共享资源的锁定
对比synchronized和Lock:
synchronized不管是同步代码块还是同步方法,都需要在一对{}之后,释放对同步资源的调用
Lock是通过两个方法控制需要被同步的代码,更灵活。Lock作为接口,提供了多种实现类,适合更多复杂的场景,效率更高。
线程通信
涉及到三个方法的使用:
wait():线程一旦执行此方法,就进入等待状态。同时会释放对同步监视器的调用。
notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。如果多个被wait的线程优先级相同,那么就随机唤醒一个。被唤醒的线程从当初被wait的位置继续执行。
notifyAll():唤醒所有被wait的线程。
注意:
- 这三个方法必须在同步代码块或同步方法中使用;
- 这三个方法的调用者必须是同步监视器。否则会报IlleagalMonitorStateException异常
- 这三个方法声明在Object类中
wait()和sleep()区别?
相同点:调用后都会使线程进入阻塞状态
不同点:
-
声明的位置:
- wait():声明在Object类中
- sleep():声明在Thread类中,静态的
-
使用的场景不同:
- wait():只能使用在同步代码块或同步方法中
- sleep():可以在任何需要使用的场景
-
使用在同步代码块或同步方法中:
- wait():一旦执行,会释放同步监视器
- sleep():一旦执行,不会释放同步监视器
-
结束阻塞的方式:
- wait():到达指定时间自动结束阻塞 或 通过被notify唤醒,结束阻塞
- sleep():到达指定时间自动结束阻塞