-多线程-

1.1 线程的概念

1.1.1 线程、进程与程序的区别

程序:为了完成任务,用特定语言编写的一组指令的集合。即一段静态的代码。

进程:正在运行中的应用程序,进程是操作系统调度和分配资源的最小单位,系统会为每个进程分配不同的内存区域。

线程:进程可以进一步细化为线程,是程序内部的一条执行路径,一个进程中至少有一个线程。一个进程同一时间如果可以并行执行多个线程,就是支持多线程的。

线程是cpu的最小的调度和执行单位

1.1.2 线程的内存

一个进程中的多个线程共享相同的内存单元,他们从同一个中分配对象,可以访问相同的变量和对象,提高程序的运行速度。但多个线程操作的共享系统资源可能会有安全隐患,栈和程序计数器是独立的。

注意:不同的进程之间是不共享内存的,进程之间的数据交换和通信成本很高,但它们有时是必要的,比如手机购物付款

1.1.4 线程调度

1.分时调度:所有线程轮流使用cpu的使用权,并且平均分配每个线程占用cpu的使用时间

2.抢占式调度:让优先级高的线程以较大的概率优先使用cpu。如果线程的优先级相同,那么会随机选择,java使用的是抢占式调度。

1.1.4 多线程的优点

背景:在单核Cpu中,使用单线程串行执行任务要比单核并行执行任务要快,为什么还需要多线程?

原因:在单核Cpu的情况下,并行执行任务需要频繁切换线程,导致速度大幅降低,多核Cpu不存在此问题。

现在计算机很少存在单核cpu,故使用多线程有助于:

1.提高程序的相应速度,对图形化界面更有意义,增强用户的体验

2.提高计算机系统的Cpu利用率

3.改善程序结构。将又长又复杂的进程分为多个线程,独立运行,方便修改和理解。

4核cpu一定就比单核cpu性能强4倍?

理论上4核cpu比单核cpu性能强4倍,但其中要考虑到多核心的其它资源共用限制(内存,寄存器和cache)以及多核cpu之间的协调管理消耗。

1.1.5 并行与并发

1.并行(parallel)

并行是指同时执行,比如两个车道的车齐头并进;

在进程中指的是两条线程并行执行。

2.并发(concurrency)

并发并非同时执行,而是看起来像同时执行。并发的原理是在多种任务中快速轮换,比如一个学生写三种不同学科的作业,写一部分会切换至另一学科写,直到写完。

并发分为线程级并发,进程级并发,指令级并发,数据库并发,数据级并发等,其中最常见的为线程级并发。

1.线程级并发是指在多条线程中的每个线程同时非常快速地轮换交替执行,使得宏观上多条线程在同时执行。

注意在单核Cpu中,由于每一时刻只能有一条线程执行,此时线程并发需要Cpu在多个线程内来回切换,拖慢速度,而多核cpu可以把多条线程分配到多个核心内处理,从而真正的实现了并发,大大地提高性能。

2.指令级并发(ILP):指令级并发指的是单个线程内同时执行多个指令,要注意指令级并发并非显式控制的,需要通过设计并发算法和合理地使用线程来利用指令级并发,提高程序的性能。

3.数据库并发:数据库并发控制是指在数据库系统中多个线程同时对数据进行读写操作时的并发管理。数据库系统通过并发控制机制来确保数据的一致性和完整性,并最大限度地提高性能。

4.进程级并发:指操作系统同时执行多个进程的能力

5.数据级并发:数据级并发是指同一时间对多个数据进行操作的能力。

Tip:Juc为并发编程。

1.2 创建和启动线程(重点)

Java中的线程为Thread类,创建线程即创建Thread的类对象(可在JavaApi中搜索到)。

创建方法一:继承Thread类; 原理为Thread类也实现了Runnable接口且Thread类重写了此接口的run()方法,我们创建一个新的对象再次重写Thread类的run()方法.用此对象.start()启动线程。

1.创建一个继承于Thread类的子类

2.重写Thread类的run()方法 -->将此线程要执行的操作声明在此方法中

3.创建当前继承于Thread子类的对象

4.通过对象调用start()启动线程,注意不能用run()代替start // 可以直接用Thread类的匿名实现类创建,要注意使用匿名实现类优先级问题

由于java中是单继承,所以此方法创建的线程有局限性

创建方法二:声明实现 Runnable 接口; 原理:创建新的对象实现此接口并重写Runnable接口run方法,然后调用Thread构造器(对象).start()启动线程。

1.新建一个实现 Runnable接口的类()。

2.重写接口中的run()方法-->将此线程要执行的操作声明在此方法中

3.创建实现了Runnable接口的类对象

4.将创建的类对象作为参数传递到Thread类型的构造器中,创建Thread的实例 (new Thread (创建的类对象)

5.Thread的实例类调用start(),注意不能用run()代替start也可以用Thread类的匿名实现类的匿名对象创建,要注意使用匿名实现类优先级问题

共同点:

1.启动线程使用的都是Thread类中定义的start()方法。

2.创建的线程对象都是Thread类或其子类对象

不同点:

1.一个是类的继承,一个是接口的实现。

建议:通过接口实现,以避免类单继承的局限性,且接口更适合处理共享数据的问题,实现了数据和代码的分离。

使用继承修改共享数据的方法:

1.数据类型加上static静态声明可以在同一类下使用

2.定义一个新的类用于接收非静态的共享数据

//定义一个新的类用于接收非静态的共享数据

联系:public class Thread implements Runnable (Thread类也实现了Runnable接口。代理模式,框架要用到

代理模式:

//tip:

1.线程之间有可能会出现交互,但是线程之间不会互相影响。

2.不能让已经start()的线程再次start(),否则报异常

创建方法三:实现Callable接口(了解)

实现Callable接口需要重写Call()方法,这种实现方式可以有具体的返回值(因为使用了泛型参数),对比Runnable来说Callable可以直接throws抛出异常,要比Runnable更灵活。Runnable接口只能在catch中创建自定义的运行时异常然后抛出。(Thread使用的是Runnable接口注意)

//缺点是如果在主线程中要获取分线程的返回值,此时主线程是阻塞的。

创建方法四:使用线程池(实际开发中常用的)

1.问题背景:如果并发的线程数量极其庞大,且每个线程都是执行一个时间很短的任务就结束了,如果使用以上几种方式,这样频繁地创建、销毁线程就会大大地降低系统的效率,此时线程池就可以发挥巨大的作用。

2.线程池的原理:线程执行完一个任务,不销毁,而是让它继续执行其它任务,思路就是提前创建许多线程,放入线程池中要使用时直接从线程池中获取,使用完毕放回线程池中,这样就可以重复利用,实现系统效率的提升。

3.创建写法:Runnable和Callable接口都可以

首先提供指定线程数量的线程池

ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;

线程池的方法

service1.setMaximumPoolSize(50);//设置线程池中线程的上限
service1.execute(new Window());//创建新线程,需线程对ava应的对象;适用Runnable
service1.execute(Callable callable);//创建新线程,需线程对应的对象;适用Callable
service1.shutdown();//关闭线程池

//注意:上方直接使用Executors创建线程的方式是禁止使用的!因为它返回的ExecutorService接口的实例会有资源耗尽的风险,详见下方阿里文档:

ThreadPoolExecutor的构造方法参数:

参数名解释备注
int corePoolSize线程池的线程数量(核心线程数)不能小于0
int maximumPoolSize线程池可支持的最大线程数最大数量必须>=核心线程数
long keepAliveTime所有空闲线程的最大存活时间不能小于0
TimeUnit unit指定存活时间的单位(秒,分,时,天)时间单位
BlockingQueue<Runnable> workQueue任务队列
ThreadFactory threadFactory指定哪个线程工厂创建线程
RejectedExecutionHandler handler拒绝策略

ThreadPool使用示例:

    ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
    ThreadPoolExecutor pool = new ThreadPoolExecutor(
            3, // 核心线程数
            10,// 最大线程数
            10, // 最大存活时间
            TimeUnit.SECONDS,// 时间单位
            queue);// 阻塞队列

    // 给线程池提交任务
    for (int i = 1; i < 30; i++) {
        pool.execute(new Runnable( ) {
            @Override
            public void run() {
                Thread thread = Thread.currentThread( );
                System.out.println(thread.getName( ) + "执行任务");
            }a
        });
    }
}

上方为ThreadPoolExecutor的7个简易参数,可以使用ArrayBlockingQueue或BlockingQueue的类对象来创建线程,最后创建ThreadPoolExecutor工具类对象来管理该线程池,线程工厂和拒绝策略不定义会执行默认设定,注意这只是一个简单的替代方案,更复杂的方案需要学习JUC并发编程。

1.3 Thread类的常用方法和生命周期

1.3.1 Thread类的常用方法

Thread类的构造方法:

1.public Thread() 分配一个新的线程

2.public Thread(String name) 分配一个指定名字的新的线程,Thread子类的构造方法可以使用super()将name返回。

3.public Thread(Runnable target) 分配一个指定目标的新的线程

4.public Thread(Runnable target , String name) 分配一个指定名字且指定目标的新线程,Thread子类的构造方法可以使用super()将name返回。

Thread类的常用方法:

1.start(): 启动线程,调用当前线程(Thread类)的run()方法

2.run():将线程要执行的操作声明在run()方法中,一般通过重写的方式使用。

run方法内不能throws,因为所有线程的父类Thread的run()方法没有throws,具体参考异常中的子类重写异常的问题。

3.currentThread(): 获取当前执行代码对应的线程

4.getName(): 获取线程名

5.setName():设置线程名 (不推荐,要在start前中使用,除非修改主线程,建议使用构造方法创建)

6.sleep(long millis): 不论哪个对象调用了此方法,调用此方法的线程都会暂停毫秒值。默认会抛出编译时异常,需要处理。

7.yield():静态方法,不论是哪个对象调用了该方法,调用此方法所在的线程都会释放cpu的执行权(注意是毫秒级,故只适用于特定场景);

8.join():不论是哪个对象调用了该方法,调用此方法所在的线程都会进入阻塞状态。(一般用于联网获取数据)

例如:ever.join():不论是哪个对象调用了该方法,调用此方法的线程都会进入阻塞状态,等待ever线程执行完毕。

坑:this.join() 调用此方法的线程进入阻塞状态,等待它自己执行完毕?会进入无限等待,要带上超时值。

- sleep是Thread类的静态方法,wait是Object类的方法
- sleep让线程等待不执行,在加锁时,不让出资源 ;
  wait会让出资源
- sleep()可以用在非同步代码中;wait()必须放在同步代码中且被锁对象调用

9.isAlive():判断调用此方法的线程是否存活。

过时方法:

1.stop() 强行结束一个线程,直接进入死亡状态。不建议使用,可能会导致数据不一致和清理工作无法完成

2.void suspend() / void resume() 线程挂起和线程恢复,一般成对使用,但是操作不当会造成死锁问题,不推荐使用。

线程的优先级

1.getPriority() :获取线程的优先级

2.setPriority() :设置线程的优先级[1-10]

3.Thread类内部声明的三个常量

-MAX_PRIORITY(10) 最高优先级

-MIN_PRIORITY(1) 最低优先级

-NORM_PRIORITY(5) 普通优先级

·

1.3.2 Thread线程的生命周期

//JDK1.5前

JDK1.5前分为新建,就绪,运行,阻塞,死亡五种状态,

//JDK 1.5 后

1.4 线程的安全问题与线程的同步机制(重点)

背景:当多个线程同时修改数据时,就会出现安全问题,例如两个线程同时进入if语句出现阻塞导致多重计算,结果超出预料的情况。

如下图,当票数为共享状态时购票线程遭遇阻塞(访问人数爆炸而出现的阻塞延迟问题),也就是当前线程停留在if判断完毕尚未完成对ticket票的操作时其它线程参与了进来对ticket票进行了操作,大量的线程有此问题就会出现大量的重票和负票的情况(只要有10ms的延迟,不处理就会出现此状况)。

//解决方法:一个线程在操作共享数据时其它的线程必须等待,直到操作共享数据的线程执行完毕。Java使用线程的同步机制来解决安全问题。

方式一:同步代码块

方法:synchronized(同步监视器){

//需要被同步的代码

}

1.需要被同步的代码就是需要被操作的共享数据

2.共享数据:多个线程都可以操作的数据

3.被synchronized包裹以后,一个线程在操作时其它的线程就必须等待。

4.同步监视器就是锁,谁获得了锁,谁就可以修改共享数据

5.锁可以使用任何一个类充当,但是操作共享数据的多个线程必须共用同一个锁(唯一性),此外,最好选择不可变的对象作为锁,如果不确定,就创建一个空的对象或当前类.class(反射)作为锁//尤其是使用继承Thread类的方式下同步监视器时慎用this且创建Object对象需要加上static注意。

6.要注意声明位置,避免单线程进入执行出不来导致性能降低。

方式二:同步方法

如果操作共享数据的代码完整地声明在了一个方法中,那么将此方法声明为同步方法即可。

1.synchronized要被声明在方法中的返回值类型之前,在使用中依旧需要注意声明位置,避免单线程进入执行出不来导致性能降低。

2.此方式的同步监视器的对象是this,所以要注意唯一性,解决方法之一是可以将方法声明成静态的,但要注意此方式的监视器对象无法修改,如果方法声明静态不适合就老实的去用同步代码块。

synchronized锁的好处与弊端

好处:解决了线程的安全问题;

弊端:多线程在操作synchronized 包裹的数据时是串行执行,这意味着性能的下降。(数据库也有此问题,并发问题,隔离级别),优化思路是读取可以多线程并行,写入的优化除了串行之外另外一种方法可能会导致脏数据,暂时不在这里提出.

1.5 单例之懒汉式的线程安全问题

术语解释:单例是指一种设计模式,目的是确保一个类内只能创建一个对象(即只能new一次),且这个单例类可以在任何地方被访问!这对于共享数据的访问非常有用。

1.饿汉式:在类加载时就将对象创建出来;

2.懒汉式(需要同步机制处理):在首次访问时才创建对象,但是在多线程的情况下会出现线程安全问题,需要处理。

//如下图懒汉式同步监视器的最优处理方式,但此方式存在指令重排的问题:

指令重排:指令重排是指处理器为了提高执行速度而对指令执行的顺序进行重新排序的一种优化技术,在懒汉式中创建对象时可能会导致一些问题。

在多线程下,new对象时由于Cpu指令重排的特性,其线程除了对象的init()初始化方法之外其他操作均执行完毕,所以此时图中的Instance已经非null,如果此时其它线程进入,会直接判断Instance非null返回,那么图中示例就会有概率出现空指针异常(NullPointerException);

解决方法:

1.共享的数据前加入volatile

2.双重检查锁 // 未看

3.静态内部类 //未看

Tip:在典型的单例模式的设计中,类创建的对象通常来说应当是类自己本身。但是理论上可以使用任意对象,如果创建其它对象可能会导致非唯一对象(比如使用非自类被继承)。

1.6 线程同步机制的弊端:死锁(deadlock)

1.定义:死锁是指不同的线程分别占用对方需要同步的资源且都在等待对方放弃自己需要同步的资源就形成了死锁。

例如:线程A占用了同步监视器A,需要同步监视器B的同时线程B占用了同步监视器B,需要同步监视器A。

死锁不会报任何异常和提示,但所有线程都会进入阻塞状态无法继续运行。

//注意编写程序时应当尽量避免死锁!

2.死锁形成的原因

1.互斥条件:互斥无法解决,互斥是同步监视器所必须的。

2.占用等待:指线程占用了一个同步监视器,等待另一个同步监视器的释放叫做占用等待。

3.不可抢夺:线程只能等待另一个同步监视器释放,不能抢过来执行。

4.循环等待:最终导致互相等待对方先释放同步监视器,永久阻塞。

解决方法:

1.一个线程释放完全部的锁以后另一个线程再请求用到的锁,会降低性能

2.占用监视器的线程在申请下一个监视器时,如果无法申请到目标监视器,那么就先释放自己的监视器(自己持有一个锁时尽量避免再去请求另一个锁)

3.所有线程按照同样的顺序请求同步监视器,就不会造成死锁,因为会获取自己用不到的锁,所以性能比第一种还低。

4.使用JDK5.0新增的Lock锁。(推荐)

死锁例题:

 

1.7 Lock锁

java.util.concurrent.locks (JUC, JAVA并发编程后期会学。)

使用方法:

1.创建Lock的类对象ReentrantLock,确保多个线程使用同一个Lock对象(static),尤其是继承时。

2.调用Lock对象的lock()方法,锁定对共享资源的调用

3.调用Lock对象的unlock()方法,释放对共享资源的锁定,且最好放在finally中,如果请求不成功,那么依旧可以释放共享资源的锁定。

//Lock作为接口提供了多种实现类,不仅适合更复杂的场景,更灵活,而且效率比synchronized更好。

1.8 线程之间的通信机制

释义:线程之间是独立的,当某个线程需要与其它线程产生交互时就需要特殊的机制来实现通信,线程唤醒的前提是同步监视器(synchronized)。//注意不能用在Lock锁内!Lock需要Condition配合

例如:线程A执行到一定程度时停止,要通知其它的线程,让其它之前暂停的线程继续执行!

1.8.1 线程的唤醒机制

注意此节内容算是线程之间的通信机制,但线程之间的通信机制也涉及许多其他内容。

1.wait() / wait(time) 等待,必须在同步监视器内调用。

当某个线程满足一定的条件进入wait()方法时会进入休眠状态并释放对同步监视器的占用(Sleep不会释放),不占用cpu资源,如果不带有time时间限制,那么只有使用notify()/notifyAll方法唤醒该线程,否则此线程会永久沉睡,有可能会引起死锁和程序假死问题.。

带有time时间限制等时间结束后该线程便会自动被唤醒。

线程被唤醒后会重新进入调度队列,继续执行所在代码,不会优先调度注意。

2.notify() 唤醒在当前对象休眠的一个优先级最高的线程,如果优先级相同则随机唤醒一个,从被wait()的位置继续执行。必须在同步监视器范围内调用。

3.notifyAll(),唤醒在当前对象休眠的全部线程,必须在同步监视器范围内调用。

//注意:等待和唤醒方法没有显式地表现出调用者时它们的调用者都是this,且同步监视器、notify和wait的调用对象必须都是同一个,且必须都在同步监视器范围内。

wait和sleep的区别:

相同点:一旦执行都会进入阻塞状态

不同点:

1.声明位置不同,wait声明在Object类中,sleep声明在Thread中且是静态的

2.使用场景不同:wait只能用在同步代码块或同步方法中,sleep无使用限制。

3.如果使用在同步监视器中:wait会释放同步监视器,sleep不会释放同步监视器

4.结束阻塞的方法:wait可以计时自动结束,也可以手动结束。sleep只能计时自动结束。

//1.8.2 线程之间共享内存通信

//1.8.3 线程之间并发队列、管道和消息中间件

//1.8.4 线程的信号量(Semaphore)

//1.8.5 线程条件变量(Condition)

//1.8.6 异步编程模型:

附录

线程中匿名实现类的优先级问题

1.匿名内部类:匿名内部类在声明和创建对象的同时定义该对象的类,通常这会是接口子类,且可以通过实例初始化块来模拟构造函数。

所以,第13行的new Thread(everNumber){}通过有参构造函数创建了一个匿名的Thread的子类,然后传入了一个了Runnable类型的everNumber类,最后.start()启动了这个匿名内部类的线程(就相当于everNumber.start() )。

此时根据构造函数的特性(先父类后子类),Thread类的构造函数会将其定义的属性初始化(everNumber类),属性初始化完毕后由于是匿名内部类启动的线程,随后便会对匿名内部类进行初始化。Thread会优先检查匿名内部类内是否存在重写的run()方法(匿名内部类也是Thread的子类),如果存在则直接调用Runnable接口执行。

由于是匿名类启动的线程,所以即使下方继承Thread的对象改成实现Runnable接口,匿名内部类的run()方法优先级依旧比下方对象的run方法()高。

此时,如果将匿名类中重写的run()方法删除,当Thread检查匿名内部类重写的run()方法不存在时,由于指向了everNumber类,Thread便会调用EverNumber类中重写的run()方法。

// 注意Thread构造方法内的init()方法是调用的Thread类下的普通init()方法,可以ctrl+左键跳转过去。且要注意继承的构造方法是先创建父类后创建子类!

 

2.线程的安全问题和死锁问题不是每次都必定出现,有出现的可能时要提前预料到。

  • 46
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

we are a cloudED

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值