目录
ReentrantLock的使用(另一种解决线程安全问题的方式)
程序、进程和线程与并行、并发的概念
程序(program):为了完成特定的任务,用某种语言编写的一组指令,即指一段静态的代码
进程(process):程序的一次执行过程。程序是静态的,进程是动态的。进程作为操作系统调度和分配资源的最小单位
线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。线程作为CPU调度和执行的最小单位。
并行(parallel):指两个或多个事件在同一时刻发生(同时发生),有多条指令在多个CPU上同时执行。
并发(concurrency):指两个或多个事件在同一时间段发生。即在一段时间内,有多条指令在单个CPU上快速轮换、交替执行,使得宏观上具有多个进程同时执行的效果
创建和启动线程(重点)
创建方式1:继承Thread类
- 1.1创建一个继承于Thread类的子类
- 1.2重写Thread类的run( )--->将此线程要执行的操作,声明在此方法体中。
- 1.3创建当前Tread的子类的对象
- 1.4通过对象调用start( ):
- 1.启动线程
- 2.调用当前线程的run()方法
- 注意:
- 不能让已经start( )的线程,再次执行start( ),否则会报异常IllegalThreadStateException,每个创建线程都会有一个status变量,第一次调用start方法时此staus会改变,当再次调用时,会先判断这个状态变没变,如果变了就会报错。
- 不能调用用对象调用run()方法,如果这样做,实际只是在一个线程中,先调用了run方法里面的内容而已,并没有启动线程;所以start也有启动线程的重要功能。
//创建继承于Tread类的子类
class PrintNumber extends Thread{
//重写run方法
public void run(){
for(int i=1;i<=100;i++){
if(i%2==0)
System.out.println(i);
}
}
}
public class EvenNumberTest{
public static void main(String[] args){
//创建当前Thread的子类对象
PrinterNumber t1 = new PrinterNumber();
//通过对象调用start方法
t1.start();
}
}
创建方式2:实现Runnable接口
- 2.1创建一个实现Runnable接口的类
- 2.2实现接口中的run( )方法
- 2.3创建当前实现类的对象
- 2.4将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
- 2.5Thread类的实例调用start( ):1.启动线程2.调用当前线程的run()方法
//创建实现Runnable接口的类
class PrintNumber implements Runnable{
//重写run方法
public void run(){
for(int i=1;i<=100;i++){
if(i%2==0)
System.out.println(i);
}
}
}
public class EvenNumberTest{
public static void main(String[] args){
//创建对象
PrinterNumber p = new PrinterNumber();
//作为参数传递到Thread类的构造器中
Thread t1 = new Thread(p);
//通过对象调用start方法
t1.start();
}
}
对比两种方式:
- 共同点:①启动线程,使用的是Thread类中定义的start( ) ②创建的线程对象都是Thread类或其子类的对象
- 不同点:一个是类的继承,一个是接口的实现
- 建议使用使用Runnable接口的方式
- Runnable方式的好处:
- 实现的方式,避免了类的单继承性
- 更适合处理有共享数据的问题
- 实现了代码和数据的分离
- Runnable方式的好处:
两个方法的联系:在源码中,Thread类是这样定义的:public class Thread implements Runnable(代理模式),所以其实Thread也是实现Runnable接口的类
Thread类的常用方法和生命周期
线程中的构造器
- public Thread( ):分配一个新的线程对象
- public Thread( String name):分配一个指定名字的新的线程对象
- public Thread( Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run()方法。
- public Thread( Runnable target ,String nam):分配一个带有指定目标新的线程对象并指定名字。
class PrintNumber extends Thread{
public PrintNumber(){
}
public PrintNumber(String name){
super(name);
}
//……………………
public void run(){
for(int i=1;i<=100;i++){
if(i%2==0)
System.out.println(i);
}
}
}
线程中常用的方法
- start( ):1.启动线程;2.调用线程中的run( )。
- run( ):将线程要执行的操作,声明在run( )方法中。
- currentThread( ):获取当前执行代码对应的线程。
- getName( ):获取线程名。
- setName( ):设置线程名字(会抛出来非运行异常,记得处理)。
- sleep(long millis ):静态方法,调用时,可以使得当前线程睡眠指定毫秒数。
- yield() :静态方法,一旦执行此方法,就要释放CPU的执行权。(可能会被别的线程抢到,也可能抢不到)。
- join( ):在线程a中通过线程b调用join( ),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行。
- isAlive( ):判断当前线程是否存活。
过时方法:
- stop( ):强行结束一个线程的执行,直接进入死亡状态。不建议使用(可能造成资源不能被关闭,造成内存泄漏)
- void suspend( )/void resume( ):可能造成死锁,不建议使用
线程的优先级
- getPriority( ):获取线程优先级
- 三个全局变量
- MAX_PRIORITY(10)最高优先级
- NORM_PRIORITY(5)普通优先级
- MIN_PRIORITY(1)最低优先级·
- 三个全局变量
- setPriority():(范围1到10),设置优先级。
还是会有交互的,不过高优先级分配CPU的概率会更高,并不是一定先执行完。
生命周期
(jdk5之前)
新建、就绪、运行、死亡、阻塞(临时状态)5种状态
jak5之后(阻塞细分了)
阻塞细分为锁阻塞、计时等待、无限等待
同步代码块解决两种线程创建方式的线程安全问题(重点)
生活中的例子:
卖火车票,
- 多线程卖票,出现的问题:重票和错票
- 什么原因导致的?
- 线程操作ticket过程中,尚未结束的情况下,其他线程也参与进来,对ticket进行操作
- 如何解决?必须保证一个线程在操作ticket的过程中,其他线程必须等待,直到该线程操作ticket结束之后,其它线程才可以进来继续操作ticket。
使用线程同步机制解决
方式1:同步代码块
synchronized(同步监视器)}{//需要被同步的代码}
说明
- 需要被同步的代码:即为操作共享数据的代码
- 共享数据:即多个线程都需要操作的数据。比如ticket
- 需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。
- 同步监视器(锁):哪个线程获取了锁,哪个线程就能执行需要被同步的代码
- 同步监视器,可以使用任何一个类的对象充当。但是多个线程必须公用同一个同步监视器。
- 注意:实现接口的方式中锁可以用this,继承的方式中锁慎用this,可以用当前类.class来代换。
方式2:同步方法
- 如果操作的共享数据的代码完整的声明在了一个方法中,那么就可以将此方法声明为同步方法即可
- 非静态的同步方法,默认同步监视器是this
- 静态的同步方法,默认同步监视器是当前类本身(当前类.class)
优缺点
- 好处:解决了线程的安全问题
- 弊端:在操作共享数据时,多线程其实是串行执行的,意味着性能低一些
解决单例模式中的懒汉式的线程安全问题
代码演示
class Bank{
private Bank(){}
private static volatile Bank instance = null;//volatile避免指令重排,下面的两种方式其实都应该加上。主要是防止new Bank(),操作后,虽创建了对象,但并没有执行init方法,就return返回了,这时instance还是null,可能会造成线程的不安全问题。,
//方式1:同步方法
public static synchronized Bank getinstance(){
if(instance == null){
instance = new Bank();
}
return instance;
}
}
class Bank{
private Bank(){}
private static Bank instance = null;
//方式2:同步代码块
public static Bank getinstance(){
synchronized(Bank.class){
if(instance == null){
instance = new Bank();
}
return instance;
}
}
}
class Bank{
private Bank(){}
private static Bank instance = null;
//方式3:效率更高
public static Bank getinstance(){
if(instance == null){//这次判断是优化,使创建单例之后,其他线程直接不执行以下操作
synchronized(Bank.class){
if(instance == null){
instance = new Bank();
}
return instance;
}
}
}
}
死锁
线程的同步机制带来的问题
理解:
不同的线程分别占用对方需要的同步资源不放弃,都在等对方放弃自己需要的同步资源,就形成了线程的死锁。我们编写程序时,要避免出现死锁。
诱发死锁的原因?以下四个条件同时出现就会出现死锁
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待
如何解决?
以下条件,破坏一个条件就解决了
- 针对条件1:基本改变不了
- 针对条件2:可以考虑一次性申请所有所需资源,这样就不存在等待的问题了
- 针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已占用的资源
- 针对条件4:将资源改为线性排序。申请资源时,先申请序号小的,避免循环等待的问题。
ReentrantLock的使用(另一种解决线程安全问题的方式)
步骤:
- 1.创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为static final
- 2.执行lock()方法,锁定对共享资源的调用
- 3.unlock()的调用,释放对共享数据的锁定
面试题synchronized同步的方式于Lock的对比?
- synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用
- lock是通过两个方法控制需要被同步的代码块,更灵活
- lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高
线程的通信机制与生产者消费案列(不是重点)
想看了看一下
线程间通信的理解
当我们“需要多个线程”来完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调他们的工作,以此实现多线程共同操作一份数据
涉及到三个方面:
- wait( ):线程一旦执行此方法,就会进入等待状态,同时会释放对同步监视器的调用
- notify():一旦执行此方法,就会唤醒被wait( )的线程中优先级最高的那个,(如果优先级一样,则随机唤醒),被唤醒的线程从当初被wait()的位置继续执行
- notifyAll():一旦执行此方法,就会唤醒所有被wait()的线程。
- 注意点:
- 此三个方法的使用必须是在同步代码块或同步方法中(lock需要配合Condition实现线程的)
- 此三个方法的调用者必须是同步监视器。否则会报异常。
- 此三个方法声明在Object类中。
wait和sleep的区别
- 相同点:一旦执行,当前线程都会进入阻塞状态
- 不同点:
- 声明的位置:wait声明在Object中,sleep声明在Thread中、静态方法
- 使用场景:wait只能使用在同步代码块和同步方法中,sleep可以在任何需要的场景使用
- 使用在同步代码块和同步方法中,wait会释放同步监视器,sleep不会释放
- 结束阻塞的方式:wait到达指定时间自动结束阻塞,或者通过被notify唤醒结束阻塞。sleep到达指定时间主动结束阻塞
消费者&生产者
clerk
生产者
消费者
测试
线程的创建方式(5.0新增特性)(2个)
Callable(相较于Runnable)
- call() 可以有返回值,更灵活
- call() 可以使用throws的方式解决异常
- Callable使用了泛型参数,可以指明call() 的返回值类型
- 如果需要在主线程当中获取分线程call( )的返回值,则此时的主线程是阻塞状态的
线程池(juc学)
- 好处:
- 提高了程序执行的效率。(因为线程已经提前创建好了)
- 提高了资源的复用率,因为执行完的线程并未销毁,而可以执行其他任务
- 可以设置相关参数,对线程池中的线程的使用进行管理