Java基础笔记7——线程

Java基础笔记7
十三、线程
顶级父类:Thread
1.多线程概念
进程:操作系统中独立运行的程序或服务通常就是一个进程。计算机中可以同时运行多个进程。
线程:一个进程内部,还可以分割为多个并发的线程,每个线程都可以独立的执行相关的任务。一个线程可以认为是程序执行的一条执行路径,在需要时可以在程序中开辟更多执行路径来并行的执行任务。当在一个进程中开启多个线程从而在并行的执行多个任务时,这就是一个多线程的程序。
当程序开启多个线程时,多个线程抢夺cpu,哪个线程抢夺到哪个线程就可以执行,通过不停的抢夺cpu,多个线程都可以得到执行,而cpu处理的速度非常的快,切换线程也非常的快,所以在人看起来这些线程似乎都在并行的执行。
java本身就支持多线程的开发。
jvm虚拟机本身也是个进程,可以通过代码在jvm虚拟机进程中开启多个线程,从而开发多线程的程序。
2.java中多线程的开发
java中有代表线程的类,java.lang.Thread
在这个类中,提供了创建线程、启动线程、操作线程、控制线程等相关的方法,是java多线程开发的核心类。
实现多线程的方法
1.写一个类继承Thread类 ,重写run方法,然后通过start方法启动。
2.实现Runnable接口,重写run方法,然后创建Thread对象。
3.实现Callable接口,重写call方法。
通过观察发现,多个线程的执行的先后顺序并没有任何规律,谁抢到cpu谁就执行。
3.线程的状态切换
线程有如下的几个状态:
未启动:线程对象被创建出来,但是还没有执行start(),并未执行起来
冻结:线程启动了起来,有抢夺执行权限,参与抢夺cpu,但是未抢到cpu状态下,不能得到执行。
执行:线程启动了起来,抢到了cpu,在执行
挂起:线程启动了起来,但是通过sleep或wait进入挂起状态,没有执行权限,不再抢夺cpu,直到sleep结束或wait()被notify或notifyAll方法唤醒,重新进入冻结状态,开始抢夺cpu
结束:线程启动了起来,执行结束或被中断,则线程结束,释放占用的内存和cpu。
总结:sleep和wait有什么区别?
sleep方法需要指定睡眠时间,到点自然醒。释放执行权,不释放锁。被设计在Thread类上,是一个静态方法
wait方法可以指定等待时间也可以不指定,如果不指定时间需要唤醒。释放执行权,释放锁。被设计在了Object类上,是一个普通的方法、wait方法必须结合锁来使用。
状态转换的过程:参考图

4.线程中的常用方法
static Thread currentThread() 获取当前正在执行的线程
long getId() 获取线程的编号,每个线程在执行的过程中,jvm都会为其分配独一无二的编号来唯一的进行表示,可以通过这个方法获取线程的id
String getName() 获取线程的名称,每个线程在执行的过程中,jvm都会为其分配一个名称,这个名称可以通过getName来获取
void setName(String name) 设置线程的名字
Thread.State getState() 获取线程的状态
void interrupt() 中断线程
static void sleep(long millis) 让当前线程进入休眠状态,被挂起,放弃执行权,不再抢夺cpu,持续指定的时间
int getPriority() 获取当前线程的优先级
void setPriority(int newPriority) 设置当前线程的优先级 优先级的取值范围为1~10 越大优先级越高 如果不设置 优先级为5
注意:Thread中虽然提供了stop方法,但是此方法已经被过时,不再有效,并且没有提供替代的方法,因为在线程执行的过程中强制退出,会具有固有的不安全性,可能造成很多意外的不符合预期的结果,所以Thread中现在没有一个明确的退出线程的方法,如果想要正常的退出线程,应该通过线程内代码的设计,来实现线程可以根据不同的状态,正常退出。当线程真的无法正常退出,需要强制退出时,可以选择interrupt方法中断执行。
5.多线程并发安全问题
当多个线程并发执行 并且 操作了同一个共享资源时,由于线程的执行先后顺序是不确定的,产生了一些意外的结果,这样的现象就称之为产生了多线程并发安全问题。
并发安全问题产生的条件:
①多个线程参与
②有共享资源
③同时操作 且 对共享资源的修改
java.lang包在虚拟机启动的时候被自动加载进来,而其他包下的类会在第一次被使用到时加载到内存中,只会加载一次,加载过后会一直保留在内存中,为后续程序服务。
多线程安全问题的解决方法:
突破点:破坏产生同步安全问题的三个条件
分析:(1)多个线程参与 - 无法解决,要的就是多线程。
(2) 有共享资源 - 取消共享资源,让每个线程都具有单独局部变量。 可以解决问题,但是并不适合于所有场景。
(3) 同时操作涉及到修改 - 通常通过控制同时操作的行为解决问题
解决办法:利用同步代码块,利用锁对象,将多个操作共享资源的代码进行隔离。
线程执行到同步代码块时,需要先得到锁对象上锁的控制权,才可以进入同步代码块执行。
这样多个线程,抢夺同一个锁对象上的锁,只有一个线程能够抢夺成功,同一时间内只有一个线程可以获取到锁进入同步代码,来操作共享资源,从而保证了同一时刻共享资源只被一个线程操作,避免了线程安全问题。
如果有多个同步代码块需要同步 则需要多个同步代码块 选择同一个锁对象 关键在于 必须使用同一个锁对象 同一把锁
同步:同一时间内只允许一个线程执行。
异步:同一时间内允许多个线程执行。
同步代码块的锁对象——可以任意的选择, 一般常用的锁对象包括:共享资源对象本身、类的字节码、this等等。
synchronized(lock){
需要同步的代码
}
同步方法:在定义一个方法时,可以在返回值之前 写上一个synchronized关键字,则这个方法就成为了一个同步方法。 所有的线程在进入这个方法之前 都需要得到锁后才可以进入。
锁对象:
1.this (适用于非静态同步方法)
2.当前类的字节码(适用于静态同步方法)
public synchronized void mx(){}
同步:一段逻辑在一个时间段内只允许一个线程执行
异步:一段逻辑在一个时间段内只允许多个线程执行
同步一定安全—是
不安全一定异步—是
安全不一定同步—宏观角度—微观上安全一定同步
6.死锁
当具有多个共享资源时 一部分线程持有一部分资源的锁 要求另外的线程持有的另外的资源的锁 形成了各自持有各自的锁而要求对方的锁的状态 这样 进入了一个互相等待的状态 都无法继续执行 则称之为产生了死锁的情况
死锁产生的条件:
①多个共享资源
②多个线程
③同步嵌套 - 所谓的同步嵌套 就是多个syncronized代码块存在嵌套关系 这就意味着 会产生 持有一个资源 要求另一个资源的情况
解决死锁:避免死锁
突破点: 通过控制产生死锁的条件 保证不会产生死锁
分析:(1)多个共享资源 - 给每个线程都配置该资源 可以避免死锁 但是这并不是所有的时候都可以实现的
(2) 多个线程 - 用单一线程完成任务 但是也不是所有的时候都可以做到的
(3) 同步嵌套 - 尽力的避免同步嵌套 是避免死锁的最好的方式 但是这并不是所有的时候都可以实现的
解决办法:检测死锁,打开死锁。
检测死锁—— 看锁占用的情况是否形成了一个互相要求对方锁环
发现死锁后——打开死锁(异常退出一方),让其他线程可以得到足够的资源继续执行。
7.线程之间的通信
多个线程 相当于进程中独立运行的 流程
每个线程都有各自的 内存空间 包括 栈 堆 都是独立的 无法互相访问对方声明的属性
可以认为 多个线程之间是互相隔离的
但是真正的开发的过程中 是有可能需要在线程之间 传递信息的 如何实现呢?
共享内存
共享内存机制——通过在多个线程都可以访问到的内存位置保存并访问数据 来进行信息的传递。
一般用在多个线程之间, 需要传递属性、信息等场景下。
案例:某一个线程通过控制一个布尔类型的信号量,控制另一个线程执行的流程。
等待唤醒机制
一般是用来在协调过个线程执行先后顺序

		在同步的过程中 必须选择一个锁对象 锁对象 可以是任意的对象 作为锁使用 并不会调用该对象的任何属性和方法 也不会对该对象产生任何额外的影响 仅仅是使用该对象 作为一个锁标记 记录的位置来使用
		锁对象可以是任意选取的对象 而java中任何对象的类 都可以认为是由Object类派生出来的 而在Object类上 定义有 和等待唤醒机制相关的方法
			void wait() //使当前线程 进入挂起的状态 释放cpu 放弃执行权 不再抢夺cpu 释放当前占有的锁 进入一个挂起等待状态 直到被其他线程唤醒 退出挂起状态 如果一直没有其他线程唤醒 会一直等待下去 
			void wait(long timeout) //使当前线程 进入挂起的状态 释放cpu 放弃执行权 不再抢夺cpu 释放当前占有的锁 进入一个挂起等待状态 直到被其他线程唤醒 或 等待超过指定的时间 退出挂起状态 
			void notify() //唤醒一个在当前锁上通过wait进入挂起状态的线程 如果当前锁上有多个wait挂起的线程 唤醒哪一个是随机的 退出挂起状态并不意味着可以立即执行 仍然要抢夺到cpu才可以执行
			void notifyAll() //唤醒所有在当前锁上通过wait进入挂起状态的线程 退出挂起状态并不意味着可以立即执行 仍然要抢夺到cpu才可以执行
		所以 我们可以在同步代码块中 利用锁对象的这些方法 实现等待唤醒机制 从而控制多线程执行的先后的顺序				
		案例:通过等待唤醒机制 实现 之前修改打印案例 修改和打印依次执行的效果

sleep和wait方法的区别:
sleep是Thread对象上调用的 wait是在锁对象上调用的。
sleep方法会设定一个超时时间时间结束后自动醒来,wait方法可以设定也可以不设定超时时间,可以通过notify或notifyAll唤醒,或如果设定过超时时间 在超时时间结束后自动醒来。
wait方法会使当前线程进入挂起状态,会释放cpu 放弃执行权,不再抢夺cpu,释放了锁。sleep方法会使当前线程进入挂起状态,会释放cpu,放弃执行权,不再抢夺cpu,不会释放锁!
案例(了解):实现一个多线程环境下的阻塞式队列的实现
8.多线程中其他的操作
join()加入线程 当某个线程执行的时候 可以调用join将另外一个线程加入进来 则当前线程会释放执行cpu 并转让执行权 给加入进来的线程 直到加入进来的线程执行完成 当前线程才继续执行
1号线程在执行。。。
调用2号线程的join()
1号线程挂起 将执行权转让给2号线程
2号线程得到执行权 开始执行
直到2号线程执行结束 退出 将执行权再还给1号线程
1号线程才继续执行。。。
守护线程Daemon
setDaemon()
用于守护其他线程。如果被守护的线程结束,守护线程无论执行到哪里都随之结束。
一个线程要么是守护线程,要么是被守护的线程。——直到最后一个被守护的线程结束,所有的守护线程才会随之结束。
一个线程只要不手动设置就是被守护线程。-------最常见的守护线程GC、类加载器(classloader)
线程的优先级
优先级0~10,理论上数字越大,优先级越高,抢占CPU的概率越大。
实际应用中,相邻的两个优先级的差异不明显。至少需要想差5个优先级及以上才会相对明显一点。
考虑:引入多线程模型的意义在哪儿?
减少响应时间,提高CPU的利用率。
单例模式
在全局过程中只有一个实例。
对象定义的时候就初始化叫饿汉式
对象在真正使用的时候才初始化叫懒汉式
饿汉式会增加加载过程,懒汉式能够避免这个问题。
懒汉式会出现线程安全问题,饿汉式不会出现线程安全问题
扩展:掌握单例模式的七种方式
单例模式共有7种实现:
第一种(懒汉,线程不安全):
Java代码  
1.public class Singleton {  
2.    private static Singleton instance;  
3.    private Singleton (){}  
4.  
5.    public static Singleton getInstance() {  
6.    if (instance == null) {  
7.        instance = new Singleton();  
8.    }  
9.    return instance;  
10.    }  
11.}  
  这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。
第二种(懒汉,线程安全):
Java代码  
1.public class Singleton {  
2.    private static Singleton instance;  
3.    private Singleton (){}  
4.    public static synchronized Singleton getInstance() {  
5.    if (instance == null) {  
6.        instance = new Singleton();  
7.    }  
8.    return instance;  
9.    }  
10.}  
 
 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
第三种(饿汉):
Java代码  
1.public class Singleton {  
2.    private static Singleton instance = new Singleton();  
3.    private Singleton (){}  
4.    public static Singleton getInstance() {  
5.    return instance;  
6.    }  
7.}  
 
 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
第四种(饿汉,变种):
Java代码  
1.public class Singleton {  
2.    private Singleton instance = null;  
3.    static {  
4.    instance = new Singleton();  
5.    }  
6.    private Singleton (){}  
7.    public static Singleton getInstance() {  
8.    return this.instance;  
9.    }  
10.}  
 
 表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。
第五种(静态内部类):
Java代码  
1.public class Singleton {  
2.    private static class SingletonHolder {  
3.    private static final Singleton INSTANCE = new Singleton();  
4.    }  
5.    private Singleton (){}  
6.    public static final Singleton getInstance() {  
7.    return SingletonHolder.INSTANCE;  
8.    }  
9.}  
 
这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。
第六种(枚举):
Java代码  
1.public enum Singleton {  
2.    INSTANCE;  
3.    public void whateverMethod() {  
4.    }  
5.}  
 
 这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。
第七种(双重校验锁):
Java代码  
1.public class Singleton {  
2.    private volatile static Singleton singleton;  
3.    private Singleton (){}  
4.    public static Singleton getSingleton() {  
5.    if (singleton == null) {  
6.        synchronized (Singleton.class) {  
7.        if (singleton == null) {  
8.            singleton = new Singleton();  
9.        }  
10.        }  
11.    }  
12.    return singleton;  
13.    }  
14.}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值